From 32448a5abaf7e405303c8b9c453f4efa8ae1d54c Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 22 Oct 2026 00:36:37 -0700 Subject: [PATCH 1/8] test: run every test that is skipped on Windows About 1,000 tests, or parts of tests, are skipped on Windows, and nobody has checked recently whether each skip is still needed. This removes all of them so that CI shows which ones really fail. - Removes the Windows term from `skipIf`, `todoIf`, `if`, ternaries, early returns and `if (!isWindows)` guards. Other terms stay: `skipIf(isWindows || isRoot)` becomes `skipIf(isRoot)`. - Removes the Windows guards that Bun added to 10 vendored Node.js tests. Skips that come from Node.js itself stay. - Leaves Windows-only, Linux-only and macOS-only tests alone. Not meant to merge as is: skips that CI proves necessary go back in. --- test/bake/deinitialization.test.ts | 40 +- test/bake/dev/production.test.ts | 4 +- test/bake/serve-plugins-dev-server.test.ts | 4 +- test/bundler/bun-build-api.test.ts | 87 ++- test/bundler/bun-build-compile.test.ts | 190 +++--- test/bundler/compile-corrupted-embed.test.ts | 5 +- test/bundler/metafile.test.ts | 4 +- test/bundler/native-plugin.test.ts | 2 +- test/cli/bake/bake.test.ts | 151 +++-- test/cli/bun.test.ts | 4 +- test/cli/check/check.test.ts | 53 +- test/cli/create/create-jsx.test.ts | 18 +- test/cli/format/format.test.ts | 63 +- test/cli/format/oxfmt-cli/oxfmt-cli.test.ts | 4 +- .../format/prettier-cli/prettier-cli.test.ts | 4 +- test/cli/format/tailwind/tailwind.test.ts | 4 +- test/cli/heap-prof.test.ts | 43 +- test/cli/hot/hot.test.ts | 2 +- test/cli/hot/watch.test.ts | 4 +- test/cli/init/init.test.ts | 46 +- test/cli/inspect/BunFrontendDevServer.test.ts | 4 +- test/cli/inspect/HTTPServerAgent.test.ts | 4 +- test/cli/inspect/inspect.test.ts | 4 +- test/cli/inspect/test-reporter.test.ts | 4 +- test/cli/install/bad-workspace.test.ts | 69 ++- test/cli/install/bun-add-filter.test.ts | 29 +- test/cli/install/bun-add.test.ts | 7 +- test/cli/install/bun-create.test.ts | 12 +- test/cli/install/bun-install-backends.test.ts | 2 +- test/cli/install/bun-install-env-file.test.ts | 4 +- test/cli/install/bun-install-git-deps.test.ts | 245 ++++---- .../bun-install-lifecycle-scripts.test.ts | 186 +++--- .../cli/install/bun-install-link-path.test.ts | 2 +- .../bun-install-native-binlink.test.ts | 123 ++-- test/cli/install/bun-install-patch.test.ts | 9 +- test/cli/install/bun-install-registry.test.ts | 87 ++- .../bun-install-security-provider.test.ts | 4 +- test/cli/install/bun-install-staging.test.ts | 192 +++---- .../bun-install-streaming-extract.test.ts | 2 +- test/cli/install/bun-install.test.ts | 10 +- test/cli/install/bun-link.test.ts | 14 +- test/cli/install/bun-lock.test.ts | 4 +- test/cli/install/bun-lockb.test.ts | 2 +- test/cli/install/bun-pack.test.ts | 32 +- test/cli/install/bun-patch.test.ts | 23 +- test/cli/install/bun-pm-cache-pack.test.ts | 128 ++--- test/cli/install/bun-pm-diff.test.ts | 144 +++-- test/cli/install/bun-pm-pkg.test.ts | 4 +- test/cli/install/bun-pm.test.ts | 10 +- test/cli/install/bun-prune.test.ts | 83 ++- test/cli/install/bun-publish.test.ts | 6 +- test/cli/install/bun-remove.test.ts | 9 +- test/cli/install/bun-run-bunfig.test.ts | 2 +- test/cli/install/bun-run.test.ts | 12 +- test/cli/install/bun-upgrade.test.ts | 10 +- .../bun-workspaces-self-contained.test.ts | 19 +- test/cli/install/bun-workspaces.test.ts | 24 +- test/cli/install/bunx-directories-bin.test.ts | 6 +- test/cli/install/bunx.test.ts | 53 +- .../install/escape-control-characters.test.ts | 2 +- test/cli/install/install-fd-leak.test.ts | 4 +- test/cli/install/isolated-install.test.ts | 183 +++--- .../migration/pnpm-lock-migration.test.ts | 4 +- .../install/migration/pnpm-lock-v9.test.ts | 54 +- .../install/migration/pnpm-migration.test.ts | 4 +- test/cli/install/migration/yarn-berry.test.ts | 48 +- test/cli/install/npmrc.test.ts | 6 +- test/cli/install/redacted-config-logs.test.ts | 6 +- test/cli/install/shebang-normalize.test.ts | 2 +- .../install/symlink-path-traversal.test.ts | 540 +++++++++--------- test/cli/lint/config-files.test.ts | 10 +- test/cli/lint/conformance.test.ts | 2 +- test/cli/lint/explain.test.ts | 6 +- test/cli/lint/git.test.ts | 150 +++-- test/cli/lint/help.test.ts | 14 +- test/cli/lint/init.test.ts | 6 +- test/cli/lint/lint.test.ts | 103 ++-- test/cli/run/as-node.test.ts | 4 +- test/cli/run/cpu-prof.test.ts | 6 +- test/cli/run/env.test.ts | 16 +- test/cli/run/filter-workspace.test.ts | 14 +- test/cli/run/garbage-env.test.ts | 4 +- test/cli/run/markdown-entrypoint.test.ts | 4 +- test/cli/run/multi-run.test.ts | 6 +- test/cli/run/no-orphans.test.ts | 16 +- test/cli/run/require-cache.test.ts | 2 +- test/cli/run/run-autoinstall.test.ts | 10 +- test/cli/run/run-crash-handler.test.ts | 81 ++- test/cli/run/run-extensionless.test.ts | 4 +- test/cli/run/run-propagate-sigkill.test.ts | 6 +- test/cli/run/run-shell.test.ts | 75 ++- test/cli/run/transpiler-cache.test.ts | 2 +- test/cli/run/tsconfig-override.test.ts | 4 +- test/cli/test/bun-test.test.ts | 165 +++--- test/cli/test/parallel.test.ts | 113 ++-- test/cli/test/test-changed.test.ts | 4 +- .../cli/update_interactive_formatting.test.ts | 12 +- test/cli/watch/watch.test.ts | 249 ++++---- test/config/bunfig/bunfig-errors.test.ts | 4 +- test/integration/esbuild/esbuild.test.ts | 4 +- .../next-pages/test/dev-server.test.ts | 12 +- test/integration/pprof/pprof.test.ts | 4 +- test/internal/build-link-lazy-objects.test.ts | 8 +- .../build-rust-toolchain-probe.test.ts | 4 +- .../internal/build-toolchain-identity.test.ts | 8 +- test/internal/rust-check-all.test.ts | 4 +- test/internal/rust-windows-sys-link.test.ts | 28 +- test/internal/sigaction-layout.test.ts | 3 +- .../source-lints/windows-cross-config.test.ts | 3 +- test/js/bun/archive.test.ts | 10 +- .../compile/standalone-madvise-tla.test.ts | 6 +- test/js/bun/console/console-write.test.ts | 4 +- test/js/bun/dns/resolve-dns.test.ts | 2 +- test/js/bun/ffi/cc.test.ts | 51 +- test/js/bun/gc/gc-controller-cadence.test.ts | 2 +- test/js/bun/glob/path-length.test.ts | 4 +- test/js/bun/glob/scan.test.ts | 6 +- .../bun/http/bun-listen-connect-args.test.ts | 4 +- test/js/bun/http/bun-serve-args.test.ts | 4 +- test/js/bun/http/bun-serve-file.test.ts | 10 +- test/js/bun/http/bun-serve-html-405.test.ts | 6 +- test/js/bun/http/bun-serve-ssl.test.ts | 45 +- test/js/bun/http/fetch-file-upload.test.ts | 56 +- test/js/bun/http/http-server-chunking.test.ts | 8 +- test/js/bun/http/proxy.test.ts | 35 +- .../http/serve-direct-readable-stream.test.ts | 4 +- .../serve-stream-reject-flush-leak.test.ts | 42 +- test/js/bun/http/serve-syscall-fault.test.ts | 4 +- test/js/bun/http/serve.test.ts | 9 +- test/js/bun/image/image-adversarial.test.ts | 2 +- test/js/bun/image/image.test.ts | 2 +- test/js/bun/io/bun-write.test.js | 8 +- test/js/bun/jsc/bun-jsc.test.ts | 6 +- .../bun/module-graph/module-graph-io.test.ts | 2 +- .../module-graph-isolation.test.ts | 34 +- test/js/bun/net/socket-retention.test.ts | 2 +- test/js/bun/net/socket-syscall-fault.test.ts | 2 +- test/js/bun/net/socket.test.ts | 173 +++--- test/js/bun/net/tcp-server.test.ts | 4 +- .../net/tls-reject-before-client-cert.test.ts | 4 +- test/js/bun/net/unix-socket-unlink.test.ts | 4 +- test/js/bun/patch/patch.test.ts | 15 +- test/js/bun/perf/linker-order.test.ts | 73 ++- test/js/bun/repl/repl.test.ts | 8 +- ...dynamic-import-evaluation-error-gc.test.ts | 40 +- test/js/bun/resolve/resolve-error.test.ts | 40 +- test/js/bun/resolve/resolve.test.ts | 12 +- ...esolver-permission-denied-ancestor.test.ts | 4 +- test/js/bun/shell/bunshell.test.ts | 117 ++-- test/js/bun/shell/commands/cat.test.ts | 6 +- test/js/bun/shell/commands/cp.test.ts | 4 +- test/js/bun/shell/commands/ls.test.ts | 16 +- test/js/bun/shell/commands/rm.test.ts | 219 ++++--- test/js/bun/shell/commands/which.test.ts | 2 +- test/js/bun/shell/epipe.test.ts | 6 +- test/js/bun/shell/file-io.test.ts | 4 +- test/js/bun/shell/shell-blocking-pipe.test.ts | 5 +- test/js/bun/shell/shell-cmdsub-crash.test.ts | 4 +- test/js/bun/shell/shell-load.test.ts | 4 +- .../shell/shell-worker-terminate-leak.test.ts | 16 +- test/js/bun/shell/shelloutput.test.ts | 4 +- test/js/bun/spawn/bun-ipc-inherit.test.ts | 6 +- test/js/bun/spawn/spawn-kill-signal.test.ts | 2 +- test/js/bun/spawn/spawn-maxbuf.test.ts | 2 +- test/js/bun/spawn/spawn-path.test.ts | 4 +- test/js/bun/spawn/spawn-pipe-leak.test.ts | 12 +- .../spawn-pipe-stale-fd-unregister.test.ts | 4 +- test/js/bun/spawn/spawn-renamed-cwd.test.ts | 6 +- .../spawn/spawn-socketpair-shutdown.test.ts | 4 +- .../spawn/spawn-stdin-pipe-fd-leak.test.ts | 8 +- .../spawn/spawn-stdin-readable-stream.test.ts | 50 +- .../spawn-stdout-filereader-gc-uaf.test.ts | 295 +++++----- .../spawn/spawn-stdout-iterate-leak.test.ts | 4 +- .../spawn/spawn-sync-loop-init-fail.test.ts | 4 +- .../bun/spawn/spawn-unread-stdout-gc.test.ts | 54 +- test/js/bun/spawn/spawn.ipc.bun-node.test.ts | 180 +++--- test/js/bun/spawn/spawn.ipc.test.ts | 41 +- test/js/bun/spawn/spawn.test.ts | 91 ++- test/js/bun/spawn/spawnSync.test.ts | 12 +- .../spawnsync-isolated-event-loop.test.ts | 4 +- test/js/bun/sqlite/sqlite.test.js | 7 +- .../terminal/terminal-platform-gaps.test.ts | 71 ++- test/js/bun/terminal/terminal-spawn.test.ts | 8 +- test/js/bun/terminal/terminal.test.ts | 104 ++-- test/js/bun/test/jest-each-gc-root.test.ts | 4 +- test/js/bun/test/mock/mock-module.test.ts | 4 +- .../snapshot-tests/snapshots/snapshot.test.ts | 4 +- test/js/bun/test/test-test.test.ts | 5 +- .../transpiler-error-gc-uaf.test.ts | 4 +- test/js/bun/udp/dgram.test.ts | 109 ++-- test/js/bun/udp/udp_socket.test.ts | 2 +- test/js/bun/util/bun-file-fd-read.test.ts | 4 +- test/js/bun/util/bun-stdin-slice.test.ts | 53 +- test/js/bun/util/filesink.test.ts | 193 +++---- test/js/bun/util/filesystem_router.test.ts | 41 +- test/js/bun/util/fuzzy-wuzzy.test.ts | 7 +- test/js/bun/util/mmap.test.js | 4 +- test/js/bun/util/which.test.ts | 14 +- test/js/bun/wasm/wasi.test.js | 24 +- ...bsocket-server-backpressure-buffer.test.ts | 2 +- .../websocket-server-forced-close.test.ts | 4 +- test/js/bun/webview/webview-chrome.test.ts | 2 +- .../ws/ws-client-compat-events.test.ts | 4 +- .../ws/ws-client-compat-handshake.test.ts | 4 +- .../ws/ws-client-compat-request.test.ts | 4 +- .../ws/ws-client-compat-response.test.ts | 4 +- .../ws/ws-client-compat-send.test.ts | 8 +- .../ws/ws-server-compat-close.test.ts | 4 +- .../ws/ws-server-compat-send.test.ts | 4 +- .../ws/ws-server-compat-transports.test.ts | 4 +- .../first_party/ws/ws-syscall-fault.test.ts | 4 +- .../child-process-rlimit-nofile.test.ts | 32 +- .../child_process/child-process-stdio.test.js | 4 +- .../node/child_process/child_process.test.ts | 130 ++--- .../child_process_ipc_handle.test.ts | 4 +- test/js/node/cluster.test.ts | 116 ++-- test/js/node/crypto/crypto.test.ts | 4 +- .../dns/dns-tcp-bidirectional-poll.test.ts | 4 +- test/js/node/dns/node-dns.test.js | 4 +- test/js/node/fs/cp.test.ts | 63 +- test/js/node/fs/dir.test.ts | 2 +- test/js/node/fs/fs-mkdir.test.ts | 2 +- test/js/node/fs/fs-oom.test.ts | 27 +- test/js/node/fs/fs-path-length.test.ts | 4 +- test/js/node/fs/fs.test.ts | 267 ++++----- .../readdirSync-recursive-error-leak.test.ts | 54 +- test/js/node/fs/rm-windows-ntstatus.test.ts | 2 +- .../fs/translate-uv-error-windows.test.ts | 2 +- .../node/http/node-http-backpressure.test.ts | 41 +- test/js/node/http/node-http-connect.test.ts | 37 +- .../node/http/node-http-pinned-write.test.ts | 189 +++--- .../http/node-http-server-close-drain.test.ts | 4 +- .../node/http/node-http-syscall-fault.test.ts | 4 +- test/js/node/http/node-http.test.ts | 6 +- .../http2/node-http2-syscall-fault.test.ts | 4 +- .../module/esm-registry-concurrent-gc.test.ts | 56 +- .../module-children-concurrent-gc.test.ts | 58 +- .../js/node/module/node-module-module.test.js | 105 ++-- test/js/node/net/net-syscall-fault.test.ts | 3 +- .../node/net/node-net-allowHalfOpen.test.js | 4 +- test/js/node/net/node-net.test.ts | 76 ++- test/js/node/path/browserify.test.js | 5 +- test/js/node/path/resolve-long-cwd.test.ts | 4 +- test/js/node/path/resolve.test.js | 31 +- .../process/dlopen-duplicate-load.test.ts | 4 +- .../process-array-accessor-crash.test.ts | 3 +- test/js/node/process/process-execve.test.ts | 18 +- .../process-signal-listener-count.test.ts | 12 +- test/js/node/process/process-stdin.test.ts | 4 +- test/js/node/process/process-stdio.test.ts | 359 ++++++------ test/js/node/process/process.test.js | 29 +- .../stdin/process-stdin-stale-hup.test.ts | 124 ++-- test/js/node/readline/stdin-pause-pty.test.ts | 4 +- ...ent-abort-keep-alive-queued-unix-socket.js | 2 - .../test-http-client-abort-unix-socket.js | 2 - .../parallel/test-http-client-pipe-end.js | 2 - .../test-http-client-response-domain.js | 2 - ...test-http-client-with-create-connection.js | 2 - .../test-http-unix-socket-keep-alive.js | 2 - .../test/parallel/test-http-unix-socket.js | 2 - .../test-https-unix-socket-self-signed.js | 2 - .../test/parallel/test-process-exit-code.js | 7 - .../sequential/test-cluster-inspect-brk.js | 2 - test/js/node/timers/node-timers.test.ts | 32 +- test/js/node/tls/node-tls-connect.test.ts | 60 +- test/js/node/tls/node-tls-server.test.ts | 6 +- test/js/node/tls/test-use-system-ca.test.ts | 4 +- test/js/node/tls/tls-syscall-fault.test.ts | 2 +- test/js/node/tty.test.ts | 8 +- test/js/node/url/url-pathtofileurl.test.js | 2 +- .../node/vm/sourcetextmodule-link-gc.test.ts | 122 ++-- .../js/node/vm/vm-script-fetcher-leak.test.ts | 18 +- test/js/node/vm/vm.test.ts | 22 +- test/js/node/watch/fs.watch.rewrite.test.ts | 4 +- test/js/node/watch/fs.watch.test.ts | 4 +- .../worker-shutdown-post-leak.test.ts | 4 +- .../worker_heap_snapshot_gc.test.ts | 4 +- .../js/sql/adapter-env-var-precedence.test.ts | 23 +- test/js/sql/postgres-auth.test.ts | 4 +- test/js/sql/postgres-connection.test.ts | 5 +- .../sql/sql-close-pending-connection.test.ts | 4 +- test/js/sql/sql-mysql-connection.test.ts | 4 +- test/js/sql/sql-mysql.auth.test.ts | 4 +- test/js/sql/sql-mysql.test.ts | 4 +- test/js/sql/sql-options.test.ts | 2 +- .../third_party/next-auth/next-auth.test.ts | 64 +-- .../reliability/connection-failures.test.ts | 438 +++++++------- test/js/valkey/valkey-gc.test.ts | 8 +- test/js/valkey/valkey-tls-verify.test.ts | 6 +- .../abort/abort-controller-gc-reason.test.ts | 110 ++-- .../message-event-init-gc.test.ts | 4 +- test/js/web/fetch/blob-oom.test.ts | 4 +- test/js/web/fetch/body-clone.test.ts | 10 +- test/js/web/fetch/fetch-backpressure.test.ts | 4 +- test/js/web/fetch/fetch-compress.test.ts | 4 +- .../fetch/fetch-http3-syscall-fault.test.ts | 55 +- test/js/web/fetch/fetch-keepalive.test.ts | 4 +- test/js/web/fetch/fetch-preconnect.test.ts | 4 +- .../fetch-response-finalizer-sweep.test.ts | 4 +- test/js/web/fetch/fetch-session.test.ts | 8 +- test/js/web/fetch/fetch-syscall-fault.test.ts | 4 +- test/js/web/fetch/fetch.stream.test.ts | 7 +- test/js/web/fetch/fetch.test.ts | 6 +- test/js/web/fetch/fetch.tls.test.ts | 20 +- test/js/web/fetch/fetch.unix.test.ts | 14 +- test/js/web/intl/intl.test.ts | 2 +- test/js/web/streams/streams-leak.test.ts | 101 ++-- test/js/web/streams/streams.test.js | 4 +- .../js/web/structured-clone-blob-file.test.ts | 4 +- test/js/web/websocket/error-event.test.ts | 4 +- .../websocket-proxy-close-reentrancy.test.ts | 4 +- .../websocket/websocket-syscall-fault.test.ts | 4 +- test/js/web/websocket/websocket-unix.test.ts | 4 +- .../message-port-context-destroy-leak.test.ts | 36 +- .../workers/worker-late-completion.test.ts | 6 +- .../workers/worker-terminate-lifetime.test.ts | 2 +- test/js/workerd/html-rewriter.test.js | 4 +- test/napi/napi-value-ffi.test.ts | 10 +- test/napi/napi.test.ts | 154 +++-- .../node-api/test_callback_scope/do.test.ts | 3 +- test/napi/uv.test.ts | 4 +- test/napi/uv_stub.test.ts | 4 +- test/regression/issue/02977.test.ts | 4 +- test/regression/issue/05828.test.ts | 4 +- test/regression/issue/14799.test.ts | 4 +- test/regression/issue/18239/18239.test.ts | 4 +- test/regression/issue/20144/20144.test.ts | 2 +- test/regression/issue/20753.test.js | 3 +- test/regression/issue/22003.test.ts | 4 +- test/regression/issue/24329.test.ts | 8 +- test/regression/issue/25903.test.ts | 6 +- test/regression/issue/26207.test.ts | 4 +- test/regression/issue/26249.test.ts | 6 +- test/regression/issue/26286.test.ts | 6 +- test/regression/issue/29181.test.ts | 4 +- test/regression/issue/29519.test.ts | 4 +- test/regression/issue/29925.test.ts | 4 +- test/regression/issue/30205.test.ts | 62 +- test/regression/issue/30429.test.ts | 2 +- test/regression/issue/30717.test.ts | 2 +- test/regression/issue/31503.test.ts | 4 +- test/regression/issue/39846.test.ts | 3 +- test/regression/issue/40111.test.ts | 4 +- .../atomics-waitasync-wtftimer-uaf.test.ts | 5 +- test/regression/issue/ctrl-c.test.ts | 4 +- .../issue/isArray-proxy-crash.test.ts | 2 +- .../issue/tty-readstream-ref-unref.test.ts | 3 - .../issue/tty-reopen-after-stdin-eof.test.ts | 6 +- .../issue/tui-app-tty-pattern.test.ts | 3 - test/v8/v8.test.ts | 7 +- 350 files changed, 4971 insertions(+), 5589 deletions(-) diff --git a/test/bake/deinitialization.test.ts b/test/bake/deinitialization.test.ts index 6ad8c05ed4f6..83f7abee43bb 100644 --- a/test/bake/deinitialization.test.ts +++ b/test/bake/deinitialization.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, isASAN, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, isASAN, tempDir } from "harness"; import path from "node:path"; test("dev server deinitializes itself", () => { @@ -141,13 +141,11 @@ test.skipIf(!isASAN)( 60_000, ); -test.skipIf(isWindows)( - "dev server closes the descriptor it opens to bundle an edited file again", - async () => { - using dir = tempDir("bake-reload-descriptors", { - "index.html": ``, - "entry.ts": `console.log("v0!");`, - "fixture.ts": ` +test("dev server closes the descriptor it opens to bundle an edited file again", async () => { + using dir = tempDir("bake-reload-descriptors", { + "index.html": ``, + "entry.ts": `console.log("v0!");`, + "fixture.ts": ` import { readdirSync, writeFileSync } from "node:fs"; import html from "./index.html"; @@ -170,22 +168,20 @@ test.skipIf(isWindows)( console.log(openDescriptors() - before < 10 ? "closed" : "left open"); await server.stop(true); `, - }); + }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "fixture.ts"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "inherit", - }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + await using proc = Bun.spawn({ + cmd: [bunExe(), "fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout).toEndWith("closed\n"); - expect(exitCode).toBe(0); - }, - 60_000, -); + expect(stdout).toEndWith("closed\n"); + expect(exitCode).toBe(0); +}, 60_000); test("a request that crosses maxRequestBodySize while it waits for a bundle that fails", async () => { using dir = tempDir("dev-server-deferred-limit", { diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 6771c0f96261..45e98f6fb97c 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync } from "fs"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import path from "path"; import { minimalFramework, tempDirWithBakeDeps } from "../bake-harness"; @@ -832,7 +832,7 @@ export default function IndexPage() { // A debug build with BUN_JSC_validateExceptionChecks=1 aborts on the first JSC call with an unchecked exception. // Windows resolves the import()'s absolute path as a relative path. - test.skipIf(isWindows)("the module loader hooks check for exceptions", async () => { + test("the module loader hooks check for exceptions", async () => { using dir = tempDir("bake-production-exception-checks", { "server.ts": ` export async function prerender(meta) { diff --git a/test/bake/serve-plugins-dev-server.test.ts b/test/bake/serve-plugins-dev-server.test.ts index 0a73aae7138c..6d52c1b223f5 100644 --- a/test/bake/serve-plugins-dev-server.test.ts +++ b/test/bake/serve-plugins-dev-server.test.ts @@ -5,7 +5,7 @@ // null (or garbage, depending on build mode) and the DevServer is never told that // plugin loading finished, so the request it deferred to `next_bundle` waits forever. import { expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; const indexHtml = /* html */ ` @@ -444,7 +444,7 @@ test.concurrent("releases a framework request that the client aborts while it wa }); // Windows refuses to rename over a file that the watcher has open. -test.skipIf(isWindows)("responds to a framework request queued behind the reload that bundles its page", async () => { +test("responds to a framework request queued behind the reload that bundles its page", async () => { using dir = tempDir("serve-plugins-devserver-queued-framework", { "bunfig.toml": `[serve.static]\nplugins = ["./plugin.ts"]\n`, "plugin.ts": ` diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index c7ca18450820..ebdea40f7035 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -517,7 +517,7 @@ describe("Bun.build", () => { }); }); - test.skipIf(isWindows)("fails the build when the bundler thread can't start", async () => { + test("fails the build when the bundler thread can't start", async () => { const script = ` const fs = require("fs"); const fds = []; @@ -1061,14 +1061,12 @@ describe("Bun.build", () => { // descriptor in the resolver. A second in-process Bun.build() used to reuse a // descriptor the first build had already closed, failing with EBADF. The test // host must also import the package so its fd is cached before the builds run. - test.concurrent.skipIf(isWindows)( - "repeated in-process builds of a symlinked package do not reuse a closed fd", - async () => { - using dir = tempDir("build-symlink-fd-cache", { - "vendor/pkg/package.json": `{"name":"pkg","version":"1.0.0","type":"module","exports":"./index.js"}`, - "vendor/pkg/index.js": `export const value = 1;\n`, - "entry.ts": `import { value } from "pkg";\nconsole.log(value);\n`, - "repro.test.ts": ` + test.concurrent("repeated in-process builds of a symlinked package do not reuse a closed fd", async () => { + using dir = tempDir("build-symlink-fd-cache", { + "vendor/pkg/package.json": `{"name":"pkg","version":"1.0.0","type":"module","exports":"./index.js"}`, + "vendor/pkg/index.js": `export const value = 1;\n`, + "entry.ts": `import { value } from "pkg";\nconsole.log(value);\n`, + "repro.test.ts": ` import { it } from "bun:test"; import { value } from "pkg"; void value; @@ -1082,22 +1080,21 @@ describe("Bun.build", () => { console.log("ALL_BUILDS_OK"); }); `, - }); - mkdirSync(join(String(dir), "node_modules"), { recursive: true }); - symlinkSync("../vendor/pkg", join(String(dir), "node_modules", "pkg")); + }); + mkdirSync(join(String(dir), "node_modules"), { recursive: true }); + symlinkSync("../vendor/pkg", join(String(dir), "node_modules", "pkg")); - await using proc = Bun.spawn({ - cmd: [bunExe(), "test", "repro.test.ts"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout + stderr).toContain("ALL_BUILDS_OK"); - expect(exitCode).toBe(0); - }, - ); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "repro.test.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout + stderr).toContain("ALL_BUILDS_OK"); + expect(exitCode).toBe(0); + }); test.concurrent("errors are returned as an array", async () => { const x = await buildNoThrow({ @@ -2541,14 +2538,12 @@ test("sourcemap sourcesContent is valid JSON when source contains C0 control cha // Bun.build's link step waited for the shared thread pool to go *idle* rather than for its // own tasks, so any unrelated pool work extended the build by its full duration — a // node:fs read parked on a FIFO nobody writes made every later build hang forever. -test.skipIf(isWindows)( - "Bun.build does not wait for unrelated thread-pool work", - async () => { - using dir = tempDir("build-pool-wait", { - "a.ts": `import { b } from "./b"; import "./s.css"; console.log(b);`, - "b.ts": `export const b = 1;`, - "s.css": `body { color: red }`, - "run.js": ` +test("Bun.build does not wait for unrelated thread-pool work", async () => { + using dir = tempDir("build-pool-wait", { + "a.ts": `import { b } from "./b"; import "./s.css"; console.log(b);`, + "b.ts": `export const b = 1;`, + "s.css": `body { color: red }`, + "run.js": ` const { join } = require("path"); const dir = process.argv[2]; const fs = require("fs"); @@ -2585,21 +2580,19 @@ test.skipIf(isWindows)( console.log(readDone, readErr ? readErr.code : "ok"); process.exit(0); `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "run.js", String(dir)], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toStartWith("second build took "); - expect(stdout).toBe("true true false\ntrue ok\n"); - expect(exitCode).toBe(0); - }, - 30_000, -); + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "run.js", String(dir)], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toStartWith("second build took "); + expect(stdout).toBe("true true false\ntrue ok\n"); + expect(exitCode).toBe(0); +}, 30_000); describe.concurrent("Bun.build with a path longer than the path buffer", () => { const components = Math.ceil((MAX_PATH_BYTES * 1.25) / 201); diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 7fad99c9d459..c65327d5e22d 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -1,18 +1,6 @@ import { bytecodeOrderNames } from "bun:internal-for-testing"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { - bunEnv, - bunExe, - isArm64, - isASAN, - isDebug, - isLinux, - isMacOS, - isMusl, - isPosix, - isWindows, - tempDir, -} from "harness"; +import { bunEnv, bunExe, isArm64, isASAN, isDebug, isLinux, isMacOS, isMusl, isWindows, tempDir } from "harness"; import { chmodSync, closeSync, @@ -757,49 +745,45 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, // An executable for Windows has its chunks at B:/~BUN/root/. This bun stands in for the Windows one (its builtins // section is what the internal modules are named from), so the program cannot be put in it: the build fails // there, after the link wrote what it calls the code. (On Windows that build is the round trips above.) - test.skipIf(isWindows)( - "a build for Windows calls the code what a build for this platform calls it", - async () => { - using dir = tempDir("build-compile-bytecode-order-windows", twoEntryPoints); - await record(String(dir), twoEntryPointArgs, "5\n"); - const namesOfBuild = async (target: string[], outfile: string, root: string) => { - const build = await buildIn( - String(dir), - [...twoEntryPointArgs, ...target, "--bytecode-order=app.order"], - outfile, - { - BUN_BYTECODE_ORDER_NAMES_OUT: join(String(dir), outfile + ".names"), - }, - ); - // Without where the chunks are, the executable's name, and the hash of a chunk's contents (the paths it - // imports chunks by are part of those). - const names = Object.entries(namesIn(String(dir), outfile + ".names")) - .map(([path, names]) => [ - path - .replace(root, "") - .replace(outfile, "") - .replace(/-[a-z0-9]{8}\.js$/, ".js"), - names, - ]) - .sort(); - expect(new Set(names.map(([path]) => path)).size).toBe(names.length); - return { build, names }; - }; - const here = await namesOfBuild([], "thisplatform", "/$bunfs/root/"); - expect(here.build).toEqual({ stderr: "", exitCode: 0 }); - const windows = await namesOfBuild( - ["--target=bun-windows-x64", `--compile-executable-path=${bunExe()}`], - "windows.exe", - "B:/~BUN/root/", + test("a build for Windows calls the code what a build for this platform calls it", async () => { + using dir = tempDir("build-compile-bytecode-order-windows", twoEntryPoints); + await record(String(dir), twoEntryPointArgs, "5\n"); + const namesOfBuild = async (target: string[], outfile: string, root: string) => { + const build = await buildIn( + String(dir), + [...twoEntryPointArgs, ...target, "--bytecode-order=app.order"], + outfile, + { + BUN_BYTECODE_ORDER_NAMES_OUT: join(String(dir), outfile + ".names"), + }, ); - expect(windows.build.stderr).toContain("failed to write compiled executable"); - expect(windows.build.stderr).not.toContain("names"); - // The three chunks, and node:fs with the internal modules it needs. - expect(here.names.length).toBeGreaterThan(3); - expect(windows.names).toEqual(here.names); - }, - 60_000, - ); + // Without where the chunks are, the executable's name, and the hash of a chunk's contents (the paths it + // imports chunks by are part of those). + const names = Object.entries(namesIn(String(dir), outfile + ".names")) + .map(([path, names]) => [ + path + .replace(root, "") + .replace(outfile, "") + .replace(/-[a-z0-9]{8}\.js$/, ".js"), + names, + ]) + .sort(); + expect(new Set(names.map(([path]) => path)).size).toBe(names.length); + return { build, names }; + }; + const here = await namesOfBuild([], "thisplatform", "/$bunfs/root/"); + expect(here.build).toEqual({ stderr: "", exitCode: 0 }); + const windows = await namesOfBuild( + ["--target=bun-windows-x64", `--compile-executable-path=${bunExe()}`], + "windows.exe", + "B:/~BUN/root/", + ); + expect(windows.build.stderr).toContain("failed to write compiled executable"); + expect(windows.build.stderr).not.toContain("names"); + // The three chunks, and node:fs with the internal modules it needs. + expect(here.names.length).toBeGreaterThan(3); + expect(windows.names).toEqual(here.names); + }, 60_000); // The runtime runs a module from its source when the bytecode it has is not for that source. Such a run did not // see what the module's code does: the recording says nothing about it, not that it did not run. @@ -1561,28 +1545,24 @@ console.log(JSON.stringify({ n, anonKB: anon }));`, }, 60_000); // The file is read once, front to back. - test.skipIf(!isPosix)( - "an order file that is a pipe", - async () => { - const { plain, order } = shared; - const piped = await compile(exe("piped"), ["--bytecode-order=/dev/stdin"], {}, order); - expect(piped.stderr).not.toContain("bytecode order file"); - expect(piped.exitCode).toBe(0); - expect(hasLinkedPayload(exe("piped"))).toBe(true); - expect(await run(exe("piped"), recordedArgv)).toEqual(plain); - expect(await stats(exe("piped"), recordedArgv)).toMatchObject({ - hot: expect.any(Number), - unknown: 0, - cold: 0, - }); - }, - 60_000, - ); + test("an order file that is a pipe", async () => { + const { plain, order } = shared; + const piped = await compile(exe("piped"), ["--bytecode-order=/dev/stdin"], {}, order); + expect(piped.stderr).not.toContain("bytecode order file"); + expect(piped.exitCode).toBe(0); + expect(hasLinkedPayload(exe("piped"))).toBe(true); + expect(await run(exe("piped"), recordedArgv)).toEqual(plain); + expect(await stats(exe("piped"), recordedArgv)).toMatchObject({ + hot: expect.any(Number), + unknown: 0, + cold: 0, + }); + }, 60_000); }); // process.kill(process.pid, signal) with no handler ends the process without the usual exit: the recording is written // before the signal is sent, like the profiles and the compile cache. - describe.skipIf(isWindows)("a process that sends itself a fatal signal still writes the order file", () => { + describe("a process that sends itself a fatal signal still writes the order file", () => { let selfKillDir: ReturnType | undefined; let built: string; afterAll(() => selfKillDir?.[Symbol.dispose]()); @@ -2769,44 +2749,40 @@ if (process.platform === "android") { // removed AFTER the process starts, which `Bun.spawn`'s `cwd` can't do, so a // shell wrapper `cd`s in, `rmdir`s, then execs the binary (how a user hits it). describe("compiled binary in a deleted cwd", () => { - test.if(isPosix)( - "exits cleanly instead of crashing", - async () => { - using dir = tempDir("build-compile-deleted-cwd", { - "app.js": `console.log("should-not-run");`, - }); - const outfile = join(String(dir), "app"); + test("exits cleanly instead of crashing", async () => { + using dir = tempDir("build-compile-deleted-cwd", { + "app.js": `console.log("should-not-run");`, + }); + const outfile = join(String(dir), "app"); - await using build = Bun.spawn({ - cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [, buildStderr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); - expect(buildStderr).not.toContain("error:"); - expect(buildExit).toBe(0); + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, buildStderr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect(buildStderr).not.toContain("error:"); + expect(buildExit).toBe(0); - // A fresh directory to stand in and delete — NOT `dir`, which holds the - // compiled binary we still need to exec. - using cwdDir = tempDir("build-compile-gone-cwd", {}); - const gone = String(cwdDir); + // A fresh directory to stand in and delete — NOT `dir`, which holds the + // compiled binary we still need to exec. + using cwdDir = tempDir("build-compile-gone-cwd", {}); + const gone = String(cwdDir); - await using proc = Bun.spawn({ - cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${outfile}"`], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + await using proc = Bun.spawn({ + cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${outfile}"`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe(""); - expect(stderr).toContain("The current working directory was deleted"); - expect(exitCode).toBe(1); - }, - 60_000, - ); + expect(stdout).toBe(""); + expect(stderr).toContain("The current working directory was deleted"); + expect(exitCode).toBe(1); + }, 60_000); }); // Every region of the embedded module graph (file contents, names, bytecode, the diff --git a/test/bundler/compile-corrupted-embed.test.ts b/test/bundler/compile-corrupted-embed.test.ts index 3919c6621144..0f01ea029d93 100644 --- a/test/bundler/compile-corrupted-embed.test.ts +++ b/test/bundler/compile-corrupted-embed.test.ts @@ -168,9 +168,8 @@ test.skipIf(isMacOS)( expect(await run(exe)).toEqual({ stdout: "hello from standalone\n", stderr: "", exitCode: 0 }); await expectGracefulFallback(corruptedCopy(exe, "length", isWindows ? peStompLength : elfStompLength)); - if (!isWindows) { - await expectGracefulFallback(corruptedCopy(exe, "vaddr", path => elfStompVaddr(path, 0x4141414141414141n))); - } + + await expectGracefulFallback(corruptedCopy(exe, "vaddr", path => elfStompVaddr(path, 0x4141414141414141n))); }, TIMEOUT, ); diff --git a/test/bundler/metafile.test.ts b/test/bundler/metafile.test.ts index 8953f2ac6bb9..b2fd542768b7 100644 --- a/test/bundler/metafile.test.ts +++ b/test/bundler/metafile.test.ts @@ -862,7 +862,7 @@ describe("Bun.build metafile option variants", () => { }); // CLI tests for --metafile-md -import { bunEnv, bunExe, isWindows } from "harness"; +import { bunEnv, bunExe } from "harness"; describe("bun build --metafile-md", () => { test("generates markdown metafile with default name", async () => { @@ -1418,7 +1418,7 @@ describe("bun build --metafile-md", () => { describe("bun build --metafile", () => { // Bun.spawn replaces a lone surrogate in a JS string with U+FFFD before it reaches argv, // so the raw bytes have to come from the shell. Windows argv is UTF-16 and cannot carry them. - test.skipIf(isWindows)("escapes a lone surrogate and an invalid byte in an output path", async () => { + test("escapes a lone surrogate and an invalid byte in an output path", async () => { using dir = tempDir("metafile-wtf8", { "index.js": `console.log(1);`, }); diff --git a/test/bundler/native-plugin.test.ts b/test/bundler/native-plugin.test.ts index 92e8242cceee..d885174541ec 100644 --- a/test/bundler/native-plugin.test.ts +++ b/test/bundler/native-plugin.test.ts @@ -411,7 +411,7 @@ const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] // and aborts before the crash handler can print the name. musl: the crash // handler re-raises and the agent writes a core, which the runner counts as a // failed job even though every test passed. - it.skipIf(process.platform === "win32" || isASAN || isMusl)("prints name when plugin crashes", async () => { + it.skipIf(isASAN || isMusl)("prints name when plugin crashes", async () => { const prelude = /* ts */ `import values from "./stuff.ts" const many_foo = ["foo","foo","foo","foo","foo","foo","foo"] `; diff --git a/test/cli/bake/bake.test.ts b/test/cli/bake/bake.test.ts index 734d25ce2cc3..01c3f5759f3c 100644 --- a/test/cli/bake/bake.test.ts +++ b/test/cli/bake/bake.test.ts @@ -451,10 +451,10 @@ test.concurrent.each([ const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); // One save can start the process twice on Windows, and each start prints its own port. - if (!isWindows) { - expect(await get(url, "/missing")).toBe("200 1"); - return; - } + + expect(await get(url, "/missing")).toBe("200 1"); + return; + const deadline = Date.now() + 10_000; let response = ""; while (response !== "200 1" && Date.now() < deadline) { @@ -465,7 +465,7 @@ test.concurrent.each([ }); // Creating a symlink on Windows needs a privilege that CI does not have. -test.concurrent.skipIf(isWindows)("bun bake serves pages that are symlinks or in a symlinked folder", async () => { +test.concurrent("bun bake serves pages that are symlinks or in a symlinked folder", async () => { using dir = tempDir("bake-command-symlink", { "shared/page.html": `

Shared

`, "shared/docs/intro.html": `

Intro

`, @@ -617,7 +617,7 @@ test.concurrent("bun bake accepts a frozen export of app/server.ts", async () => }); // Bun.serve's `unix` option takes a socket path, which this test only makes on POSIX. -test.concurrent.skipIf(isWindows)("bun bake keeps a unix socket from the export of app/server.ts", async () => { +test.concurrent("bun bake keeps a unix socket from the export of app/server.ts", async () => { using dir = tempDir("bake-command-unix", {}); const socket = join(String(dir), "s.sock"); await Bun.write( @@ -794,7 +794,7 @@ test.concurrent("bun bake runs the SIGTERM handlers before it starts again for b // On a terminal, `bun bake` adds its own SIGTERM listener, which exits. Bun.spawn's `terminal` is // POSIX only. -test.concurrent.skipIf(isWindows)( +test.concurrent( "on a terminal, bun bake runs every SIGTERM listener before it starts again for bake.config.ts", async () => { const server = (version: string) => ` @@ -864,35 +864,32 @@ test.concurrent("bun bake exits when a start for a bake.config.ts change finds a }); // Windows has no SIGUSR2. -test.concurrent.skipIf(isWindows)( - "bun --watch-kill-signal ... bake --check sends that signal before a restart", - async () => { - using dir = tempDir("bake-command-kill-signal", { - "tsconfig.json": checkedTsconfig, - "app/server.ts": ` +test.concurrent("bun --watch-kill-signal ... bake --check sends that signal before a restart", async () => { + using dir = tempDir("bake-command-kill-signal", { + "tsconfig.json": checkedTsconfig, + "app/server.ts": ` declare const Response: any; declare const process: any; declare const console: any; process.on("SIGUSR2", () => console.log("SIGUSR2 before restart")); export default { fetch: () => new Response("v1") }; `, - }); + }); - // `--check` starts the process again after a change, which sends the signal: SIGTERM without the flag. - await using proc = spawnBake(String(dir), ["--watch-kill-signal", "SIGUSR2", "bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/url: /); + // `--check` starts the process again after a change, which sends the signal: SIGTERM without the flag. + await using proc = spawnBake(String(dir), ["--watch-kill-signal", "SIGUSR2", "bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/url: /); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/SIGUSR2 before restart/); - }, -); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/SIGUSR2 before restart/); +}); // Windows has no SIGUSR2. -test.concurrent.skipIf(isWindows)( +test.concurrent( "bun --watch-kill-signal ... bake sends that signal before it starts again for bake.config.ts", async () => { using dir = tempDir("bake-command-kill-signal-config", { @@ -978,38 +975,35 @@ test.concurrent("bun bake reads a bake.config.ts whose default export is a promi // On Windows, a manager process starts each restart with its own environment, so a restart cannot // tell it is one, and exits on this error as at the first start. -test.concurrent.skipIf(isWindows)( - "bun bake --check waits for a fix when a restart finds a page it cannot serve", - async () => { - using dir = tempDir("bake-command-check-restart-error", { - "tsconfig.json": checkedTsconfig, - "app/pages/index.html": page, - "app/server.ts": `declare const Response: any;\nexport default { fetch: () => new Response("v1") };\n`, - }); +test.concurrent("bun bake --check waits for a fix when a restart finds a page it cannot serve", async () => { + using dir = tempDir("bake-command-check-restart-error", { + "tsconfig.json": checkedTsconfig, + "app/pages/index.html": page, + "app/server.ts": `declare const Response: any;\nexport default { fetch: () => new Response("v1") };\n`, + }); - await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/url: /); + await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/url: /); - // `--check` starts the process again on a change: the error must not end the session. - const slug = join(String(dir), "app/pages/[slug].html"); - await Bun.write(slug, page); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/a page cannot have/); + // `--check` starts the process again on a change: the error must not end the session. + const slug = join(String(dir), "app/pages/[slug].html"); + await Bun.write(slug, page); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/a page cannot have/); - rmSync(slug); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, - ); - const [, url] = await output.waitFor(/a page cannot have[\s\S]*url: (http:\/\/\S+)/); + rmSync(slug); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, + ); + const [, url] = await output.waitFor(/a page cannot have[\s\S]*url: (http:\/\/\S+)/); - expect(await get(url, "/missing")).toBe("200 v3"); - }, -); + expect(await get(url, "/missing")).toBe("200 v3"); +}); test.concurrent.each([ { name: "nothing to serve", files: { "README.md": "" }, error: "Nothing to serve in" }, @@ -1031,36 +1025,33 @@ test.concurrent.each([ }); // On Windows, a restart cannot tell it is one, as above. -test.concurrent.skipIf(isWindows)( - "bun bake --check waits on a restart error also when the first start had a type error", - async () => { - using dir = tempDir("bake-command-check-typeerror-restart", { - "tsconfig.json": checkedTsconfig, - "app/pages/[slug].html": page, - "app/server.ts": `declare const Response: any;\nconst n: number = "x";\nexport default { fetch: () => new Response("v1") };\n`, - }); +test.concurrent("bun bake --check waits on a restart error also when the first start had a type error", async () => { + using dir = tempDir("bake-command-check-typeerror-restart", { + "tsconfig.json": checkedTsconfig, + "app/pages/[slug].html": page, + "app/server.ts": `declare const Response: any;\nconst n: number = "x";\nexport default { fetch: () => new Response("v1") };\n`, + }); - await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/TS2322/); + await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/TS2322/); - // The fix restarts the process, which then finds the page it cannot serve: it must wait. - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/a page cannot have/); + // The fix restarts the process, which then finds the page it cannot serve: it must wait. + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/a page cannot have/); - rmSync(join(String(dir), "app/pages/[slug].html")); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, - ); - const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); + rmSync(join(String(dir), "app/pages/[slug].html")); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, + ); + const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); - expect(await get(url, "/missing")).toBe("200 v3"); - }, -); + expect(await get(url, "/missing")).toBe("200 v3"); +}); /** Links the React of the test suite into a project in `dir`. A junction needs no privilege on Windows. */ function linkReact(dir: string) { diff --git a/test/cli/bun.test.ts b/test/cli/bun.test.ts index 34d5af32f1bb..edd45b06bed2 100644 --- a/test/cli/bun.test.ts +++ b/test/cli/bun.test.ts @@ -44,7 +44,7 @@ describe("bun", () => { // openpty via bun:ffi so one stdio fd can be a real TTY while the other is // a pipe. glibc keeps openpty in libutil; musl and macOS keep everything in // libc. Same pattern as test/js/bun/terminal/terminal-spawn.test.ts. - describe.skipIf(isWindows)("per-stream color detection", () => { + describe("per-stream color detection", () => { const colorEnv = { ...bunEnv, NO_COLOR: undefined, @@ -243,7 +243,7 @@ describe("bun", () => { }); }); // On Windows `bun completions` installs bunx as a hardlink (or a .cmd shim) instead of a symlink. - describe.skipIf(isWindows)("completions", () => { + describe("completions", () => { const bunxName = isDebug ? "bunx-debug" : "bunx"; test("installs a bunx symlink to the executable, falling back through the install directories", async () => { diff --git a/test/cli/check/check.test.ts b/test/cli/check/check.test.ts index ca76a4b6fb6c..df6393fe9a3e 100644 --- a/test/cli/check/check.test.ts +++ b/test/cli/check/check.test.ts @@ -486,7 +486,7 @@ describe.concurrent("bun check", () => { }); // A dedicated thread draws the progress line, and only when a person is at a terminal. - test.skipIf(isWindows || !hasTerminal)("shows progress in a terminal", async () => { + test.skipIf(!hasTerminal)("shows progress in a terminal", async () => { using dir = project({ "index.ts": `const wrong: string = 1;\n` }); let output = ""; await using proc = spawn({ @@ -2911,7 +2911,7 @@ export const a: number[] = [file, one, two]; }); // Opening such a pipe blocks until a writer opens it. package.json is opened to look for a "check" script. - test.skipIf(isWindows)("doesn't hang when package.json is a symlink to a named pipe with no writer", async () => { + test("doesn't hang when package.json is a symlink to a named pipe with no writer", async () => { using dir = project({ "a.ts": `export {};\n` }); expect(Bun.spawnSync(["mkfifo", join(String(dir), "pipe")]).exitCode).toBe(0); symlinkSync("pipe", join(String(dir), "package.json")); @@ -3035,7 +3035,7 @@ export const a: number[] = [file, one, two]; expect(exitCode).toBe(1); }); - test.skipIf(process.platform === "win32")("ignores a named pipe in the project directory", async () => { + test("ignores a named pipe in the project directory", async () => { using dir = project({ "a.ts": `export const a: string = 1;\n` }); // Reading a pipe without a writer blocks forever. expect(Bun.spawnSync(["mkfifo", join(String(dir), "pipe.ts")]).exitCode).toBe(0); @@ -22829,7 +22829,7 @@ user${i}.ts(10,22): error TS2322: Type 'A' is not assignable to type 'never'.`, }); // Root can read every file. - const canReadEverything = isWindows || isRoot; + const canReadEverything = isRoot; test.skipIf(canReadEverything)("reports TS5083 for a source file that can't be read", async () => { using dir = project({ "a.ts": `import "./secret";\n`, "secret.ts": `export {};\n` }); chmodSync(join(String(dir), "secret.ts"), 0o000); @@ -24228,28 +24228,25 @@ describe.concurrent("--check", () => { }); // The path is compared as `bun run` spells it. - test.skipIf(isWindows)( - "`bun check` type checks in a `check` script started by a Bun that sets only BUN_INTERNAL_CHECK_SCRIPTS", - async () => { - using dir = project({ - "package.json": JSON.stringify({ scripts: { check: "echo the script ran" } }), - "tsconfig.json": tsconfig, - "a.ts": `export const a: number = "1";\n`, - }); - const cwd = String(dir); - const results = await Promise.all([ - run(cwd, ["check", "--pretty", "false"], { BUN_INTERNAL_CHECK_SCRIPTS: `${Buffer.byteLength(cwd)}:${cwd}` }), - // Another package's path. - run(cwd, ["check", "--pretty", "false"], { BUN_INTERNAL_CHECK_SCRIPTS: `2:/x` }), - ]); - expect( - results.map(it => [it.stdout.includes("TS2322"), it.stdout.includes("the script ran"), it.exitCode]), - ).toEqual([ - [true, false, 1], - [false, true, 0], - ]); - }, - ); + test("`bun check` type checks in a `check` script started by a Bun that sets only BUN_INTERNAL_CHECK_SCRIPTS", async () => { + using dir = project({ + "package.json": JSON.stringify({ scripts: { check: "echo the script ran" } }), + "tsconfig.json": tsconfig, + "a.ts": `export const a: number = "1";\n`, + }); + const cwd = String(dir); + const results = await Promise.all([ + run(cwd, ["check", "--pretty", "false"], { BUN_INTERNAL_CHECK_SCRIPTS: `${Buffer.byteLength(cwd)}:${cwd}` }), + // Another package's path. + run(cwd, ["check", "--pretty", "false"], { BUN_INTERNAL_CHECK_SCRIPTS: `2:/x` }), + ]); + expect( + results.map(it => [it.stdout.includes("TS2322"), it.stdout.includes("the script ran"), it.exitCode]), + ).toEqual([ + [true, false, 1], + [false, true, 0], + ]); + }); test("--check with --filter, --parallel or --sequential checks the project before the scripts", async () => { using dir = project({ @@ -24315,7 +24312,7 @@ describe.concurrent("--check", () => { }); // These draw the same progress line as `bun check`, on a dedicated thread, and the process keeps running afterwards. - describe.skipIf(isWindows || !hasTerminal)("in a terminal", () => { + describe.skipIf(!hasTerminal)("in a terminal", () => { const files = (n: string) => ({ "package.json": JSON.stringify({ scripts: { hello: "echo ran 1" } }), "bun-test.d.ts": `declare module "bun:test" {\n export function test(name: string, fn: () => void): void;\n}\n`, @@ -24406,7 +24403,7 @@ describe.concurrent("--check", () => { ["a shell script", ["--check", "./tool.sh"]], ])("bun --check with %s", (_, cmd) => { // On Windows it would have to be a real executable, because `bun` doesn't start batch files. - test.skipIf(isWindows && cmd.includes("tool"))("type checks the project first", async () => { + test("type checks the project first", async () => { const files = { "tool.sh": `echo ran\n`, "node_modules/.bin/tool": `#!/bin/sh\necho ran\n` }; using good = project({ ...files, "a.ts": `export const a: number = 1;\n` }); using bad = project({ ...files, "a.ts": `export const a: number = "1";\n` }); diff --git a/test/cli/create/create-jsx.test.ts b/test/cli/create/create-jsx.test.ts index b9883c9ac35a..add03952174d 100644 --- a/test/cli/create/create-jsx.test.ts +++ b/test/cli/create/create-jsx.test.ts @@ -2,7 +2,7 @@ import type { Subprocess } from "bun"; import { beforeEach, describe, expect, test } from "bun:test"; import { readFileSync, statSync } from "fs"; import { cp, readdir } from "fs/promises"; -import { bunEnv, bunExe, isCI, isWindows, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isCI, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness"; import path from "path"; async function getServerUrl(process: Subprocess, all = { text: "" }) { @@ -126,7 +126,7 @@ for (const development of [true, false]) { }); }); - test.todoIf(isCI || isWindows)("dev server", async () => { + test.todoIf(isCI)("dev server", async () => { console.log({ dir }); await using process = Bun.spawn([bunExe(), "create", "./index.jsx"], { cwd: dir, @@ -161,7 +161,7 @@ for (const development of [true, false]) { } }); - test.todoIf(isWindows)("build", async () => { + test("build", async () => { { const process = Bun.spawn([bunExe(), "create", "./index.jsx"], { cwd: dir, @@ -193,7 +193,7 @@ for (const development of [true, false]) { }); }); - test.todoIf(isCI || isWindows)("dev server", async () => { + test.todoIf(isCI)("dev server", async () => { const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { cwd: dir, env: env, @@ -227,7 +227,7 @@ for (const development of [true, false]) { } }); - test.todoIf(isWindows)("build", async () => { + test("build", async () => { { const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { cwd: dir, @@ -259,7 +259,7 @@ for (const development of [true, false]) { }); }); - test.todoIf(isCI || isWindows)("dev server", async () => { + test.todoIf(isCI)("dev server", async () => { const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { cwd: dir, env: env, @@ -298,7 +298,7 @@ for (const development of [true, false]) { } }); - test.todoIf(isCI || isWindows)("build", async () => { + test.todoIf(isCI)("build", async () => { { const process = Bun.spawn([bunExe(), "create", "./index.tsx"], { cwd: dir, @@ -326,7 +326,7 @@ for (const development of [true, false]) { // Windows: `bun create` never prints the "--only-missing install" line this // asserts on, so the dependency detection cannot be observed there. -test.todoIf(isWindows)("auto-install passes detected dependencies as positionals", async () => { +test("auto-install passes detected dependencies as positionals", async () => { using dir = tempDir("create-arg-separator", { "Component.tsx": `import "--trust"; @@ -469,7 +469,7 @@ function identity(file: string) { } // Windows: `bun create ` does not reach its install step there (see create-jsx.test.ts). -describe.concurrent.todoIf(isWindows)("bun create and files that are already there", () => { +describe.concurrent("bun create and files that are already there", () => { const userCss = "/* my own stylesheet */\n.card { color: red; }\n"; const packageJson = JSON.stringify({ name: "my-app", private: true }); diff --git a/test/cli/format/format.test.ts b/test/cli/format/format.test.ts index bf602778695e..b589ed5e014b 100644 --- a/test/cli/format/format.test.ts +++ b/test/cli/format/format.test.ts @@ -467,7 +467,7 @@ describe.concurrent("bun fmt", () => { }); // With `ulimit -f 16` a write fails with EFBIG once it's 8 KB into a file, much like on a full disk. - test.skipIf(isWindows)("keeps its old text when writing it in place fails partway", async () => { + test("keeps its old text when writing it in place fails partway", async () => { const text = `{${Array.from({ length: 3000 }, (_, i) => `"k${i}": [1,2,3]`).join(",")}}\n`; using dir = tempDir("bun-format", { "a.json": text }); linkSync(join(String(dir), "a.json"), join(String(dir), "other-name.txt")); @@ -479,7 +479,7 @@ describe.concurrent("bun fmt", () => { }); // A new file would get fewer permission bits, unless the process's umask is 0. - test.skipIf(isWindows)("keeps its mode regardless of the umask", async () => { + test("keeps its mode regardless of the umask", async () => { using dir = tempDir("bun-format", { "a.js": ugly, "b.js": ugly }); chmodSync(join(String(dir), "a.js"), 0o664); chmodSync(join(String(dir), "b.js"), 0o777); @@ -522,19 +522,16 @@ describe.concurrent("bun fmt", () => { }); // A file name can have at most 255 bytes. On Windows the path would also be too long. - test.skipIf(isWindows)( - "can have the longest possible name, and is still replaced rather than written in place", - async () => { - const name = `${Buffer.alloc(252, "a")}.ts`; - using dir = tempDir("bun-format", { [name]: ugly }); - const before = statSync(join(String(dir), name)).ino; - const { exitCode } = await write(String(dir), []); - expect(read(String(dir), name)).toBe(formatted); - expect(statSync(join(String(dir), name)).ino).not.toBe(before); - expect(readdirSync(String(dir))).toEqual([name]); - expect(exitCode).toBe(0); - }, - ); + test("can have the longest possible name, and is still replaced rather than written in place", async () => { + const name = `${Buffer.alloc(252, "a")}.ts`; + using dir = tempDir("bun-format", { [name]: ugly }); + const before = statSync(join(String(dir), name)).ino; + const { exitCode } = await write(String(dir), []); + expect(read(String(dir), name)).toBe(formatted); + expect(statSync(join(String(dir), name)).ino).not.toBe(before); + expect(readdirSync(String(dir))).toEqual([name]); + expect(exitCode).toBe(0); + }); // For example in a container with a user's project mounted into it. test.skipIf(!isRoot)("keeps its owner if root formats it", async () => { @@ -552,7 +549,7 @@ describe.concurrent("bun fmt", () => { }); // Prettier doesn't need write access to the directory. - test.skipIf(isWindows || isRoot)("can be in a read-only directory", async () => { + test.skipIf(isRoot)("can be in a read-only directory", async () => { using dir = tempDir("bun-format", { "src/a.js": ugly }); chmodSync(join(String(dir), "src"), 0o555); try { @@ -5731,15 +5728,12 @@ exports.format = async text => "by Prettier"; expect(result.exitCode).toBe(2); }); - test.skipIf(isWindows)( - "/dev/null works as an ignore file without patterns and as a configuration without options", - async () => { - const files = { ".prettierignore": "a.js\n", ".prettierrc": `{ "semi": false }\n`, "a.js": formatted }; - expect(await different(files, ["."])).toEqual([]); - expect(await different(files, ["--ignore-path", "/dev/null", "."])).toEqual(["a.js"]); - expect(await different(files, ["--ignore-path", "/dev/null", "--config", "/dev/null", "."])).toEqual([]); - }, - ); + test("/dev/null works as an ignore file without patterns and as a configuration without options", async () => { + const files = { ".prettierignore": "a.js\n", ".prettierrc": `{ "semi": false }\n`, "a.js": formatted }; + expect(await different(files, ["."])).toEqual([]); + expect(await different(files, ["--ignore-path", "/dev/null", "."])).toEqual(["a.js"]); + expect(await different(files, ["--ignore-path", "/dev/null", "--config", "/dev/null", "."])).toEqual([]); + }); // This means "never break a line". Prettier accepts every integer from 0 up, and Infinity. test.each([ @@ -6122,7 +6116,7 @@ exports.format = async text => "by Prettier"; ); // Same as Prettier 3.9.9 and oxfmt 0.72, which load a configuration in Node. On Windows it still runs. - test.skipIf(isWindows).each(["prettier.config.mjs", "oxfmt.config.ts"])( + test.each(["prettier.config.mjs", "oxfmt.config.ts"])( "a `preload` in bunfig.toml doesn't run before %s", async name => { const files = { @@ -6162,15 +6156,13 @@ exports.format = async text => "by Prettier"; expect(result.exitCode).toBe(0); }); - test.skipIf(isWindows)("a named pipe whose name ends in .js is skipped, like in Prettier", async () => { + test("a named pipe whose name ends in .js is skipped, like in Prettier", async () => { const before = (dir: string) => expect(Bun.spawnSync(["mkfifo", join(dir, "pipe.js")]).exitCode).toBe(0); expect(await different({ "a.js": ugly }, [], { before })).toEqual(["a.js"]); }); // Opening a pipe like this blocks until a writer opens it. - test - .skipIf(isWindows) - .each([".gitignore", ".git/info/exclude", ".prettierignore", ".editorconfig", ".prettierrc", "package.json"])( + test.each([".gitignore", ".git/info/exclude", ".prettierignore", ".editorconfig", ".prettierrc", "package.json"])( "doesn't hang when %s is a link to a named pipe without a writer", async name => { const before = (dir: string) => { @@ -6182,7 +6174,7 @@ exports.format = async text => "by Prettier"; }, ); - test.skipIf(isWindows).each([ + test.each([ ["writes to it is read", "echo a.js", "b.js\n"], ["closes it without writing is read as an empty file", ":", "a.js\nb.js\n"], ])("a pipe whose writer %s", async (_, writer, expected) => { @@ -6200,7 +6192,7 @@ exports.format = async text => "by Prettier"; expect(exitCode).toBe(1); }); - describe.skipIf(isWindows)("a named pipe passed to a flag", () => { + describe("a named pipe passed to a flag", () => { const makePipes = (dir: string) => expect(Bun.spawnSync(["mkfifo", join(dir, "pipe"), join(dir, "pipe.json")]).exitCode).toBe(0); @@ -6634,7 +6626,7 @@ exports.format = async text => "by Prettier"; // A case's guard used to be printed without a stack check, so the stack overflowed at depths between the printer's // limit and the parser's. The threads that format files have stack to spare, so this uses the main thread with half // of its usual stack. - test.skipIf(isWindows || isDebug || isASAN).concurrent.each([4_500, 4_612, 4_728, 4_846, 4_967, 5_091])( + test.skipIf(isDebug || isASAN).concurrent.each([4_500, 4_612, 4_728, 4_846, 4_967, 5_091])( "Flow's match with %d nested guards, on a 4 MB stack", async depth => { using dir = tempDir("bun-format-small-stack", {}); @@ -8278,7 +8270,7 @@ try { expect(inNested.files).toEqual(after(null, 0, 4)); }); - test.skipIf(isWindows)("/dev/null", async () => { + test("/dev/null", async () => { const files = { ".prettierrc.mjs": program(8), "a.js": code(0) }; const result = await format(files, ["--config=/dev/null", "."], { reads: ["evaluated", "a.js"] }); expect(result.files).toEqual({ "evaluated": null, "a.js": code(2) }); @@ -9080,7 +9072,6 @@ try { // Some sandboxes have no pseudo-terminals. const hasTerminal = (() => { - if (isWindows) return false; try { new Bun.Terminal({}).close(); return true; @@ -10136,7 +10127,7 @@ describe.concurrent("other things named fmt in the project", () => { expect(result.exitCode).toBe(0); }); - test.skipIf(isWindows)("a package's executable wins over the formatter, and nothing is rewritten", async () => { + test("a package's executable wins over the formatter, and nothing is rewritten", async () => { const files = { "node_modules/.bin/fmt": "#!/bin/sh\necho the executable\n", "a.js": ugly }; const result = await format(files, [], { reads: ["a.js"], diff --git a/test/cli/format/oxfmt-cli/oxfmt-cli.test.ts b/test/cli/format/oxfmt-cli/oxfmt-cli.test.ts index e4e58c346444..eab664bb18f1 100644 --- a/test/cli/format/oxfmt-cli/oxfmt-cli.test.ts +++ b/test/cli/format/oxfmt-cli/oxfmt-cli.test.ts @@ -3,7 +3,7 @@ // `--list-different` list. // `cases.json` is written by `test/cli/lint/oracle/driver/oxfmt-fixtures.mjs --record`. import { afterAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { readFileSync, symlinkSync } from "node:fs"; import { join } from "node:path"; import { endChildren, spawn } from "../../children"; @@ -21,7 +21,7 @@ describe.concurrent("bun fmt behaves like oxfmt", () => { for (const it of cases) { const files = { ...(fixtures as Record)[it.name], ...it.gitignore } as Files; const links = Object.entries(files).filter(([, file]) => typeof file !== "string") as [string, { link: string }][]; - test.skipIf(isWindows && links.length > 0)(`${it.name}/${it.index}: ${it.args.join(" ")}`, async () => { + test(`${it.name}/${it.index}: ${it.args.join(" ")}`, async () => { const texts = Object.entries(files).filter(([, file]) => typeof file === "string") as [string, string][]; // Without a configuration file, `bun fmt` behaves like Prettier. An empty one above the fixtures makes it behave // like oxfmt. diff --git a/test/cli/format/prettier-cli/prettier-cli.test.ts b/test/cli/format/prettier-cli/prettier-cli.test.ts index 68df7a389d7e..84b1c5495e21 100644 --- a/test/cli/format/prettier-cli/prettier-cli.test.ts +++ b/test/cli/format/prettier-cli/prettier-cli.test.ts @@ -4,7 +4,7 @@ // input. `bun fmt` writes unless told otherwise, so Prettier was given `--write` where the command line only prints. // `cases.json` is written by `test/cli/lint/oracle/driver/prettier-fixtures.mjs --record`. import { afterAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { readFileSync, symlinkSync } from "node:fs"; import { join } from "node:path"; import { endChildren, spawn } from "../../children"; @@ -25,7 +25,7 @@ describe.concurrent("bun fmt behaves like Prettier", () => { const links = Object.entries(files).filter(([, file]) => typeof file !== "string") as [string, { link: string }][]; // Links, or a name that Windows doesn't allow. const needsPosix = links.length > 0 || Object.keys(files).some(name => /[<>:"|?*\\]/.test(name)); - test.skipIf(isWindows && needsPosix)(`${it.test}: ${it.directory} $ ${it.args.join(" ")}`, async () => { + test(`${it.test}: ${it.directory} $ ${it.args.join(" ")}`, async () => { const texts = Object.entries(files).filter(([, file]) => typeof file === "string") as [string, string][]; using dir = tempDir("prettier-cli", Object.fromEntries(texts.map(([name, text]) => [`${top}/${name}`, text]))); for (const [name, { link }] of links) symlinkSync(link, join(String(dir), top, name)); diff --git a/test/cli/format/tailwind/tailwind.test.ts b/test/cli/format/tailwind/tailwind.test.ts index 7c167152d3c3..126d0815f026 100644 --- a/test/cli/format/tailwind/tailwind.test.ts +++ b/test/cli/format/tailwind/tailwind.test.ts @@ -5,7 +5,7 @@ // // The order comes from the project's `tailwindcss` package. Here that's a stub that knows the classes in order.json. import { afterAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isLinux, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, isLinux, tempDir } from "harness"; import { chmodSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { endChildren, spawn } from "../../children"; @@ -163,7 +163,7 @@ if (now.join() !== before.join()) throw new Error("There is " + now.filter(it => }); // As with Nix, an image layer owned by root, or a sandbox. Skipped as root, which can write anywhere. - test.skipIf(isWindows || process.getuid?.() === 0)("works with read-only packages", async () => { + test.skipIf(process.getuid?.() === 0)("works with read-only packages", async () => { using dir = tempDir("bun-format-tailwind", { ...version4, ".oxfmtrc.json": '{ "sortTailwindcss": {} }\n', diff --git a/test/cli/heap-prof.test.ts b/test/cli/heap-prof.test.ts index f94ce71e788e..5df60bb15338 100644 --- a/test/cli/heap-prof.test.ts +++ b/test/cli/heap-prof.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "path"; const testScript = `const arr = []; for (let i = 0; i < 100; i++) arr.push({ x: i, y: "hello" + i }); console.log("done");`; @@ -71,30 +71,27 @@ test("--heap-prof writes the profile when the script calls process.exit", async expectV8HeapSnapshotShape(profile); }); -test.skipIf(process.platform === "win32")( - "--heap-prof writes the profile on a self-directed SIGINT with no JS handler", - async () => { - using dir = tempDir("heap-prof-sigint-test", {}); +test("--heap-prof writes the profile on a self-directed SIGINT with no JS handler", async () => { + using dir = tempDir("heap-prof-sigint-test", {}); - await using proc = Bun.spawn({ - cmd: [bunExe(), "--heap-prof", "-e", `process.kill(process.pid, "SIGINT");`], - cwd: String(dir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "--heap-prof", "-e", `process.kill(process.pid, "SIGINT");`], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); - const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(proc.signalCode).toBe("SIGINT"); - expect(exitCode).not.toBe(0); + const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(proc.signalCode).toBe("SIGINT"); + expect(exitCode).not.toBe(0); - const glob = new Bun.Glob("*.heapprofile"); - const files = Array.from(glob.scanSync({ cwd: String(dir) })); - expect(files.length).toBe(1); - const profile = await readProfile(String(dir), files[0]); - expectV8HeapSnapshotShape(profile); - }, -); + const glob = new Bun.Glob("*.heapprofile"); + const files = Array.from(glob.scanSync({ cwd: String(dir) })); + expect(files.length).toBe(1); + const profile = await readProfile(String(dir), files[0]); + expectV8HeapSnapshotShape(profile); +}); test("--heap-prof-md generates markdown heap profile on exit", async () => { using dir = tempDir("heap-prof-md-test", {}); @@ -295,7 +292,7 @@ for (const [flag, value] of [ // POSIX 5000 bytes exceeds the fixed path buffer (Linux 4096, macOS 1024); // 2500 + 2500 exercises the combined bound with components that fit // individually on Linux. -test.skipIf(isWindows).each([ +test.each([ ["--heap-prof-dir", ["--heap-prof-dir", Buffer.alloc(5000, "d").toString()]], ["--heap-prof-name", ["--heap-prof-name", Buffer.alloc(5000, "n").toString()]], [ diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index f8c6887f5d9c..1099fb8fa366 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1086,7 +1086,7 @@ it.todoIf(!isWindows)("reloads when a deleted import is recreated", async () => }); // The Windows watcher never reads the cached listing. -it.skipIf(isWindows)("survives a directory event for a folder whose cached listing failed to read", async () => { +it("survives a directory event for a folder whose cached listing failed to read", async () => { using dir = tempDir("hot-dir-read-error", { "trace.log": "", "src/lib/a.ts": `export {};`, diff --git a/test/cli/hot/watch.test.ts b/test/cli/hot/watch.test.ts index 4a6a5c2908b7..aeb08fd0a7ca 100644 --- a/test/cli/hot/watch.test.ts +++ b/test/cli/hot/watch.test.ts @@ -1,10 +1,10 @@ import { spawn } from "bun"; import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, forEachLine, isBroken, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, forEachLine, tempDir } from "harness"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; -describe.todoIf(isBroken && isWindows)("--watch works", async () => { +describe("--watch works", async () => { for (const watchedFile of ["entry.js", "tmp.js"]) { test(`with ${watchedFile}`, async () => { await using tmpdir_ = tempDir("watch-fixture", { diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 62c8471b61fd..73c5fd1cd781 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -100,7 +100,7 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; // Ctrl-D is EOF only on a POSIX tty; the Windows console has no equivalent // key that ends a cooked-mode read. - test.skipIf(isWindows)("bun init exits quietly on Ctrl-D at a text prompt", async () => { + test("bun init exits quietly on Ctrl-D at a text prompt", async () => { await using temp = tempDir("bun-init-ctrl-d", {}); const decoder = new TextDecoder(); @@ -550,31 +550,27 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; 180_000, ); - test.skipIf(isWindows)( - "`bun fmt --check` passes on the generated CLAUDE.md", - async () => { - await using temp = tempDir("bun-init-claude-md", { "bin/claude": "#!/bin/sh\n" }); - fs.chmodSync(path.join(temp, "bin/claude"), 0o755); - await using init = Bun.spawn({ - cmd: [bunExe(), "init", "-y", "app"], - cwd: temp, - stdio: ["ignore", "ignore", "inherit"], - env: { ...bunEnv, PATH: `${path.join(temp, "bin")}:${bunEnv.PATH}`, CURSOR_AGENT_RULE_DISABLED: "1" }, - }); - expect(await init.exited).toBe(0); - expect(fs.readFileSync(path.join(temp, "app/CLAUDE.md"), "utf8")).toStartWith("Default to using Bun"); + test("`bun fmt --check` passes on the generated CLAUDE.md", async () => { + await using temp = tempDir("bun-init-claude-md", { "bin/claude": "#!/bin/sh\n" }); + fs.chmodSync(path.join(temp, "bin/claude"), 0o755); + await using init = Bun.spawn({ + cmd: [bunExe(), "init", "-y", "app"], + cwd: temp, + stdio: ["ignore", "ignore", "inherit"], + env: { ...bunEnv, PATH: `${path.join(temp, "bin")}:${bunEnv.PATH}`, CURSOR_AGENT_RULE_DISABLED: "1" }, + }); + expect(await init.exited).toBe(0); + expect(fs.readFileSync(path.join(temp, "app/CLAUDE.md"), "utf8")).toStartWith("Default to using Bun"); - await using format = Bun.spawn({ - cmd: [bunExe(), "fmt", "--check", "CLAUDE.md"], - cwd: path.join(temp, "app"), - stdio: ["ignore", "pipe", "pipe"], - env: bunEnv, - }); - const [stdout, stderr, exitCode] = await Promise.all([format.stdout.text(), format.stderr.text(), format.exited]); - expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); - }, - 30_000, - ); + await using format = Bun.spawn({ + cmd: [bunExe(), "fmt", "--check", "CLAUDE.md"], + cwd: path.join(temp, "app"), + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + const [stdout, stderr, exitCode] = await Promise.all([format.stdout.text(), format.stderr.text(), format.exited]); + expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); + }, 30_000); test("nested `bun install` output is inherited", async () => { // `bun init` spawns `bun install` via spawn_sync_inherit. The child must diff --git a/test/cli/inspect/BunFrontendDevServer.test.ts b/test/cli/inspect/BunFrontendDevServer.test.ts index a5301cac32e9..f72b0ab3f142 100644 --- a/test/cli/inspect/BunFrontendDevServer.test.ts +++ b/test/cli/inspect/BunFrontendDevServer.test.ts @@ -3,7 +3,7 @@ import { decodeSerializedError } from "bake/error-serialization.ts"; import { Subprocess, spawn } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunExe, bunEnv as env, isPosix, tempDir, tmpdirSync } from "harness"; +import { bunExe, bunEnv as env, tempDir, tmpdirSync } from "harness"; import path, { join } from "node:path"; import { InspectorSession, connect } from "./junit-reporter"; import { SocketFramer } from "./socket-framer"; @@ -72,7 +72,7 @@ class BunFrontendDevServerSession extends InspectorSession { } } -describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => { +describe("BunFrontendDevServer inspector protocol", () => { let devServerProcess: Subprocess<"ignore", "pipe", "pipe">; let serverUrl: URL; let session: BunFrontendDevServerSession; diff --git a/test/cli/inspect/HTTPServerAgent.test.ts b/test/cli/inspect/HTTPServerAgent.test.ts index 30a09bb64d1d..f90ef342eafb 100644 --- a/test/cli/inspect/HTTPServerAgent.test.ts +++ b/test/cli/inspect/HTTPServerAgent.test.ts @@ -1,7 +1,7 @@ import { Subprocess, spawn } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunExe, bunEnv as env, isPosix, tmpdirSync } from "harness"; +import { bunExe, bunEnv as env, tmpdirSync } from "harness"; import { join } from "node:path"; import { InspectorSession, connect } from "./junit-reporter"; import { SocketFramer } from "./socket-framer"; @@ -78,7 +78,7 @@ class HTTPServerInspectorSession extends InspectorSession { } } -describe.if(isPosix)("HTTPServer inspector protocol", () => { +describe("HTTPServer inspector protocol", () => { let serverProcess: Subprocess; let serverUrl: URL; let session: HTTPServerInspectorSession; diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts index 0e3e7eb58fb1..7947ded67216 100644 --- a/test/cli/inspect/inspect.test.ts +++ b/test/cli/inspect/inspect.test.ts @@ -1,7 +1,7 @@ import { Subprocess, spawn } from "bun"; import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunEnv, bunExe, isPosix, randomPort, tempDir } from "harness"; +import { bunEnv, bunExe, randomPort, tempDir } from "harness"; import { join } from "node:path"; import stripAnsi from "strip-ansi"; import { WebSocket } from "ws"; @@ -392,7 +392,7 @@ describe("unix domain socket without websocket", () => { fs.rmSync(tempdir, { recursive: true, force: true }); }); - if (isPosix) { + { async function runTest(path: string, args: string[], env = bunEnv) { let { promise, resolve, reject } = Promise.withResolvers(); diff --git a/test/cli/inspect/test-reporter.test.ts b/test/cli/inspect/test-reporter.test.ts index 607d04a5ea37..5b898a46739f 100644 --- a/test/cli/inspect/test-reporter.test.ts +++ b/test/cli/inspect/test-reporter.test.ts @@ -1,6 +1,6 @@ import { Subprocess, spawn, write } from "bun"; import { afterEach, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isPosix, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "node:path"; import { InspectorSession, connect } from "./junit-reporter"; import { SocketFramer } from "./socket-framer"; @@ -165,7 +165,7 @@ class TestReporterSession extends InspectorSession { } } -describe.if(isPosix)("TestReporter inspector protocol", () => { +describe("TestReporter inspector protocol", () => { let proc: Subprocess<"ignore", "pipe", "pipe"> | undefined; let socket: ReturnType extends Promise ? T : never; diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts index 3fcf4715832d..edcf27d48405 100644 --- a/test/cli/install/bad-workspace.test.ts +++ b/test/cli/install/bad-workspace.test.ts @@ -2,7 +2,7 @@ import { spawn, spawnSync } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isLinux, isRoot, isWindows, tempDir, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isLinux, isRoot, tempDir, tmpdirSync } from "harness"; import { dirname, join } from "path"; let cwd: string; @@ -58,7 +58,7 @@ test.concurrent("glob entry under a missing directory reports the errno name and // npm, pnpm and yarn 1 fail here too. Root ignores the mode bits, and chmod on Windows // cannot make a directory unreadable or unsearchable. -test.concurrent.skipIf(isWindows || isRoot).each([ +test.concurrent.skipIf(isRoot).each([ ["unreadable", 0o000, "open", "secret"], ["unsearchable", 0o444, "fstatat", join("secret", "package.json")], ])("glob entry that reaches an %s directory names what it failed on", async (_, mode, syscall, failedOn) => { @@ -329,7 +329,7 @@ describe.concurrent("workspaces entries longer than the path buffer", () => { // The entry is read from `${dir}/${entry}/package.json`. One byte below the buffer size // that path still reaches the OS; from the buffer size on it is rejected before that. - test.skipIf(isWindows).each([ + test.each([ ["one byte below", -1, "Workspace not found"], ["exactly", 0, "ENAMETOOLONG reading package.json for workspace package"], ["one byte above", 1, "ENAMETOOLONG reading package.json for workspace package"], @@ -392,39 +392,36 @@ describe.concurrent("workspaces entries longer than the path buffer", () => { // Globs are walked relative to the project directory, so a match can lie deeper than the // buffer holds once the project directory is put in front of it. Windows has no such // depth: the buffer there holds more than the longest path the OS accepts. - test.skipIf(isWindows)( - "glob match whose absolute package.json path does not fit fails with ENAMETOOLONG", - async () => { - using dir = tempDir("bad-workspace-deep-glob-match", { "package.json": rootPackageJson(["deep/**"]) }); - // The directory itself fits PATH_MAX (so it can be created), `/package.json` - // does not fit the buffer. - const absoluteDirBytes = POSIX_PATH_BUFFER_BYTES - 6; - const names = deepDirectoryNames(absoluteDirBytes - Buffer.byteLength(String(dir)) - "/".length); - const workspaceDir = join(String(dir), ...names); - expect(Buffer.byteLength(workspaceDir)).toBe(absoluteDirBytes); - mkdirSync(workspaceDir, { recursive: true }); - // Too long to be written by its absolute path: write it relative to the parent directory. - await using writer = spawn({ - cmd: [ - bunExe(), - "-e", - `require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`, - names.at(-1)!, - ], - cwd: dirname(workspaceDir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - expect(await writer.stderr.text()).toBe(""); - expect(await writer.exited).toBe(0); + test("glob match whose absolute package.json path does not fit fails with ENAMETOOLONG", async () => { + using dir = tempDir("bad-workspace-deep-glob-match", { "package.json": rootPackageJson(["deep/**"]) }); + // The directory itself fits PATH_MAX (so it can be created), `/package.json` + // does not fit the buffer. + const absoluteDirBytes = POSIX_PATH_BUFFER_BYTES - 6; + const names = deepDirectoryNames(absoluteDirBytes - Buffer.byteLength(String(dir)) - "/".length); + const workspaceDir = join(String(dir), ...names); + expect(Buffer.byteLength(workspaceDir)).toBe(absoluteDirBytes); + mkdirSync(workspaceDir, { recursive: true }); + // Too long to be written by its absolute path: write it relative to the parent directory. + await using writer = spawn({ + cmd: [ + bunExe(), + "-e", + `require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`, + names.at(-1)!, + ], + cwd: dirname(workspaceDir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + expect(await writer.stderr.text()).toBe(""); + expect(await writer.exited).toBe(0); - const { stderr, exitCode } = await runInstall(String(dir)); + const { stderr, exitCode } = await runInstall(String(dir)); - expect(stderr).toContain(`error: ENAMETOOLONG reading package.json for workspace package "${names.join("/")}"`); - expect(exitCode).toBe(1); - }, - ); + expect(stderr).toContain(`error: ENAMETOOLONG reading package.json for workspace package "${names.join("/")}"`); + expect(exitCode).toBe(1); + }); }); async function runBun(args: string[], cwd: string, env: Record = bunEnv) { @@ -571,7 +568,7 @@ describe.concurrent.each([ // The package.json that `bun install` finds above the directory it runs in becomes the root of // the install: it says which lifecycle scripts are trusted, and its .npmrc and bunfig.toml apply. // In a directory with the mode of /tmp any local user can put one above somebody else's project. -describe.concurrent.skipIf(isWindows)("package.json in a sticky, shared directory above the cwd", () => { +describe.concurrent("package.json in a sticky, shared directory above the cwd", () => { // Its postinstall runs only if the root of the install trusts it. const dep = JSON.stringify({ name: "dep", @@ -710,7 +707,7 @@ describe.concurrent.skipIf(isWindows)("package.json in a sticky, shared director // A working directory can be PATH_MAX - 1 bytes long, and `/package.json` is 13 bytes longer. // On Windows the path buffer is longer than any path the OS accepts. -describe.concurrent.skipIf(isWindows)("working directory close to PATH_MAX", () => { +describe.concurrent("working directory close to PATH_MAX", () => { /** A project in a directory whose absolute path is exactly `cwdBytes` bytes long. */ function deepProject(cwdBytes: number) { const dir = tempDir("root-discovery-deep-cwd", { diff --git a/test/cli/install/bun-add-filter.test.ts b/test/cli/install/bun-add-filter.test.ts index e3b8a3a9289a..1ea7250b01ae 100644 --- a/test/cli/install/bun-add-filter.test.ts +++ b/test/cli/install/bun-add-filter.test.ts @@ -1,7 +1,7 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { chmod, exists, mkdir, readdir, rm } from "fs/promises"; -import { bunEnv, bunExe, isRoot, isWindows, normalizeBunSnapshot, tempDir } from "harness"; +import { bunEnv, bunExe, isRoot, normalizeBunSnapshot, tempDir } from "harness"; import { join } from "path"; import { TestRegistry } from "registry"; @@ -2322,22 +2322,19 @@ test.concurrent( ); // Windows is left out: its path buffer holds more than any path the OS accepts. -test.concurrent.skipIf(isWindows)( - "a local path longer than the path buffer is rejected before anything is written", - async () => { - const dir = await makeMonorepo(); - const before = await allPackageJsonTexts(dir); +test.concurrent("a local path longer than the path buffer is rejected before anything is written", async () => { + const dir = await makeMonorepo(); + const before = await allPackageJsonTexts(dir); - const path = "./" + Buffer.alloc(4200, "a").toString(); - const { stderr, exitCode } = await run(["add", path, "--filter", "api"], dir); - expect(stderr).toBe(`error: Dependency "${path}" has an unsafe folder path\n`); - expect(exitCode).toBe(1); + const path = "./" + Buffer.alloc(4200, "a").toString(); + const { stderr, exitCode } = await run(["add", path, "--filter", "api"], dir); + expect(stderr).toBe(`error: Dependency "${path}" has an unsafe folder path\n`); + expect(exitCode).toBe(1); - expect(await allPackageJsonTexts(dir)).toStrictEqual(before); - expect(await exists(join(dir, "bun.lock"))).toBeFalse(); - expect(await exists(join(dir, "node_modules"))).toBeFalse(); - }, -); + expect(await allPackageJsonTexts(dir)).toStrictEqual(before); + expect(await exists(join(dir, "bun.lock"))).toBeFalse(); + expect(await exists(join(dir, "node_modules"))).toBeFalse(); +}); test.concurrent("an absolute local path is written verbatim into every target", async () => { const dir = await makeMonorepo(); @@ -2408,7 +2405,7 @@ test.concurrent("remove --filter --dry-run touches nothing", async () => { expect(await exists(join(dir, "node_modules", "no-deps", "package.json"))).toBeTrue(); }); -test.concurrent.skipIf(isWindows || isRoot)( +test.concurrent.skipIf(isRoot)( "an unwritable target is reported by name, the rest is still written, exit code 1", async () => { const dir = await makeMonorepo(); diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 0168a56d61f2..929fbd1b4127 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -7,7 +7,6 @@ import { bunExe, bunEnv as env, hostedGitRepository, - isWindows, readdirSorted, tempDir, tmpdirSync, @@ -617,7 +616,7 @@ it("should reject positionals longer than 2048 bytes without stack overflow", as }); // PATH_MAX is 4096 on Linux and 1024 on macOS. Windows' path buffer is larger than any command line. -it.skipIf(isWindows)("should fail with ENAMETOOLONG for a local tarball path longer than PATH_MAX", async () => { +it("should fail with ENAMETOOLONG for a local tarball path longer than PATH_MAX", async () => { const package_json = JSON.stringify({ name: "foo", version: "0.0.1" }); using dir = tempDir("bun-add-long-tarball-path", { "package.json": package_json }); const tarball = `./${Buffer.alloc(5000, "a").toString()}.tgz`; @@ -3042,7 +3041,7 @@ describe("a relative local path typed from a directory below package.json", () = }); // A Windows command line cannot carry a path longer than the path buffer. - it.skipIf(isWindows)("a path that does not fit a path buffer once joined with the cwd is an error", async () => { + it("a path that does not fit a path buffer once joined with the cwd is an error", async () => { const cwd = await makeNestedCwd(); const typed = "./" + Buffer.alloc(6000, "a").toString(); @@ -3052,7 +3051,7 @@ describe("a relative local path typed from a directory below package.json", () = expect(exitCode).toBe(1); }); - it.skipIf(isWindows)("a path whose re-spelled form does not fit a path buffer is an error", async () => { + it("a path whose re-spelled form does not fit a path buffer is an error", async () => { const app = join(package_dir, "a", "b", "c", "d", "e", "f"); const cwd = join(app, "src"); await mkdir(cwd, { recursive: true }); diff --git a/test/cli/install/bun-create.test.ts b/test/cli/install/bun-create.test.ts index 4a1c095847b4..662735d0ba87 100644 --- a/test/cli/install/bun-create.test.ts +++ b/test/cli/install/bun-create.test.ts @@ -396,7 +396,7 @@ describe.concurrent("git", () => { }); // The stub `git` logs the files each call sees. POSIX-only: it is a shell script. - it.skipIf(!isPosix)("runs every git command before the first task", async () => { + it("runs every git command before the first task", async () => { using dir = tempDir("create-git-order", { ...localdep, "bin/git": `#!/bin/sh\necho "$1" $(LC_ALL=C ls -A) >> "$GIT_LOG"\n`, @@ -463,7 +463,7 @@ describe.concurrent("git", () => { }); // With nothing to install, git's outcome was stored without the `!`. - it.skipIf(!isPosix)("reports the git repository when there is nothing to install", async () => { + it("reports the git repository when there is nothing to install", async () => { using dir = tempDir("create-git-no-deps", { "bin/git": "#!/bin/sh\nexit 0\n", "empty/.keep": "", @@ -846,7 +846,7 @@ describe.concurrent("template given as a path", () => { }); // A path on Windows holds more than a command line. - it.skipIf(!isPosix).each(["", "a/", "a@", "@a/", "./"])( + it.each(["", "a/", "a@", "@a/", "./"])( "refuses a template name longer than a path: %p and 5000 bytes", async prefix => { using dir = tempDir("create-long-template-name", { "home/.keep": "", "proj/keep.js": "mine" }); @@ -1416,7 +1416,7 @@ describe.concurrent("bun-create tasks", () => { expect(exitCode).toBe(0); }); - it.skipIf(!isPosix)("do not look for a bare command in the destination", async () => { + it("do not look for a bare command in the destination", async () => { const { exitCode, written } = await createFrom( { "bun-create": { postinstall: "local-task first" } }, { "local-task": stubs["bin/create-task"] }, @@ -1427,7 +1427,7 @@ describe.concurrent("bun-create tasks", () => { }); // The script needs its shebang to be started, so POSIX only. - it.skipIf(!isPosix)("run a path relative to the destination", async () => { + it("run a path relative to the destination", async () => { const { err, exitCode, written } = await createFrom( { "bun-create": { postinstall: "./setup.sh first second" } }, { "setup.sh": stubs["bin/create-task"] }, @@ -1486,7 +1486,7 @@ describe.each([ expect(exitCode).toBe(0); }); - it.skipIf(!isPosix)("in the name of a file that is in the way", async () => { + it("in the name of a file that is in the way", async () => { using dir = tempDir("create-conflict", { "bun-create/tpl/package.json": JSON.stringify({ name: "tpl" }), [`bun-create/tpl/a${raw}b.js`]: "template", diff --git a/test/cli/install/bun-install-backends.test.ts b/test/cli/install/bun-install-backends.test.ts index d3109ae33193..50be153c4b02 100644 --- a/test/cli/install/bun-install-backends.test.ts +++ b/test/cli/install/bun-install-backends.test.ts @@ -161,7 +161,7 @@ function projectWithLocalPackage(name: string) { return { dir, projectDir: String(dir), packageDir: join(String(dir), "pkg") }; } -describe.skipIf(isWindows)("install backends on POSIX", () => { +describe("install backends on POSIX", () => { // The copyfile backend only materializes files (parents are created on // demand), the others also create the package's directories. const directoryBackends = ["hardlink", "symlink", ...(isMacOS ? ["clonefile_each_dir"] : [])]; diff --git a/test/cli/install/bun-install-env-file.test.ts b/test/cli/install/bun-install-env-file.test.ts index b93a7c663b53..891c5ad10f33 100644 --- a/test/cli/install/bun-install-env-file.test.ts +++ b/test/cli/install/bun-install-env-file.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "node:path"; // https://github.com/oven-sh/bun/issues/12011 @@ -464,7 +464,7 @@ describe("bun install .env loading and the security scanner", () => { }); // A pipe can be read once. Install reads it, so the scanner must not be sent to the path. - test.skipIf(isWindows)("--env-file from a pipe", async () => { + test("--env-file from a pipe", async () => { const { seen, exitCode } = await scannerEnv({ sh: `printf 'SCAN_CUSTOM=from-pipe\\n' | "$0" install --env-file=/dev/stdin`, }); diff --git a/test/cli/install/bun-install-git-deps.test.ts b/test/cli/install/bun-install-git-deps.test.ts index 47c5ebc990a4..7d6b51cdf6a6 100644 --- a/test/cli/install/bun-install-git-deps.test.ts +++ b/test/cli/install/bun-install-git-deps.test.ts @@ -675,7 +675,7 @@ function expectOneStoreEntry(project: string, aliases: string[]) { // folder taken moves it away (`move_to_cache_directory` in extract_tarball.rs) while another one is still reading its // package.json. Occasionally that one creates a package with no name and no dependencies. for (const linker of ["isolated", "hoisted"] as const) { - test.concurrent.skipIf(isWindows)( + test.concurrent( `${linker} linker installs github: refs that point to one commit as one package`, async () => { using dir = tempDir(`github-one-commit-${linker}`, {}); @@ -739,7 +739,7 @@ for (const linker of ["isolated", "hoisted"] as const) { // The same for a repository whose package.json has no name, or that has no package.json. The package is named after the // repository, for every ref. -test.concurrent.skipIf(isWindows).each([ +test.concurrent.each([ ["a package.json without a name", { "package.json": JSON.stringify({ version: "1.0.0" }) }], ["no package.json", {}], ])( @@ -1642,7 +1642,7 @@ test.concurrent( // that finishes second finds the other's clone already in the cache. It used to swap in its own clone and delete the // other one, which the first install still had open. The first install's checkout then failed with "fatal: repository // '/..tmp (deleted)' does not exist". The same applies to the checkout folder. -test.concurrent.skipIf(isWindows)( +test.concurrent( "an install keeps the git cache folders that another install published first", async () => { using dir = tempDir("git-dep-publish-race", {}); @@ -2299,7 +2299,7 @@ for (const linker of ["hoisted", "isolated"] as const) { // `git checkout` fails for a repository that holds a path the platform // cannot create. Here a wrapper around git fails it once `fail-checkout` exists. - test.concurrent.skipIf(isWindows)( + test.concurrent( `${linker} linker checks out again for a locked dependency after the checkout failed for another one`, async () => { using dir = tempDir(`git-dep-${linker}-failed-checkout`, {}); @@ -2572,7 +2572,7 @@ test.concurrent("a GIT_SSH_COMMAND in the project's .env does not replace the us expect(exitCode).toBe(1); }); -test.concurrent.skipIf(isWindows)("a PATH in the project's .env does not choose the git of the install", async () => { +test.concurrent("a PATH in the project's .env does not choose the git of the install", async () => { using dir = tempDir("git-dep-dotenv-path", {}); const root = String(dir); const ran = join(root, "ran"); @@ -2658,7 +2658,7 @@ async function failingGitCalls(dependency: string) { } // The install tries the https form of a clone URL, then the ssh form. -test.concurrent.skipIf(isWindows).each([ +test.concurrent.each([ // #36931: a URL that parses is passed on as it is. The port used to become a // path segment (`ssh://git@localhost/52626/...`). ["git+ssh://git@localhost:52626/user/repo.git#v1.0.0", "git@localhost:52626/user/repo.git"], @@ -2826,7 +2826,7 @@ test.concurrent.each([ }); // It's neither a URL nor an scp-style path, so git would read it as a directory in the project and clone that. -test.concurrent.skipIf(isWindows)("does not hand a bare word to git clone", async () => { +test.concurrent("does not hand a bare word to git clone", async () => { const { calls, stderr, exitCode } = await failingGitCalls("git+:latest"); expect(calls).toEqual([]); // It's a git dependency, so the registry wasn't asked for a `git+:latest` tag. @@ -2917,25 +2917,23 @@ test.concurrent("bun install sorts the workspace dependency by its res // one of the git processes instead of stopping the install, and concurrent // clones raced on the forwarder's process-wide state. On Linux the git // processes now carry PR_SET_PDEATHSIG, so they die with bun. -test.concurrent.skipIf(isWindows)( - "SIGINT during git clones stops the install and is not forwarded to git", - async () => { - using dir = tempDir("git-dep-sigint", {}); - const root = String(dir); - const bin = join(root, "bin"); - mkdirSync(bin); - const running = join(root, "git-running"); - const exited = join(root, "git-exited"); - const gotSigint = join(root, "git-got-sigint"); - const lineCount = (file: string) => (existsSync(file) ? readFileSync(file, "utf8").split("\n").length - 1 : 0); - // A fake git. Each process appends a line to `running` when it starts, to - // `exited` when it ends, and blocks until the test deletes `running`. A - // SIGINT that bun forwards to one of them is recorded in `gotSigint`, which - // makes every other fake git (the concurrent clone, the retry over ssh) - // exit at once. The `git config` that comes before the first clone is not one of them. - writeFileSync( - join(bin, "git"), - `#!/bin/sh +test.concurrent("SIGINT during git clones stops the install and is not forwarded to git", async () => { + using dir = tempDir("git-dep-sigint", {}); + const root = String(dir); + const bin = join(root, "bin"); + mkdirSync(bin); + const running = join(root, "git-running"); + const exited = join(root, "git-exited"); + const gotSigint = join(root, "git-got-sigint"); + const lineCount = (file: string) => (existsSync(file) ? readFileSync(file, "utf8").split("\n").length - 1 : 0); + // A fake git. Each process appends a line to `running` when it starts, to + // `exited` when it ends, and blocks until the test deletes `running`. A + // SIGINT that bun forwards to one of them is recorded in `gotSigint`, which + // makes every other fake git (the concurrent clone, the retry over ssh) + // exit at once. The `git config` that comes before the first clone is not one of them. + writeFileSync( + join(bin, "git"), + `#!/bin/sh [ "$6" = config ] && exit 1 trap 'echo $$ >> ${quote(exited)}' EXIT trap ': > ${quote(gotSigint)}; exit 130' INT @@ -2944,68 +2942,67 @@ i=0 while [ -e ${quote(running)} ] && [ ! -e ${quote(gotSigint)} ] && [ $i -lt 600 ]; do sleep 0.05; i=$((i+1)); done exit 1 `, - { mode: 0o755 }, - ); - // two repositories, so two clones run at the same time - const project = writeProject(root, { - [nameOf("a")]: "git+https://localhost/scope/pkg-a.git", - [nameOf("b")]: "git+https://localhost/scope/pkg-b.git", - }); - const env: typeof gitEnv = { ...gitEnv, BUN_INSTALL_CACHE_DIR: join(root, "cache"), PATH: `${bin}:${gitEnv.PATH}` }; - delete env.BUN_FEATURE_FLAG_NO_ORPHANS; - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: project, - env, - stdout: "pipe", - stderr: "pipe", - }); - const stdout = proc.stdout.text(); - const stderr = proc.stderr.text(); - try { - const deadline = Date.now() + 20_000; - while (lineCount(running) < 2) { - if (proc.exitCode !== null || proc.signalCode !== null) { - throw new Error(`install exited before it ran git:\n${await stderr}`); - } - if (Date.now() > deadline) throw new Error(`install ran ${lineCount(running)} of 2 clones`); - await Bun.sleep(10); + { mode: 0o755 }, + ); + // two repositories, so two clones run at the same time + const project = writeProject(root, { + [nameOf("a")]: "git+https://localhost/scope/pkg-a.git", + [nameOf("b")]: "git+https://localhost/scope/pkg-b.git", + }); + const env: typeof gitEnv = { ...gitEnv, BUN_INSTALL_CACHE_DIR: join(root, "cache"), PATH: `${bin}:${gitEnv.PATH}` }; + delete env.BUN_FEATURE_FLAG_NO_ORPHANS; + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: project, + env, + stdout: "pipe", + stderr: "pipe", + }); + const stdout = proc.stdout.text(); + const stderr = proc.stderr.text(); + try { + const deadline = Date.now() + 20_000; + while (lineCount(running) < 2) { + if (proc.exitCode !== null || proc.signalCode !== null) { + throw new Error(`install exited before it ran git:\n${await stderr}`); } - proc.kill("SIGINT"); - await Promise.all([stdout, stderr, proc.exited]); - // Read the marker only once every fake git is gone, so that a trap that - // fires after bun exited still counts. - const gone = Date.now() + 10_000; - const pids = readFileSync(running, "utf8").trim().split("\n").map(Number); - if (isLinux) { - // The kernel kills them together with bun (PR_SET_PDEATHSIG). `running` still exists. - const alive = (pid: number) => { - try { - return !readFileSync(`/proc/${pid}/status`, "utf8").includes("State:\tZ"); - } catch { - return false; - } - }; - while (pids.some(alive)) { - if (Date.now() > gone) throw new Error(`fake gits outlived bun: ${pids.filter(alive)}`); - await Bun.sleep(10); - } - } else { - rmSync(running, { force: true }); - while (lineCount(exited) < pids.length) { - if (Date.now() > gone) throw new Error(`${lineCount(exited)} of ${pids.length} fake gits exited`); - await Bun.sleep(10); + if (Date.now() > deadline) throw new Error(`install ran ${lineCount(running)} of 2 clones`); + await Bun.sleep(10); + } + proc.kill("SIGINT"); + await Promise.all([stdout, stderr, proc.exited]); + // Read the marker only once every fake git is gone, so that a trap that + // fires after bun exited still counts. + const gone = Date.now() + 10_000; + const pids = readFileSync(running, "utf8").trim().split("\n").map(Number); + if (isLinux) { + // The kernel kills them together with bun (PR_SET_PDEATHSIG). `running` still exists. + const alive = (pid: number) => { + try { + return !readFileSync(`/proc/${pid}/status`, "utf8").includes("State:\tZ"); + } catch { + return false; } + }; + while (pids.some(alive)) { + if (Date.now() > gone) throw new Error(`fake gits outlived bun: ${pids.filter(alive)}`); + await Bun.sleep(10); } - expect({ signalCode: proc.signalCode, gitGotSigint: existsSync(gotSigint) }).toEqual({ - signalCode: "SIGINT", - gitGotSigint: false, - }); - } finally { + } else { rmSync(running, { force: true }); + while (lineCount(exited) < pids.length) { + if (Date.now() > gone) throw new Error(`${lineCount(exited)} of ${pids.length} fake gits exited`); + await Bun.sleep(10); + } } - }, -); + expect({ signalCode: proc.signalCode, gitGotSigint: existsSync(gotSigint) }).toEqual({ + signalCode: "SIGINT", + gitGotSigint: false, + }); + } finally { + rmSync(running, { force: true }); + } +}); // bun.lock resolves an optional git dependency, the cache is cold, and the // repository is gone. The install phase failed for it with either linker. An @@ -3495,7 +3492,7 @@ test.concurrent("bun update moves a git dependency without a committish to the n }); // Ubuntu 22.04 has git 2.34, Debian 11 has 2.30. -test.concurrent.skipIf(isWindows)("clones with a git older than 2.39, which refuses --bare with --origin", async () => { +test.concurrent("clones with a git older than 2.39, which refuses --bare with --origin", async () => { using dir = tempDir("git-dep-old-git", { "bin/git": `#!/bin/sh for arg in "$@"; do @@ -3783,7 +3780,7 @@ test.concurrent("installs that refresh one cached repository at the same time al }); // The same without relying on timing: while the first `git fetch` runs, another git holds the lock on the branch. -test.concurrent.skipIf(isWindows)("runs git fetch once more when it could not write a ref", async () => { +test.concurrent("runs git fetch once more when it could not write a ref", async () => { using dir = tempDir("git-dep-refresh-locked", {}); const root = String(dir); const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); @@ -3932,7 +3929,7 @@ test.concurrent("a git dependency with a submodule, under the user's own setting }); // How git fails when it can't use the clone depends on its version. -test.concurrent.skipIf(isWindows)("clones again after a git fetch that did not fail at the remote", async () => { +test.concurrent("clones again after a git fetch that did not fail at the remote", async () => { using dir = tempDir("git-dep-fetch-exit-code", {}); const root = String(dir); const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); @@ -3974,7 +3971,7 @@ type Places = { clone: string; other: string; kept: string; command: string }; // since git 2.45 const usageOfInit = Bun.spawnSync({ cmd: ["git", "init", "-h"], env: gitEnv }); const gitKnowsReftable = `${usageOfInit.stdout}${usageOfInit.stderr}`.includes("--ref-format"); -test.concurrent.skipIf(isWindows).each<{ what: string; change(at: Places): void }>([ +test.concurrent.each<{ what: string; change(at: Places): void }>([ { what: "is a link", change({ clone, other }) { @@ -4146,7 +4143,7 @@ test.concurrent.each([ await install(); // A checkout that an older version left in the cache is created again. - if (isWindows) return; + const checkout = join(cache, `@G@${branchCommits(bare).main}`); symlinkSync(join(root, target), join(checkout, "package.json")); rmSync(join(project, "node_modules"), { recursive: true }); @@ -4183,7 +4180,7 @@ test.concurrent("a package.json that is a link to a file of the repository is a await install(); // A checkout that an older version left in the cache is created again. - if (isWindows) return; + rmSync(cached); symlinkSync("real/manifest.json", cached); rmSync(join(project, "node_modules"), { recursive: true }); @@ -4333,7 +4330,7 @@ test.concurrent("a git dependency keeps its executable files and the folder of a expect(exitCode).toBe(0); }); -test.concurrent.skipIf(isWindows)("reports the required git version when git has no `restore` command", async () => { +test.concurrent("reports the required git version when git has no `restore` command", async () => { const notACommand = "git: 'restore' is not a git command. See 'git --help'."; using dir = tempDir("git-dep-old-git", { "bin/git": `#!/bin/sh @@ -4371,7 +4368,7 @@ test.concurrent("two commits of one repository in one install", async () => { // The https form of an ssh URL is tried first. A host answers "not found" for a private // repository to a login without access, so that answer does not end the search. What git // said of a form that failed is of interest only if no form works. -test.concurrent.skipIf(isWindows).each([ +test.concurrent.each([ { status: 404, body: "Repository not found.", sshWorks: true }, { status: 403, body: "Forbidden", sshWorks: true }, { status: 404, body: "Repository not found.", sshWorks: false }, @@ -4474,7 +4471,7 @@ test.concurrent("every git starts with the same settings, spelled for the platfo expect(started[0][4]).toBe(`--git-dir=${nullDevice}`); }); -test.concurrent.skipIf(isWindows)("asks git for the user's own commands once, before the first clone", async () => { +test.concurrent("asks git for the user's own commands once, before the first clone", async () => { using dir = tempDir("git-dep-config-once", { "bin/git": `#!/bin/sh\necho "$5 $6 $7" >> "$GIT_CALLS"\nexit 1\n` }); const root = String(dir); chmodSync(join(root, "bin", "git"), 0o755); @@ -4527,7 +4524,7 @@ type Place = { where: string; files?: Record; env?: Record([ +test.concurrent.each([ { where: "~/.gitconfig", files: { "home/.gitconfig": mySshCommand } }, { where: "~/.gitconfig, under [Core]", files: { "home/.gitconfig": "[Core]\n\tsshCommand = ROOT/mine\n" } }, { where: "~/.gitconfig, in one line", files: { "home/.gitconfig": "[core] sshCommand = ROOT/mine\n" } }, @@ -4549,7 +4546,7 @@ test.concurrent.skipIf(isWindows).each([ }); // No clone reads the config of the repository it is started in. -test.concurrent.skipIf(isWindows).each([ +test.concurrent.each([ { where: "nowhere" }, { where: "only in the repository of the project", @@ -4566,7 +4563,7 @@ test.concurrent.skipIf(isWindows).each([ expect(ran.filter(line => !line.startsWith("ssh -oStrictHostKeyChecking=accept-new "))).toEqual([]); }); -test.concurrent.skipIf(isWindows)("git runs the user's own core.askPass from ~/.config/git/config", async () => { +test.concurrent("git runs the user's own core.askPass from ~/.config/git/config", async () => { await using server = Bun.serve({ port: 0, hostname: "127.0.0.1", @@ -4582,13 +4579,11 @@ test.concurrent.skipIf(isWindows)("git runs the user's own core.askPass from ~/. // dependency spell the same repository in two ways. A cold-cache install from the lockfile with the isolated linker // cloned it under both spellings and never finished. The ssh replacement runs the command on this machine. for (const user of ["git", "alice"]) { - test - .skipIf(isWindows) - .concurrent( - `a cold-cache install from bun.lock clones an scp-form dependency (${user}@host:path) once`, - async () => { - using dir = tempDir("git-dep-scp", { - "ssh.sh": `#!/bin/sh + test.concurrent( + `a cold-cache install from bun.lock clones an scp-form dependency (${user}@host:path) once`, + async () => { + using dir = tempDir("git-dep-scp", { + "ssh.sh": `#!/bin/sh echo "$*" >> "$(dirname "$0")/ssh.log" while [ $# -gt 1 ]; do case "$1" in @@ -4600,29 +4595,29 @@ done shift exec sh -c "$1" `, - }); - const root = String(dir); - chmodSync(join(root, "ssh.sh"), 0o755); - const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); - await git(bare, "symbolic-ref", "HEAD", "refs/heads/main"); - const project = writeProject(root, { dep: `${user}@localhost:${bare}` }); - const env = { GIT_SSH_COMMAND: join(root, "ssh.sh"), GIT_TERMINAL_PROMPT: "0" }; + }); + const root = String(dir); + chmodSync(join(root, "ssh.sh"), 0o755); + const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); + await git(bare, "symbolic-ref", "HEAD", "refs/heads/main"); + const project = writeProject(root, { dep: `${user}@localhost:${bare}` }); + const env = { GIT_SSH_COMMAND: join(root, "ssh.sh"), GIT_TERMINAL_PROMPT: "0" }; - expect((await runInstall(project, join(root, "cache-1"), env, "--linker=isolated")).exitCode).toBe(0); - const lockfile = readFileSync(join(project, "bun.lock"), "utf8"); - expect(lockfile).toContain(`"dep@git+ssh://${user}@localhost:`); + expect((await runInstall(project, join(root, "cache-1"), env, "--linker=isolated")).exitCode).toBe(0); + const lockfile = readFileSync(join(project, "bun.lock"), "utf8"); + expect(lockfile).toContain(`"dep@git+ssh://${user}@localhost:`); - rmSync(join(project, "node_modules"), { recursive: true }); - rmSync(join(root, "ssh.log")); - const { exitCode } = await runInstall(project, join(root, "cache-2"), env, "--linker=isolated"); - expect(await installedVersionOf(project, "dep")).toBe("main"); - expect( - readFileSync(join(root, "ssh.log"), "utf8") - .split("\n") - .filter(line => line.includes("git-upload-pack")), - ).toHaveLength(1); - expect(readFileSync(join(project, "bun.lock"), "utf8")).toBe(lockfile); - expect(exitCode).toBe(0); - }, - ); + rmSync(join(project, "node_modules"), { recursive: true }); + rmSync(join(root, "ssh.log")); + const { exitCode } = await runInstall(project, join(root, "cache-2"), env, "--linker=isolated"); + expect(await installedVersionOf(project, "dep")).toBe("main"); + expect( + readFileSync(join(root, "ssh.log"), "utf8") + .split("\n") + .filter(line => line.includes("git-upload-pack")), + ).toHaveLength(1); + expect(readFileSync(join(project, "bun.lock"), "utf8")).toBe(lockfile); + expect(exitCode).toBe(0); + }, + ); } diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts index b2b134c55469..d16fed19e177 100644 --- a/test/cli/install/bun-install-lifecycle-scripts.test.ts +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -2140,7 +2140,7 @@ describe("the trusted set", () => { // Nobody trusts `payload`, and it has no script. Its `bin` has the name of a command that `native`, which is trusted, // runs. - describe.skipIf(isWindows)("a bin of another package does not replace a command inside a trusted script", () => { + describe("a bin of another package does not replace a command inside a trusted script", () => { const files = (script: "preinstall" | "postinstall", trusted: boolean) => ({ "package.json": { name: "foo", @@ -2510,20 +2510,17 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("a bin whose file a s tool: { bin: { tool: "dist/tool" }, scripts: { postinstall: build }, files }, }); await expectLinked(ctx, { dependencies: { "a-tool": "1.0.0", tool: "1.0.0" }, trustedDependencies: ["tool"] }); - if (!isWindows) expect(readlinkSync(ctx.installed(".bin", "tool"))).toBe(join("..", "a-tool", "cli.js")); + expect(readlinkSync(ctx.installed(".bin", "tool"))).toBe(join("..", "a-tool", "cli.js")); }); // The file of the bin is in the tarball, so nothing is left to link after the script. - test.skipIf(isWindows || linker === "isolated")( - "a program that a script writes in place of its link is kept", - async () => { - using ctx = await setupScriptPackages(linker, { - tool: { bin: { tool: "cli.js" }, scripts: { postinstall: "rm ../.bin/tool && cp cli.js ../.bin/tool" }, files }, - }); - await expectLinked(ctx, { dependencies: { tool: "1.0.0" }, trustedDependencies: ["tool"] }); - expect(lstatSync(ctx.installed(".bin", "tool")).isFile()).toBe(true); - }, - ); + test.skipIf(linker === "isolated")("a program that a script writes in place of its link is kept", async () => { + using ctx = await setupScriptPackages(linker, { + tool: { bin: { tool: "cli.js" }, scripts: { postinstall: "rm ../.bin/tool && cp cli.js ../.bin/tool" }, files }, + }); + await expectLinked(ctx, { dependencies: { tool: "1.0.0" }, trustedDependencies: ["tool"] }); + expect(lstatSync(ctx.installed(".bin", "tool")).isFile()).toBe(true); + }); }); describe.concurrent.each(["hoisted", "isolated"] as const)("bun add --trust (%s)", linker => { @@ -2844,7 +2841,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("default node-gyp scr describe.concurrent.each(["hoisted", "isolated"] as const)("scripts that run at the same time (%s)", linker => { const names = ["a", "b", "c", "d"]; - test.skipIf(isWindows).each([ + test.each([ { title: "--concurrent-scripts=1", bunfig: "", args: ["--concurrent-scripts=1"], limit: 1 }, { title: "--concurrent-scripts=2", bunfig: "", args: ["--concurrent-scripts=2"], limit: 2 }, { title: "concurrentScripts = 1", bunfig: "concurrentScripts = 1\n", args: [], limit: 1 }, @@ -2940,69 +2937,65 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("scripts that run at 30_000, ); - test.skipIf(isWindows)( - "a script that ends makes room for the next one while another keeps running", - async () => { - using ctx = await setupScriptPackages( - linker, - Object.fromEntries( - names.map(name => [ - name, - { scripts: { postinstall: `echo ${name} >> "$STARTED" && read line < "$GATES/${name}"` } }, - ]), - ), - ); - await writeFile( - ctx.packageJson, - JSON.stringify({ - name: "foo", - dependencies: Object.fromEntries(names.map(name => [name, "1.0.0"])), - trustedDependencies: names, - }), - ); - expect((await ctx.bun("install", "--lockfile-only")).exitCode).toBe(0); + test("a script that ends makes room for the next one while another keeps running", async () => { + using ctx = await setupScriptPackages( + linker, + Object.fromEntries( + names.map(name => [ + name, + { scripts: { postinstall: `echo ${name} >> "$STARTED" && read line < "$GATES/${name}"` } }, + ]), + ), + ); + await writeFile( + ctx.packageJson, + JSON.stringify({ + name: "foo", + dependencies: Object.fromEntries(names.map(name => [name, "1.0.0"])), + trustedDependencies: names, + }), + ); + expect((await ctx.bun("install", "--lockfile-only")).exitCode).toBe(0); - const gates = join(ctx.packageDir, "gates"); - await mkdir(gates); - expect(Bun.spawnSync({ cmd: ["mkfifo", ...names.map(name => join(gates, name))] }).exitCode).toBe(0); - const pipes = new Map( - names.map(name => [name, openSync(join(gates, name), fsConstants.O_RDWR | fsConstants.O_NONBLOCK)]), - ); - const log = join(ctx.packageDir, "started.txt"); - const started = () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean) : []); + const gates = join(ctx.packageDir, "gates"); + await mkdir(gates); + expect(Bun.spawnSync({ cmd: ["mkfifo", ...names.map(name => join(gates, name))] }).exitCode).toBe(0); + const pipes = new Map( + names.map(name => [name, openSync(join(gates, name), fsConstants.O_RDWR | fsConstants.O_NONBLOCK)]), + ); + const log = join(ctx.packageDir, "started.txt"); + const started = () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean) : []); - await using proc = spawn({ - cmd: [bunExe(), "install", "--concurrent-scripts=2"], - cwd: ctx.packageDir, - env: { ...ctx.env, STARTED: log, GATES: gates }, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - try { - // The first one keeps running to the end, so each of the others has to start while it runs. - for (let count = 2; count <= names.length; count++) { - while (started().length < count) { - if (proc.exitCode !== null) throw new Error(`bun install exited with ${proc.exitCode}`); - await Bun.sleep(1); - } - expect(started()).toHaveLength(count); - writeSync(pipes.get(started()[count - 1])!, "\n"); - } - writeSync(pipes.get(started()[0])!, "\n"); - const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); - expect(err).not.toContain("error:"); - expect(exitCode).toBe(0); - } finally { - rmSync(gates, { recursive: true, force: true }); - for (const pipe of pipes.values()) { - writeSync(pipe, "\n"); - closeSync(pipe); + await using proc = spawn({ + cmd: [bunExe(), "install", "--concurrent-scripts=2"], + cwd: ctx.packageDir, + env: { ...ctx.env, STARTED: log, GATES: gates }, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + try { + // The first one keeps running to the end, so each of the others has to start while it runs. + for (let count = 2; count <= names.length; count++) { + while (started().length < count) { + if (proc.exitCode !== null) throw new Error(`bun install exited with ${proc.exitCode}`); + await Bun.sleep(1); } + expect(started()).toHaveLength(count); + writeSync(pipes.get(started()[count - 1])!, "\n"); } - }, - 10_000, - ); + writeSync(pipes.get(started()[0])!, "\n"); + const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(err).not.toContain("error:"); + expect(exitCode).toBe(0); + } finally { + rmSync(gates, { recursive: true, force: true }); + for (const pipe of pipes.values()) { + writeSync(pipe, "\n"); + closeSync(pipe); + } + } + }, 10_000); test("packages that a later install trusts, one script at a time", async () => { using ctx = await setupScriptPackages( @@ -3073,7 +3066,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("the install after a // One at a time, so that the third script to start is the one that fails. Two have finished and aren't run again, and // one hasn't started. - test.skipIf(isWindows).each([ + test.each([ { title: "preinstall", script: "preinstall", args: [] }, { title: "install", script: "install", args: [] }, { title: "postinstall", script: "postinstall", args: [] }, @@ -3103,7 +3096,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("the install after a expect(await install("third.txt", 100, ...args)).toEqual({ errors: [], ran: [], exitCode: 0 }); }); - test.skipIf(isWindows)("all at the same time: every script runs in the end", async () => { + test("all at the same time: every script runs in the end", async () => { const { ctx, install } = await setup("postinstall"); using _ = ctx; @@ -3116,7 +3109,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("the install after a // bun looks for the shell once. The first script removes it, whichever script that is and // however far the other packages are, so the second one has nothing to start with. - test.skipIf(isWindows)("a script that cannot be started", async () => { + test("a script that cannot be started", async () => { const waiting = ["v", "w", "x", "y", "z"]; using ctx = await setupScriptPackages( linker, @@ -3166,7 +3159,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("the install after a // The root's `dep` is a different package. With the hoisted linker, each parent has its own copy, and the first one // fails before the walk reaches the last. - test.skipIf(isWindows)("copies that the failed install hadn't reached", async () => { + test("copies that the failed install hadn't reached", async () => { const parents = Array.from({ length: 40 }, (_, i) => `parent-${i}`); using ctx = await setupScriptPackages(linker, { scripted: { scripts: { postinstall: `echo ran >> "$RUNS" && test -z "$FAIL"` } }, @@ -7041,31 +7034,28 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { ] as [number, string][]) : []), ]; - test.skipIf(isWindows).each(signaled)( - "a postinstall killed by signal %d is reported and re-raised", - async (signal, message) => { - using ctx = await setupTest(); - const { packageDir, packageJson, env } = ctx; - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + test.each(signaled)("a postinstall killed by signal %d is reported and re-raised", async (signal, message) => { + using ctx = await setupTest(); + const { packageDir, packageJson, env } = ctx; + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - await writeFile( - packageJson, - JSON.stringify({ name: "foo", version: "1.0.0", scripts: { postinstall: `kill -${signal} $$` } }), - ); + await writeFile( + packageJson, + JSON.stringify({ name: "foo", version: "1.0.0", scripts: { postinstall: `kill -${signal} $$` } }), + ); - await using proc = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - const [, err, exited] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + const [, err, exited] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(err).toContain(message); - expect(exited).toBe(128 + signal); - }, - ); + expect(err).toContain(message); + expect(exited).toBe(128 + signal); + }); }); describe.concurrent("stdout/stderr is inherited from root scripts during install", async () => { diff --git a/test/cli/install/bun-install-link-path.test.ts b/test/cli/install/bun-install-link-path.test.ts index 01f2304ed5ae..be369895a476 100644 --- a/test/cli/install/bun-install-link-path.test.ts +++ b/test/cli/install/bun-install-link-path.test.ts @@ -498,7 +498,7 @@ describe.each(["hoisted", "isolated"])("link: (%s)", linker => { expect(exitCode).not.toBe(0); }); - test.skipIf(isWindows).concurrent("links the target's bin and doesn't change the file's mode", async () => { + test.concurrent("links the target's bin and doesn't change the file's mode", async () => { using dir = tempDir("link-path-bin", { ...bunfig, "project/package.json": JSON.stringify({ name: "root", dependencies: { dep: "link:./vendor/dep" } }), diff --git a/test/cli/install/bun-install-native-binlink.test.ts b/test/cli/install/bun-install-native-binlink.test.ts index 736a2c838b51..5ce6d591b67b 100644 --- a/test/cli/install/bun-install-native-binlink.test.ts +++ b/test/cli/install/bun-install-native-binlink.test.ts @@ -120,7 +120,7 @@ describe.concurrent("native binlink optimization", () => { // bunx takes the bin of a package from that package's own package.json. Here the entry in `.bin` leads into // the platform package, and the file of the main package keeps the mode of its tarball. - test.skipIf(isWindows)(`bunx runs the platform-specific bin with linker ${linker}`, async () => { + test(`bunx runs the platform-specific bin with linker ${linker}`, async () => { let env = { ...bunEnv }; const { packageDir, packageJson } = await verdaccio.createTestDir(); env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); @@ -277,7 +277,7 @@ describe.concurrent("native binlink optimization", () => { } // The link leaves node_modules, so its text depends on where node_modules really is. - test.skipIf(isWindows)("in a workspace whose node_modules is a link to another folder", async () => { + test("in a workspace whose node_modules is a link to another folder", async () => { const { packageDir, packageJson } = await verdaccio.createTestDir(); const env = { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache") }; const app = join(packageDir, "packages", "app"); @@ -757,65 +757,62 @@ describe.concurrent("native binlink altpath", () => { // touching the target file (no chmod, no shebang rewrite), so the attack this // guards against can't occur there, and the resolved-containment check in // `link_bin_or_create_shim` is `#[cfg(not(windows))]` accordingly. -test.skipIf(isWindows)( - "skips bin entries whose target resolves outside the package directory and keeps outside files untouched", - async () => { - const secretContents = "#!/usr/bin/env node\r\nconsole.log('outside file');\n"; - using dir = tempDir("binlink-resolved-target", { - "package.json": JSON.stringify({ - name: "binlink-containment-app", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - "resolved-escape-pkg": "workspace:*", - "contained-bin-pkg": "workspace:*", - }, - }), - "outside/secret.txt": secretContents, - "packages/resolved-escape-pkg/package.json": JSON.stringify({ - name: "resolved-escape-pkg", - version: "1.0.0", - bin: { "escape-cmd": "./payload/secret.txt" }, - }), - "packages/contained-bin-pkg/package.json": JSON.stringify({ - name: "contained-bin-pkg", - version: "1.0.0", - bin: { "contained-cmd": "bin/run.js" }, - }), - "packages/contained-bin-pkg/bin/run.js": '#!/usr/bin/env node\nconsole.log("contained-cmd ok");\n', - }); +test("skips bin entries whose target resolves outside the package directory and keeps outside files untouched", async () => { + const secretContents = "#!/usr/bin/env node\r\nconsole.log('outside file');\n"; + using dir = tempDir("binlink-resolved-target", { + "package.json": JSON.stringify({ + name: "binlink-containment-app", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + "resolved-escape-pkg": "workspace:*", + "contained-bin-pkg": "workspace:*", + }, + }), + "outside/secret.txt": secretContents, + "packages/resolved-escape-pkg/package.json": JSON.stringify({ + name: "resolved-escape-pkg", + version: "1.0.0", + bin: { "escape-cmd": "./payload/secret.txt" }, + }), + "packages/contained-bin-pkg/package.json": JSON.stringify({ + name: "contained-bin-pkg", + version: "1.0.0", + bin: { "contained-cmd": "bin/run.js" }, + }), + "packages/contained-bin-pkg/bin/run.js": '#!/usr/bin/env node\nconsole.log("contained-cmd ok");\n', + }); - const root = String(dir); - const secretPath = join(root, "outside", "secret.txt"); - // A mode without any execute bits, so a chmod performed by bin linking is observable. - chmodSync(secretPath, 0o600); - // In-package path component that resolves to a directory outside the package. - symlinkSync(join(root, "outside"), join(root, "packages", "resolved-escape-pkg", "payload")); - - await using install = spawn({ - cmd: [bunExe(), "install"], - cwd: root, - stdout: "ignore", - stdin: "ignore", - stderr: "pipe", - env: bunEnv, - }); - const [installStderr, installExit] = await Promise.all([install.stderr.text(), install.exited]); - expect(installStderr).not.toContain("error:"); - expect(installExit).toBe(0); - - // The file outside the package keeps its mode and contents. - expect(statSync(secretPath).mode & 0o777).toBe(0o600); - expect(readFileSync(secretPath, "utf8")).toBe(secretContents); - - // No `.bin` entry was created for the bin whose resolved target leaves the package. - expect(existsSync(join(root, "node_modules", ".bin", "escape-cmd"))).toBeFalse(); - - // The workspace bin that resolves inside its package is still linked and made executable, - // even though node_modules/ is a symlink into packages/. - const containedBin = join(root, "node_modules", ".bin", "contained-cmd"); - expect(existsSync(containedBin)).toBeTrue(); - expect(realpathSync(containedBin)).toBe(realpathSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js"))); - expect(statSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")).mode & 0o111).not.toBe(0); - }, -); + const root = String(dir); + const secretPath = join(root, "outside", "secret.txt"); + // A mode without any execute bits, so a chmod performed by bin linking is observable. + chmodSync(secretPath, 0o600); + // In-package path component that resolves to a directory outside the package. + symlinkSync(join(root, "outside"), join(root, "packages", "resolved-escape-pkg", "payload")); + + await using install = spawn({ + cmd: [bunExe(), "install"], + cwd: root, + stdout: "ignore", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + const [installStderr, installExit] = await Promise.all([install.stderr.text(), install.exited]); + expect(installStderr).not.toContain("error:"); + expect(installExit).toBe(0); + + // The file outside the package keeps its mode and contents. + expect(statSync(secretPath).mode & 0o777).toBe(0o600); + expect(readFileSync(secretPath, "utf8")).toBe(secretContents); + + // No `.bin` entry was created for the bin whose resolved target leaves the package. + expect(existsSync(join(root, "node_modules", ".bin", "escape-cmd"))).toBeFalse(); + + // The workspace bin that resolves inside its package is still linked and made executable, + // even though node_modules/ is a symlink into packages/. + const containedBin = join(root, "node_modules", ".bin", "contained-cmd"); + expect(existsSync(containedBin)).toBeTrue(); + expect(realpathSync(containedBin)).toBe(realpathSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js"))); + expect(statSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")).mode & 0o111).not.toBe(0); +}); diff --git a/test/cli/install/bun-install-patch.test.ts b/test/cli/install/bun-install-patch.test.ts index b396c5f6645e..dd34403cb464 100644 --- a/test/cli/install/bun-install-patch.test.ts +++ b/test/cli/install/bun-install-patch.test.ts @@ -6,7 +6,6 @@ import { bunExe, DirectoryTree, isRoot, - isWindows, normalizeBunSnapshot as normalizeBunSnapshot_, runBunInstall, tempDir, @@ -470,7 +469,7 @@ diff --git a/package.json b/package.json // The new directories used to get the file's mode (0644), which only root can enter // (CAP_DAC_OVERRIDE), so as root the install runs without that capability. const setpriv = isRoot ? Bun.which("setpriv") : null; - test.skipIf(isWindows || (isRoot && !setpriv))( + test.skipIf(isRoot && !setpriv)( "should apply a patch that adds a file in a directory the package does not have", async () => { await using project = createProject("patch-new-dir", { @@ -1315,9 +1314,9 @@ describe("patchedDependencies path longer than the path buffer", () => { // The path is handed to the OS as is. POSIX rejects anything longer than // PATH_MAX with ENAMETOOLONG; Windows may report it as missing instead. - if (!isWindows) { - expect(stderr).toContain("error: failed to read patch file: ENAMETOOLONG: "); - } + + expect(stderr).toContain("error: failed to read patch file: ENAMETOOLONG: "); + expect(stderr).toContain(`${longName}.patch`); expect(exitCode).toBe(1); }); diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 67ffcbc8e71d..b1101e6bbbdc 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -20,7 +20,6 @@ import { bunEnv, bunExe, isDebug, - isFlaky, isLinux, isMacOS, isRoot, @@ -488,7 +487,7 @@ describe.concurrent("certificate authority", () => { // Linux, 1024 on macOS). A joined path that fits reaches the HTTP thread, which reports it in // full. One that does not fit is reported under the name the user gave. Windows is left out: its // buffer holds more than any path the OS accepts. - describe.skipIf(isWindows)("relative cafile near the path buffer length", () => { + describe("relative cafile near the path buffer length", () => { const maxPathBytes = isLinux ? 4096 : 1024; // `cafile` brings `/` to `maxPathBytes + delta` bytes. @@ -3978,7 +3977,7 @@ describe.concurrent("binaries", () => { } // https://github.com/oven-sh/bun/issues/14736 - test.skipIf(isWindows)("reinstall keeps an already-correct .bin link untouched on a read-only tree", async () => { + test("reinstall keeps an already-correct .bin link untouched on a read-only tree", async () => { const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "what-bin": "1.0.0" } })); @@ -4005,7 +4004,7 @@ describe.concurrent("binaries", () => { expect(exitCode).toBe(0); }); - test.skipIf(isWindows)("a stale link, a file and a directory in .bin are replaced", async () => { + test("a stale link, a file and a directory in .bin are replaced", async () => { const { packageDir, packageJson, env } = await setupTest(); const binDir = join(packageDir, "node_modules", ".bin"); await Promise.all([ @@ -4071,7 +4070,7 @@ describe.concurrent("binaries", () => { write(join(binDir, binEntry("map-bin-1"), "inner.txt"), "keep"), write(join(packageDir, "elsewhere.txt"), ""), ]); - if (!isWindows) await symlink(join(packageDir, "elsewhere.txt"), join(binDir, "dep-with-file-bin")); + await symlink(join(packageDir, "elsewhere.txt"), join(binDir, "dep-with-file-bin")); const { err, exitCode } = await runGlobal( packageDir, @@ -4083,11 +4082,11 @@ describe.concurrent("binaries", () => { ); expect(err).toContain("failed to link what-bin: EEXIST"); expect(err).toContain("failed to link dep-with-map-bins: EEXIST"); - if (!isWindows) expect(err).toContain("failed to link dep-with-file-bin: EEXIST"); + expect(err).toContain("failed to link dep-with-file-bin: EEXIST"); expect(await file(join(binDir, binEntry("what-bin"))).text()).toBe("not a bin of what-bin"); expect(await file(join(binDir, binEntry("map-bin-1"), "inner.txt")).text()).toBe("keep"); - if (!isWindows) expect(await readlink(join(binDir, "dep-with-file-bin"))).toBe(join(packageDir, "elsewhere.txt")); + expect(await readlink(join(binDir, "dep-with-file-bin"))).toBe(join(packageDir, "elsewhere.txt")); if (isWindows) expect(await exists(join(binDir, "what-bin.bunx"))).toBeFalse(); // The refused bin does not take the package's other bin down. expect(join(binDir, "map-bin-2")).toBeValidBin(linkTarget("dep-with-map-bins", "map-bin-2")); @@ -4226,7 +4225,7 @@ describe.concurrent("binaries", () => { // linked, and that has to fail the install with `--silent` too. Root ignores the mode of a // directory, so as root the install runs without that capability. const setpriv = isRoot ? Bun.which("setpriv") : null; - describe.skipIf(isWindows || (isRoot && !setpriv))("a link at `.bin` that cannot be removed", () => { + describe.skipIf(isRoot && !setpriv)("a link at `.bin` that cannot be removed", () => { test.each([ ["bun install", []], ["bun install --silent", ["--silent"]], @@ -5084,7 +5083,7 @@ describe.concurrent("binaries", () => { // `/.bin/` into two path buffers of MAX_PATH_BYTES (4096 bytes on Linux, 1024 on // macOS). A project deep enough for one of them not to fit has to fail like a bin name that does // not fit. Windows is left out: its buffer holds more than any path the OS accepts. - describe.skipIf(isWindows)("directories longer than the path buffer", () => { + describe("directories longer than the path buffer", () => { const maxPathBytes = isLinux ? 4096 : 1024; // Writes `/`, a package whose bin is named after it. @@ -5440,7 +5439,7 @@ describe.concurrent("binaries", () => { // The entries of a bin directory are joined onto the directory's path as // well, and so is the temporary file used to rewrite a CRLF shebang. - test.skipIf(isWindows)("entries of a directories.bin close to the path limit", async () => { + test("entries of a directories.bin close to the path limit", async () => { const { packageDir, install, createBinChain } = await setupLongBinTest(); // Most of the depth goes into the project directory: the `.bin` links are // relative, so everything below the package ends up in the link targets, @@ -5486,7 +5485,7 @@ describe.concurrent("binaries", () => { // The link itself is relative to the bin directory. A global bin directory // on another branch of the tree contributes one `..` per component, so the // link target can be longer than the target's absolute path. - test.skipIf(isWindows)("bun link with a link target longer than the path buffer", async () => { + test("bun link with a link target longer than the path buffer", async () => { const { packageDir, env, run, createBinChain } = await setupLongBinTest(); // As in the previous test, most of the depth has to sit above everything // that ends up in a link target that is meant to be created (XFS), so it @@ -5853,38 +5852,36 @@ describe.concurrent("binaries", () => { ...claim(dir, name), [`${dir}/claim.js`]: "#!/usr/bin/env node\r\n", }); - test - .skipIf(isWindows) - .each<[title: string, linker: string, files: Record, loser: string]>([ - [ - "a workspace package", - "hoisted", - { - "package.json": { name: "foo", workspaces: ["packages/*"], dependencies: direct }, - ...claimWithCRLF("packages/aaa", "aaa"), - }, - "packages/aaa/claim.js", - ], - [ - "a file: directory", - "hoisted", - { - "package.json": { name: "foo", dependencies: { ...direct, aaa: "file:./aaa" } }, - ...claimWithCRLF("aaa", "aaa"), - }, - "node_modules/aaa/claim.js", - ], - [ - "a workspace package in a member's .bin", - "isolated", - { - "package.json": { name: "foo", workspaces: ["packages/*"] }, - "packages/app/package.json": { name: "app", dependencies: { ...direct, aaa: "workspace:*" } }, - ...claimWithCRLF("packages/aaa", "aaa"), - }, - "packages/aaa/claim.js", - ], - ])("the file of %s that does not get it is made executable (%s)", async (_, linker, files, loser) => { + test.each<[title: string, linker: string, files: Record, loser: string]>([ + [ + "a workspace package", + "hoisted", + { + "package.json": { name: "foo", workspaces: ["packages/*"], dependencies: direct }, + ...claimWithCRLF("packages/aaa", "aaa"), + }, + "packages/aaa/claim.js", + ], + [ + "a file: directory", + "hoisted", + { + "package.json": { name: "foo", dependencies: { ...direct, aaa: "file:./aaa" } }, + ...claimWithCRLF("aaa", "aaa"), + }, + "node_modules/aaa/claim.js", + ], + [ + "a workspace package in a member's .bin", + "isolated", + { + "package.json": { name: "foo", workspaces: ["packages/*"] }, + "packages/app/package.json": { name: "app", dependencies: { ...direct, aaa: "workspace:*" } }, + ...claimWithCRLF("packages/aaa", "aaa"), + }, + "packages/aaa/claim.js", + ], + ])("the file of %s that does not get it is made executable (%s)", async (_, linker, files, loser) => { const { packageDir, env } = await setupTest(); await Promise.all( Object.entries(files).map(([path, content]) => @@ -7862,7 +7859,7 @@ describe.concurrent("hoisting", async () => { }); }); - test.serial.todoIf(isFlaky && isWindows)("text lockfile is hoisted", async () => { + test.serial("text lockfile is hoisted", async () => { const { packageDir, packageJson, env } = await setupTest(); // Each dependency depends on 'hoist-lockfile-shared'. // 1 - "*" @@ -13889,7 +13886,7 @@ describe("global bin links when the package dir is inside the bin dir", () => { expect(await exited).toBe(0); }); - test.skipIf(isWindows)("the bins are symlinks relative to the bin dir", async () => { + test("the bins are symlinks relative to the bin dir", async () => { expect(await readlink(join(binDir, "bin-bun"))).toBe(join("global", "node_modules", "bunx-bins", "bin-bun.ts")); expect(await exists(join(binDir, "bin-bun"))).toBe(true); }); diff --git a/test/cli/install/bun-install-security-provider.test.ts b/test/cli/install/bun-install-security-provider.test.ts index c4533bcc8996..d7025cb968f7 100644 --- a/test/cli/install/bun-install-security-provider.test.ts +++ b/test/cli/install/bun-install-security-provider.test.ts @@ -1,4 +1,4 @@ -import { bunEnv, bunExe, githubTarball, isWindows, runBunInstall, tempDir } from "harness"; +import { bunEnv, bunExe, githubTarball, runBunInstall, tempDir } from "harness"; import { existsSync, rmSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -1353,7 +1353,7 @@ describe.concurrent("where the scanner is looked up", () => { expect(await install(files)).toEqual({ ran: "a-scanner", exitCode: 0 }); }); - it.skipIf(isWindows)("a file with a quote in its name", async () => { + it("a file with a quote in its name", async () => { const files = { "project/bunfig.toml": bunfig('./s"q.ts'), 'project/s"q.ts': scannerOf("the project") }; expect(await install(files)).toEqual({ ran: "the project", exitCode: 0 }); }); diff --git a/test/cli/install/bun-install-staging.test.ts b/test/cli/install/bun-install-staging.test.ts index 983d49e458cc..70e81df2ba09 100644 --- a/test/cli/install/bun-install-staging.test.ts +++ b/test/cli/install/bun-install-staging.test.ts @@ -512,7 +512,7 @@ const otherPackageDirs = { // The script is POSIX sh; on Windows lifecycle scripts run in bun's own shell. for (const [linker, otherPackageDir] of Object.entries(otherPackageDirs)) { - test.concurrent.skipIf(isWindows)( + test.concurrent( `${linker} linker: a lifecycle script can run bun install in the project being installed`, async () => { const scripts = { install: installsInTheSameProject }; @@ -566,82 +566,79 @@ const failOnceLinkingStarts = [ ].join("; "); // The script is POSIX sh; on Windows lifecycle scripts run in bun's own shell. -test.skipIf(isWindows)( - "isolated linker: a lifecycle script that fails mid-link leaves no partial package", - async () => { - const scripts = { install: failOnceLinkingStarts }; - const manyFiles: RegistryPackage = { tgz: await new Bun.Archive(archiveEntries, { compress: "gzip" }).bytes() }; - const failingScript: RegistryPackage = { - tgz: await new Bun.Archive( - { "package/package.json": JSON.stringify({ name: "failing-script", version: "1.0.0", scripts }) }, - { compress: "gzip" }, - ).bytes(), - scripts, - }; - using registry = serveRegistry({ "many-files": manyFiles, "failing-script": failingScript }); - - const dependencies = { "many-files": "1.0.0" }; - const rootPackageJson = { name: "app", private: true, workspaces: ["packages/*"] }; - using dir = tempDir("staging-script-failure", { - "package.json": JSON.stringify({ ...rootPackageJson, trustedDependencies: ["failing-script"] }), - "packages/a/package.json": JSON.stringify({ - name: "pkg-a", - version: "1.0.0", - dependencies: { ...dependencies, "failing-script": "1.0.0" }, +test("isolated linker: a lifecycle script that fails mid-link leaves no partial package", async () => { + const scripts = { install: failOnceLinkingStarts }; + const manyFiles: RegistryPackage = { tgz: await new Bun.Archive(archiveEntries, { compress: "gzip" }).bytes() }; + const failingScript: RegistryPackage = { + tgz: await new Bun.Archive( + { "package/package.json": JSON.stringify({ name: "failing-script", version: "1.0.0", scripts }) }, + { compress: "gzip" }, + ).bytes(), + scripts, + }; + using registry = serveRegistry({ "many-files": manyFiles, "failing-script": failingScript }); + + const dependencies = { "many-files": "1.0.0" }; + const rootPackageJson = { name: "app", private: true, workspaces: ["packages/*"] }; + using dir = tempDir("staging-script-failure", { + "package.json": JSON.stringify({ ...rootPackageJson, trustedDependencies: ["failing-script"] }), + "packages/a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + dependencies: { ...dependencies, "failing-script": "1.0.0" }, + }), + "packages/b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", dependencies }), + "bunfig.toml": ({ root }) => + Bun.TOML.stringify({ + install: { registry: registry.url.href, cache: join(root, ".bun-cache"), linker: "isolated" }, }), - "packages/b/package.json": JSON.stringify({ name: "pkg-b", version: "1.0.0", dependencies }), - "bunfig.toml": ({ root }) => - Bun.TOML.stringify({ - install: { registry: registry.url.href, cache: join(root, ".bun-cache"), linker: "isolated" }, - }), - }); - const root = String(dir); - const installed = packageDirs.isolated(root); - const installedParent = join(installed, ".."); + }); + const root = String(dir); + const installed = packageDirs.isolated(root); + const installedParent = join(installed, ".."); - const lockfileOnly = await install(root, ["--lockfile-only"]); - expect(lockfileOnly.stderr).not.toContain("error"); - expect(lockfileOnly.exitCode).toBe(0); + const lockfileOnly = await install(root, ["--lockfile-only"]); + expect(lockfileOnly.stderr).not.toContain("error"); + expect(lockfileOnly.exitCode).toBe(0); - const scriptStarted = Promise.withResolvers(); - manyFiles.release = scriptStarted.promise; - const watcher = watch(root, (_, filename) => { - if (filename === "script-started") scriptStarted.resolve(); + const scriptStarted = Promise.withResolvers(); + manyFiles.release = scriptStarted.promise; + const watcher = watch(root, (_, filename) => { + if (filename === "script-started") scriptStarted.resolve(); + }); + try { + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: root, + env: { + ...installEnv, + STAGING_TEST_SCRIPT_STARTED: join(root, "script-started"), + STAGING_TEST_LINK_DIR: installedParent, + }, + stdout: "pipe", + stderr: "pipe", }); - try { - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: root, - env: { - ...installEnv, - STAGING_TEST_SCRIPT_STARTED: join(root, "script-started"), - STAGING_TEST_LINK_DIR: installedParent, - }, - stdout: "pipe", - stderr: "pipe", - }); - // An install that ends without running the script must not leave the registry waiting. - proc.exited.then(() => scriptStarted.resolve()); - const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toContain('install script from "failing-script" exited with 1'); - expect(exitCode).toBe(1); - } finally { - watcher.close(); - } - // Either the package is not at its final path yet, or all of it is. - if (existsSync(installed)) { - expect(compareWithExpectedTree(installed)).toEqual(completeTree); - } - - // This is what the reporter did next: remove the trust and install again. - writeFileSync(join(root, "package.json"), JSON.stringify(rootPackageJson)); - const second = await install(root); - expect(second.stderr).not.toContain("error"); - expect(second.exitCode).toBe(0); + // An install that ends without running the script must not leave the registry waiting. + proc.exited.then(() => scriptStarted.resolve()); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain('install script from "failing-script" exited with 1'); + expect(exitCode).toBe(1); + } finally { + watcher.close(); + } + // Either the package is not at its final path yet, or all of it is. + if (existsSync(installed)) { expect(compareWithExpectedTree(installed)).toEqual(completeTree); - expect(entriesOf(installedParent)).toEqual(["many-files"]); - }, -); + } + + // This is what the reporter did next: remove the trust and install again. + writeFileSync(join(root, "package.json"), JSON.stringify(rootPackageJson)); + const second = await install(root); + expect(second.stderr).not.toContain("error"); + expect(second.exitCode).toBe(0); + expect(compareWithExpectedTree(installed)).toEqual(completeTree); + expect(entriesOf(installedParent)).toEqual(["many-files"]); +}); // A workspace that other packages depend on under a second name is linked into // node_modules under both names, and the hoisted linker walks the packages @@ -717,7 +714,7 @@ const keepsItsDirectory = [ ].join("; "); // The script is POSIX sh; on Windows lifecycle scripts run in bun's own shell. -test.concurrent.skipIf(isWindows)( +test.concurrent( "hoisted linker: the second install of a doubly aliased package leaves a running lifecycle script its directory", async () => { const depTarball = await new Bun.Archive( @@ -792,29 +789,26 @@ for (const linker of ["hoisted", "isolated"]) { // It replaces a link at node_modules/@scope by a directory now, and the isolated // linker moves a node_modules it did not make aside first. for (const linker of ["hoisted", "isolated"]) { - test.concurrent.skipIf(isWindows)( - `${linker} linker: a dangling node_modules/@scope symlink does not hang the install`, - async () => { - const depTarball = await new Bun.Archive( - { "package/package.json": JSON.stringify({ name: "@scope/dep", version: "1.0.0" }) }, - { compress: "gzip" }, - ).bytes(); - using dir = tempDir(`staging-dangling-scope-${linker}`, { - "package.json": JSON.stringify({ name: "app", dependencies: { "@scope/dep": "file:./dep-1.0.0.tgz" } }), - "dep-1.0.0.tgz": Buffer.from(depTarball), - "node_modules": {}, - "bunfig.toml": ({ root }) => Bun.TOML.stringify({ install: { cache: join(root, ".bun-cache"), linker } }), - }); - const root = String(dir); - symlinkSync(join(root, "does-not-exist"), join(root, "node_modules", "@scope")); + test.concurrent(`${linker} linker: a dangling node_modules/@scope symlink does not hang the install`, async () => { + const depTarball = await new Bun.Archive( + { "package/package.json": JSON.stringify({ name: "@scope/dep", version: "1.0.0" }) }, + { compress: "gzip" }, + ).bytes(); + using dir = tempDir(`staging-dangling-scope-${linker}`, { + "package.json": JSON.stringify({ name: "app", dependencies: { "@scope/dep": "file:./dep-1.0.0.tgz" } }), + "dep-1.0.0.tgz": Buffer.from(depTarball), + "node_modules": {}, + "bunfig.toml": ({ root }) => Bun.TOML.stringify({ install: { cache: join(root, ".bun-cache"), linker } }), + }); + const root = String(dir); + symlinkSync(join(root, "does-not-exist"), join(root, "node_modules", "@scope")); - const { stderr, exitCode } = await install(root); - expect(stderr).not.toContain("error"); - expect(readdirSync(join(root, "node_modules", "@scope"))).toEqual(["dep"]); - expect(lstatSync(join(root, "node_modules", "@scope")).isDirectory()).toBe(true); - expect(exitCode).toBe(0); - }, - ); + const { stderr, exitCode } = await install(root); + expect(stderr).not.toContain("error"); + expect(readdirSync(join(root, "node_modules", "@scope"))).toEqual(["dep"]); + expect(lstatSync(join(root, "node_modules", "@scope")).isDirectory()).toBe(true); + expect(exitCode).toBe(0); + }); } // When linking fails midway, bun sees the error itself instead of being killed, @@ -880,7 +874,7 @@ function createTreeDeeperThanPathMax(parent: string, scratch: string) { } // On Windows no tree that exists is longer than the buffers its paths are built in. -test.concurrent.skipIf(isWindows)( +test.concurrent( "hoisted linker: a link that fails with ENAMETOOLONG leaves nothing at the package's path", async () => { const tgz = await new Bun.Archive( @@ -1011,7 +1005,7 @@ describe("hoisted linker: packages linked by the thread pool", () => { } // On Windows no tree that exists is longer than the buffers its paths are built in. - test.concurrent.skipIf(isWindows)("packages that can't be linked only fail themselves, in order", async () => { + test.concurrent("packages that can't be linked only fail themselves, in order", async () => { const names = ["a-first", "b-deep", "c-deep", "d-last"]; using registry = serveRegistry(await packAll(names)); using dir = project("pool-enametoolong", registry, { @@ -1152,7 +1146,7 @@ describe("hoisted linker: packages linked by the thread pool", () => { }); // The pool keeps the node_modules of a batch open. Each `host-N` has one, for its `inner`. - test.concurrent.skipIf(isWindows)("a low limit on open files does not fail the install", async () => { + test.concurrent("a low limit on open files does not fail the install", async () => { const hosts = Array.from({ length: 60 }, (_, i) => `host-${i}`); const files = ["index.js"]; using registry = serveRegistry({ @@ -1348,7 +1342,7 @@ describe("hoisted linker: packages linked by the thread pool", () => { // Each host has a tree with three packages that have a script. One script runs at a time, and each // waits for the bin of the last tree: it is linked when that tree is done, after which no script // joins the ones that wait, and bun sees no script end before it has gone through them. - test.concurrent.skipIf(isWindows)("scripts start in the order in which the main thread starts them", async () => { + test.concurrent("scripts start in the order in which the main thread starts them", async () => { const hosts = Array.from({ length: 7 }, (_, i) => `host-${i}`); const scripted = ["scripted-1", "scripted-2", "scripted-3"]; const nested = [...scripted, "plain-1", "plain-2", "tool"]; diff --git a/test/cli/install/bun-install-streaming-extract.test.ts b/test/cli/install/bun-install-streaming-extract.test.ts index d254ef2f066f..71e59e006f00 100644 --- a/test/cli/install/bun-install-streaming-extract.test.ts +++ b/test/cli/install/bun-install-streaming-extract.test.ts @@ -1456,7 +1456,7 @@ describe.concurrent("sparse tar members", () => { // whose first bytes were those of the refused block, and that package // stayed in the cache for every later install. // ------------------------------------------------------------------- -test.skipIf(isWindows)("streaming extract fails the install when the file system refuses a write", async () => { +test("streaming extract fails the install when the file system refuses a write", async () => { // 6,000,000 bytes where no file can grow past 4 MiB. Random bytes keep the // tarball above the size from which a registry tarball is streamed. const big = randomBytes(6_000_000); diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 011399d13c7a..68361afa1297 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -2146,7 +2146,7 @@ describe.concurrent("bun-install", () => { // buffer (4096 bytes on Linux, 1024 on macOS), so aliases a few bytes short of it used to crash // the install right there. A name over 255 bytes no longer gets that far: it is refused when it is read. // On Windows the buffer is far larger than any path the OS accepts. - describe.concurrent.skipIf(isWindows)("dependency alias that fills the path buffer", () => { + describe.concurrent("dependency alias that fills the path buffer", () => { const alias = Buffer.alloc((isLinux ? 4096 : 1024) - 6, "a").toString(); async function installIntoExistingNodeModules(cwd: string) { @@ -13730,7 +13730,7 @@ it.concurrent("does not install transitive file: dependencies with overlong fold // against the buffer when package.json is parsed, but the appended bytes were not, so a // folder path within 13 bytes of the buffer size aborted the install. Windows' buffer is // larger than any path the OS accepts, so the boundary cannot be reached there. -describe.concurrent.skipIf(isWindows)("file: dependency whose package.json path is around the path buffer size", () => { +describe.concurrent("file: dependency whose package.json path is around the path buffer size", () => { const PATH_BUFFER_BYTES = isLinux ? 4096 : 1024; // A relative path of exactly `bytes` bytes made of one letter directory names, so that a @@ -16065,7 +16065,7 @@ it.concurrent(doesNotCreateCacheEntryOutside, async () => { // collide under the seed-0 std.Wyhash that keys the folder-resolution dedupe // map must each resolve to their own package, not share one identity. // https://github.com/oven-sh/bun/issues/32741 -it.concurrent.skipIf(isWindows)("file: deps with colliding abs-path hashes resolve to distinct packages", async () => { +it.concurrent("file: deps with colliding abs-path hashes resolve to distinct packages", async () => { using dir = tempDir("folder-resolution-collision", { "package.json": JSON.stringify({ name: "victim", version: "0.0.0" }), }); @@ -16119,7 +16119,7 @@ it.concurrent.skipIf(isWindows)("file: deps with colliding abs-path hashes resol }); // The lockfile interns every string over 8 bytes in a pool keyed by Wyhash11(0, bytes). -it.concurrent.skipIf(isWindows)("two specifiers whose hashes collide stay two strings", async () => { +it.concurrent("two specifiers whose hashes collide stay two strings", async () => { const specA = "./t/m3daaaaaaaaaaaaaaaaaaaaaaaaa.7aQs_ePaaaaaaaaw9Aaaaaaaaaaaaaa.tgz"; const specB = "./t/m3daaaaaaaaaaaaaaaaaaaaaaaaa.7aQs_ePbaaaaaaaw9Aaaaaaaaaaaaaa.tgz"; expect(specA).not.toBe(specB); @@ -16650,7 +16650,7 @@ describe.concurrent("progress bar with a long name", () => { // `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux and Android, 1024 on macOS and // the BSDs) before chdir. Values that did not leave room for the NUL terminator used to abort the // process; the buffer on Windows (~96 KiB) is larger than any command line, so only POSIX is affected. -describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", () => { +describe.concurrent("--cwd that does not fit the path buffer", () => { const PATH_MAX = isLinux || isAndroid ? 4096 : 1024; const name = (length: number) => Buffer.alloc(length, "a").toString(); diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 0f98bb499114..8f7137776bd2 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -848,13 +848,13 @@ it("a link: dependency whose link is in place is not linked again", async () => expect(exitCode).toBe(0); // A hard link of the symlink keeps its inode number from being used for a new one. On macOS // link() follows the symlink, and APFS gives no number out twice. - if (!isWindows && !isMacOS) await fsLink(link, join(String(dir), "pin")); + if (!isMacOS) await fsLink(link, join(String(dir), "pin")); const before = (await lstat(link)).ino; ({ stdout, exitCode } = await bun(app, "install", "--linker=hoisted")); expect(stdout).toContain("(no changes)"); expect(exitCode).toBe(0); - if (!isWindows) expect((await lstat(link)).ino).toBe(before); + expect((await lstat(link)).ino).toBe(before); // The same name and version registered from another directory: the link has to move. expect((await bun(join(String(dir), "other"), "link")).exitCode).toBe(0); @@ -1026,7 +1026,7 @@ describe("global bin dir", () => { "elsewhere.txt": "", }); const binDir = join(String(bunInstall), "bin"); - if (!isWindows) await symlink(join(String(bunInstall), "elsewhere.txt"), join(binDir, "foreign")); + await symlink(join(String(bunInstall), "elsewhere.txt"), join(binDir, "foreign")); // Bins are linked in map order: the taken names first, then the free one. using pkg = tempDir( "link-pkg", @@ -1040,7 +1040,7 @@ describe("global bin dir", () => { expect(await readFile(join(binDir, binEntry("bun")), "utf8")).toBe("the bun binary"); if (isWindows) expect(await exists(join(binDir, "bun.bunx"))).toBeFalse(); expect(await readFile(join(binDir, binEntry("adir"), "inner.txt"), "utf8")).toBe("keep"); - if (!isWindows) expect(await readlink(join(binDir, "foreign"))).toBe(join(String(bunInstall), "elsewhere.txt")); + expect(await readlink(join(binDir, "foreign"))).toBe(join(String(bunInstall), "elsewhere.txt")); expect(exitCode).toBe(1); }); @@ -1063,7 +1063,7 @@ describe("global bin dir", () => { // The name is no longer the first package's to remove. result = await run("unlink", String(first), String(bunInstall)); expect(result.out).toContain('success: unlinked package "first-owner"'); - if (!isWindows) expect(bin).toBeValidBin(linkTarget("second-owner", "cli.js")); + expect(bin).toBeValidBin(linkTarget("second-owner", "cli.js")); expect(result.exitCode).toBe(0); }); @@ -1126,7 +1126,7 @@ describe("global bin dir", () => { await writeFile(join(binDir, binEntry("bun")), "the bun binary"); await mkdir(join(binDir, binEntry("adir"))); await writeFile(join(binDir, binEntry("adir"), "inner.txt"), "keep"); - if (!isWindows) await symlink(join(String(bunInstall), "elsewhere.txt"), join(binDir, "foreign")); + await symlink(join(String(bunInstall), "elsewhere.txt"), join(binDir, "foreign")); await writeFile( join(String(pkg), "package.json"), pkgFiles("own-bin", { "own-bin": "cli.js", bun: "cli.js", adir: "cli.js", foreign: "cli.js" })["package.json"], @@ -1139,7 +1139,7 @@ describe("global bin dir", () => { if (isWindows) expect(await exists(join(binDir, "own-bin.bunx"))).toBeFalse(); expect(await readFile(join(binDir, binEntry("bun")), "utf8")).toBe("the bun binary"); expect(await readFile(join(binDir, binEntry("adir"), "inner.txt"), "utf8")).toBe("keep"); - if (!isWindows) expect(await readlink(join(binDir, "foreign"))).toBe(join(String(bunInstall), "elsewhere.txt")); + expect(await readlink(join(binDir, "foreign"))).toBe(join(String(bunInstall), "elsewhere.txt")); expect(result.exitCode).toBe(0); }); }); diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 2e25eabe41c1..9ddfa47ba62c 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -76,7 +76,7 @@ it("should write plaintext lockfiles", async () => { }); // won't work on windows, " is not a valid character in a filename -it.skipIf(isWindows)("should escape names", async () => { +it("should escape names", async () => { const { packageDir, packageJson } = await registry.createTestDir(); await Promise.all([ write( @@ -1401,7 +1401,7 @@ describe.concurrent.each([ ["022", "644"], ["077", "600"], ])("umask %s", (umask, expectedMode) => { - it.skipIf(isWindows)("bun.lock and yarn.lock honor umask", async () => { + it("bun.lock and yarn.lock honor umask", async () => { const { packageDir } = await registry.createTestDir({ files: { "package.json": JSON.stringify({ name: "umask", workspaces: ["pkg1"], dependencies: { pkg1: "workspace:*" } }), diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index 89b7209b38f3..d4a582a87d8a 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -92,7 +92,7 @@ it("should not print anything to stderr when running bun.lockb", async () => { ).toEqual([hashed, hashed]); }); -it.skipIf(isWindows)("bun.lockb honors umask", async () => { +it("bun.lockb honors umask", async () => { const { packageDir } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false }, files: { diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 81778503351c..500fb849efdb 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -127,7 +127,7 @@ test.concurrent("the archive is not padded to a full tar record", async () => { // pack only reads package.json, so a file that is readable but not writable (a read-only // checkout, a file owned by another user) must not stop it. -test.concurrent.skipIf(isWindows)("read-only package.json", async () => { +test.concurrent("read-only package.json", async () => { using dir = tempDir("pack-read-only", { "package.json": JSON.stringify({ name: "pack-read-only", version: "1.2.3" }), "index.js": "console.log('hello ./index.js')", @@ -559,7 +559,7 @@ describe.concurrent("flags", () => { // The destination directory ends `headroom` bytes short of the path buffer, so it still fits, // but the longer "/-.tgz" appended to it does not. Windows command lines cannot // carry an argument anywhere near its path buffer size. - test.skipIf(isWindows)("--destination with no room left for the tarball name", async () => { + test("--destination with no room left for the tarball name", async () => { const pathMax = isLinux ? 4096 : 1024; const headroom = 100; const name = `pack-dest-too-long-${Buffer.alloc(2 * headroom, "n").toString()}`; @@ -624,7 +624,7 @@ describe.concurrent("flags", () => { // The destination used to be opened a second time to hash it, which on a FIFO waits for another writer forever. // This end is open before pack starts and the tarball fits the pipe's buffer, so nothing here can block; // a pack that does is killed by `timeout`. - test.skipIf(isWindows)("--filename can be a FIFO", async () => { + test("--filename can be a FIFO", async () => { using dir = tempDir("pack-fifo", { "package.json": JSON.stringify({ name: "pack-fifo", version: "1.1.1" }), "index.js": indexJs, @@ -690,7 +690,7 @@ describe.concurrent("flags", () => { }); // On Windows the path buffer is larger than any command line, so the overflow cannot be reached there. - describe.skipIf(isWindows)("--destination longer than the path buffer", () => { + describe("--destination longer than the path buffer", () => { const longName = Buffer.alloc(MAX_PATH_BYTES + 1, "d").toString(); const packageJson = JSON.stringify({ name: "pack-long-dest", version: "1.0.0" }); const expectedError = `error: archive destination name too long: "${longName}"\n`; @@ -1031,7 +1031,7 @@ describe.concurrent("flags", () => { }); // PATH_MAX is 4096 on Linux and 1024 on macOS. Windows' path buffer is larger than any command line. - describe.skipIf(isWindows)("--filename longer than PATH_MAX", () => { + describe("--filename longer than PATH_MAX", () => { const longName = Buffer.alloc(5000, "f").toString(); const packageJson = JSON.stringify({ name: "pack-cwd-long-filename", version: "1.0.0" }); @@ -1061,7 +1061,7 @@ describe.concurrent("flags", () => { // ./*.tgz` to pick up. `ulimit -f 0` (RLIMIT_FSIZE) makes every write to the tarball fail with // EFBIG, which unlike a permission based setup also works when the tests run as root; setting it // needs a POSIX shell. -describe.skipIf(isWindows)("a failed pack leaves no tarball behind", () => { +describe("a failed pack leaves no tarball behind", () => { const packageJson = JSON.stringify({ name: "pack-failed", version: "1.0.0" }); async function packExpectingFailure(cwd: string, { fileSizeLimit }: { fileSizeLimit: boolean }, ...args: string[]) { @@ -1243,7 +1243,7 @@ describe.concurrent("something already exists at the tarball's destination", () }); // Serial: `unprivilegedSpawnOptions` may change and restore the modes of directories above the test's own. - test.serial.skipIf(isWindows)("a writable file in a read-only folder", async () => { + test.serial("a writable file in a read-only folder", async () => { using dir = tempDir("pack-over-locked", { ...files, "pkg/out/pack-over-1.0.0.tgz": "an earlier tarball" }); using unprivileged = unprivilegedSpawnOptions(String(dir)); await chmod(join(dir, "pkg", "out"), 0o555); @@ -1266,7 +1266,7 @@ describe.concurrent("something already exists at the tarball's destination", () } }); - test.skipIf(isWindows)("a file keeps its mode", async () => { + test("a file keeps its mode", async () => { using dir = tempDir("pack-over-mode", { ...files, "pkg/pack-over-1.0.0.tgz": "an earlier tarball" }); const tarball = join(dir, "pkg", "pack-over-1.0.0.tgz"); await chmod(tarball, 0o600); @@ -2984,7 +2984,7 @@ describe.concurrent("bundledDependencies", () => { // The isolated linker's layout: node_modules has links into a store, where a package's dependencies are links next to // it. `y` goes next to `x`, unless the packed package has its own `y` that it doesn't bundle. - test.skipIf(isWindows).each([ + test.each([ { name: "x", own: {}, y: "node_modules/y" }, { name: "@s/x", own: {}, y: "node_modules/y" }, { name: "x", own: { y: "2.0.0" }, y: "node_modules/x/node_modules/y" }, @@ -3028,7 +3028,7 @@ describe.concurrent("bundledDependencies", () => { }); // Like npm: a link to a folder that is no installed copy is packed by that folder's own rules. - test.skipIf(isWindows).each([ + test.each([ ["", "project"], [", also below a folder named node_modules", "node_modules/project"], ])("a bundled workspace is packed with its own files and .npmignore%s", async (_, project) => { @@ -3416,7 +3416,7 @@ describe.concurrent("bundledDependencies", () => { }); // The layout of pnpm and of the isolated linker: each package is next to links to its dependencies. - describe.skipIf(isWindows)("two versions of two packages that depend on each other", () => { + describe("two versions of two packages that depend on each other", () => { async function packLinked(graph: Record>) { const tree: Record = { "package.json": JSON.stringify({ @@ -3501,7 +3501,7 @@ describe.concurrent("bundledDependencies", () => { ]); }); - test.skipIf(isWindows)("bundled dependencies that link to each other end the lookup", async () => { + test("bundled dependencies that link to each other end the lookup", async () => { using dir = tempDir("pack-bundled-dep-cycle", { "package.json": JSON.stringify({ name: "pack-bundled-dep-cycle", @@ -3537,7 +3537,7 @@ describe.concurrent("bundledDependencies", () => { }); // This is what `bun install` creates for a workspace that's a dependency. - test.skipIf(isWindows)("a scoped bundled dependency that is a link is followed", async () => { + test("a scoped bundled dependency that is a link is followed", async () => { using dir = tempDir("pack-bundled-dep-scoped-link", { "package.json": JSON.stringify({ name: "pack-bundled-dep-scoped-link", @@ -3561,7 +3561,7 @@ describe.concurrent("bundledDependencies", () => { }); // The lookup used to keep every directory it found open. - test.skipIf(isWindows)("more dependencies than file descriptors", async () => { + test("more dependencies than file descriptors", async () => { const names = Array.from({ length: 299 }, (_, i) => `d${i}`); using dir = tempDir("pack-bundled-dep-emfile", { "package.json": JSON.stringify({ @@ -5196,7 +5196,7 @@ describe.concurrent("bins", () => { // `chmod 000` keeps the user that packs out of one entry that the scan looks for. The entry is // there, so the scan must not continue as if it was not. -describe.concurrent.skipIf(isWindows)("an entry that exists but can't be opened", () => { +describe.concurrent("an entry that exists but can't be opened", () => { // Runs `bun ...cmd` in `cwd` while `locked` has no permission bits. Restores them so that the tree can be removed. async function runWithout(dir: string, locked: string, cmd: string[], cwd: string = dir) { using unprivileged = unprivilegedSpawnOptions(dir); @@ -5925,7 +5925,7 @@ describe.concurrent.each([ ])("%s is printed spelled out", (_, raw, spelled) => { const unprintable = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/; - test.skipIf(isWindows)("in a file name", async () => { + test("in a file name", async () => { using dir = tempDir("pack-file-name", { "package.json": JSON.stringify({ name: "pack-file-name", version: "1.0.0" }), [`a${raw}b.js`]: "x", diff --git a/test/cli/install/bun-patch.test.ts b/test/cli/install/bun-patch.test.ts index 3602b698aecb..6b15f86c5f63 100644 --- a/test/cli/install/bun-patch.test.ts +++ b/test/cli/install/bun-patch.test.ts @@ -1070,10 +1070,9 @@ describe.concurrent.each([ const repo = join(String(dir), "gitrepo"); // The checkout in the cache has the symlinks, and the copies in node_modules don't. That used to end up in the // patch as `deleted file mode 120000`, which no install can apply. - if (!isWindows) { - symlinkSync("index.js", join(repo, "main.js")); - symlinkSync("v2", join(repo, "docs", "latest")); - } + + symlinkSync("index.js", join(repo, "main.js")); + symlinkSync("v2", join(repo, "docs", "latest")); // keep git away from the machine's global/system config (autocrlf, gpgsign) const gitEnv = { ...bunEnv, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: join(String(dir), "no-gitconfig") }; @@ -1153,7 +1152,7 @@ const bundledAndNested = { dependencies: { "bundled-transitive": "1.0.0", "one-d describe.concurrent("a step that fails leaves node_modules as it was", () => { // RLIMIT_FSIZE makes the copy of a file larger than the limit fail with EFBIG. pkg-to-patch needs // no-deps@1.0.0 and the root no-deps@2.0.0, so the hoisted linker nests the former under it. - test.skipIf(isWindows).each([ + test.each([ ["hoisted", false], ["isolated", false], ["isolated", true], @@ -1265,7 +1264,7 @@ describe.concurrent("a step that fails leaves node_modules as it was", () => { // The git that fails is a shell script. One that cannot read a folder exits 1, as it does for // differences, and prints no patch. One that is killed prints nothing at all. - test.skipIf(isWindows).each([ + test.each([ ["fails", "echo 'fatal: not today' >&2\nexit 128\n", "fatal: not today"], [ "cannot read a folder", @@ -1812,7 +1811,7 @@ describe.concurrent("--dry-run prints what it would do and doesn't change the pr // entry that `bun patch` copies and that `bun patch --commit` compares with still has the mode from the tarball. So the // commands stopped working, and a manual chmod ended up in the patch. describe.concurrent("the commands of a package that is being patched", () => { - test.skipIf(isWindows).each(["hoisted", "isolated"] as const)( + test.each(["hoisted", "isolated"] as const)( "still run, and their mode isn't included in the patch (%s linker, copyfile backend)", async linker => { const name = "dep-with-directory-bins"; @@ -1893,7 +1892,7 @@ describe.concurrent("a package with install scripts", () => { // It used to be joined into a buffer of PATH_MAX bytes, which is a panic. A Windows path can be longer. describe.concurrent("an argument that is longer than any path", () => { - test.skipIf(isWindows).each([ + test.each([ ["bun patch ", ".", ["node_modules/LONG"]], ["bun patch ", ".", ["/LONG/node_modules/no-deps"]], ["bun patch --commit ", ".", ["--commit", "node_modules/LONG"]], @@ -2650,7 +2649,7 @@ describe.concurrent("isolated linker: a package that the project links", () => { // A patch that adds a symlink does not apply (`bad_file_mode`). Windows is skipped: git records // a symlink as mode 120000 only with `core.symlinks`, which is off there by default. - test.skipIf(isWindows)("the edits are kept when the patch doesn't apply", async () => { + test("the edits are kept when the patch doesn't apply", async () => { await using project = await installedProject({ dependencies }, { linker: "isolated" }); const { dir, env } = project; await expectPrepared(dir, env, "has-bin-entries", "has-bin-entries", folder); @@ -3222,7 +3221,7 @@ describe.concurrent("isolated linker with the global store", () => { ); // A patch that adds a symlink does not apply. Windows is skipped as in the test without the global store. - test.skipIf(isWindows)("the copy is kept when the patch doesn't apply", async () => { + test("the copy is kept when the patch doesn't apply", async () => { await using project = await twoVersions(); const { dir, env } = project; const folder = join(dir, "node_modules", "two-range-deps"); @@ -3549,7 +3548,7 @@ describe.concurrent("the patch that bun patch --commit writes", () => { // A `color.ui = always` or a `diff.noprefix` in /etc/gitconfig used to end up in the patch, because the variables // that turn the config files off were passed as bare names, without `=value`. The git that prints its environment is // a shell script. - test.skipIf(isWindows)("git runs without the config of the machine and of the user", async () => { + test("git runs without the config of the machine and of the user", async () => { await using project = await createProject("hoisted"); const { dir, env } = project; await bunOk(dir, env, "patch", "basic-1"); @@ -3570,7 +3569,7 @@ describe.concurrent("the patch that bun patch --commit writes", () => { // With more than 1000 files deleted and 1000 added, git stops looking for renames. It prints the whole patch and a // warning on stderr. The git that prints the warning is a shell script. - test.skipIf(isWindows)("a warning from git does not fail the commit", async () => { + test("a warning from git does not fail the commit", async () => { await using project = await createProject("hoisted"); const { dir, env } = project; using stub = tempDir("bun-patch-stub-git", { diff --git a/test/cli/install/bun-pm-cache-pack.test.ts b/test/cli/install/bun-pm-cache-pack.test.ts index 9e9990f83ed0..4cb606581488 100644 --- a/test/cli/install/bun-pm-cache-pack.test.ts +++ b/test/cli/install/bun-pm-cache-pack.test.ts @@ -652,40 +652,37 @@ describe("bun pm cache unpack", () => { await expectAllRefused(box, packs); }); - test.concurrent.skipIf(isWindows)( - "keeps every symlink inside its package, whatever the file system folds", - async () => { - // Refused where `A/UP` is `a/up`, harmless where it is not. - const packs = { - ascii: [dir("a"), link("a/up", ".."), link("A/UP/A/UP/out", "../../../..")], - unicode: [dir("ä"), link("ä/up", ".."), link("Ä/UP/Ä/UP/out", "../../../..")], - target: [dir("ä"), link("ä/up", ".."), link("x", "Ä/up/Ä/up/victim")], - }; - for (const [name, records] of Object.entries(packs)) { - using box = sandbox(); - writeFileSync(box.at("fold.pack"), packOf(pkg(evil, [packageJson, ...records]))); - const { err, code } = await bun(box, ["pm", "cache", "unpack", box.at("fold.pack")]); - if (code === 0) { - const root = realpathSync(join(box.cache, evil)); - for (const line of snapshot(root, false)) { - const [path, target] = line.split(" -> "); - if (target === undefined || !existsSync(join(root, path))) continue; - expect({ name, path, inside: (realpathSync(join(root, path)) + sep).startsWith(root + sep) }).toEqual({ - name, - path, - inside: true, - }); - } - } else { - expect(err).toContain("symlink collides"); - expect(readdirSync(box.cache)).toEqual([]); + test.concurrent("keeps every symlink inside its package, whatever the file system folds", async () => { + // Refused where `A/UP` is `a/up`, harmless where it is not. + const packs = { + ascii: [dir("a"), link("a/up", ".."), link("A/UP/A/UP/out", "../../../..")], + unicode: [dir("ä"), link("ä/up", ".."), link("Ä/UP/Ä/UP/out", "../../../..")], + target: [dir("ä"), link("ä/up", ".."), link("x", "Ä/up/Ä/up/victim")], + }; + for (const [name, records] of Object.entries(packs)) { + using box = sandbox(); + writeFileSync(box.at("fold.pack"), packOf(pkg(evil, [packageJson, ...records]))); + const { err, code } = await bun(box, ["pm", "cache", "unpack", box.at("fold.pack")]); + if (code === 0) { + const root = realpathSync(join(box.cache, evil)); + for (const line of snapshot(root, false)) { + const [path, target] = line.split(" -> "); + if (target === undefined || !existsSync(join(root, path))) continue; + expect({ name, path, inside: (realpathSync(join(root, path)) + sep).startsWith(root + sep) }).toEqual({ + name, + path, + inside: true, + }); } - expect(readdirSync(box.tmp)).toEqual([]); + } else { + expect(err).toContain("symlink collides"); + expect(readdirSync(box.cache)).toEqual([]); } - }, - ); + expect(readdirSync(box.tmp)).toEqual([]); + } + }); - test.concurrent.skipIf(isWindows)( + test.concurrent( "restores symlinks that stay inside, modes without setuid, and names only Windows minds", async () => { using box = sandbox(); @@ -854,7 +851,7 @@ describe("bun pm cache pack", () => { using box = sandbox(); await installedProject(box); const online = snapshot(join(box.project, "node_modules", "no-deps"), false); - if (!isWindows) { + { const entry = join( box.cache, entryNames(box.cache).find(name => name.startsWith("no-deps@"))!, @@ -1037,44 +1034,41 @@ describe("bun pm cache pack", () => { }, ); - test.concurrent.skipIf(isWindows)( - "refuses an entry that unpack would refuse, and leaves no file behind", - async () => { - using box = sandbox(); - await installedProject(box); - const entry = join( - box.cache, - entryNames(box.cache).find(name => name.startsWith("no-deps@"))!, - ); - writeFileSync(join(box.project, "cache.pack"), "the previous pack"); - - for (const [path, target] of [ - ["out", "../../victim"], - ["absolute", box.victim], - ["back", "lib/../../../victim"], - ]) { - symlinkSync(target, join(entry, path)); - const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); - expect(r.err).toContain("error: bun pm cache pack failed: cache entry has symlink that may point outside it: "); - expect(r.err).toContain(`/${path}"`); - expect(r.out).toBe(""); - expect(r.code).toBe(1); - rmSync(join(entry, path)); - } - - writeFileSync(join(entry, "back\\slash"), ""); + test.concurrent("refuses an entry that unpack would refuse, and leaves no file behind", async () => { + using box = sandbox(); + await installedProject(box); + const entry = join( + box.cache, + entryNames(box.cache).find(name => name.startsWith("no-deps@"))!, + ); + writeFileSync(join(box.project, "cache.pack"), "the previous pack"); + + for (const [path, target] of [ + ["out", "../../victim"], + ["absolute", box.victim], + ["back", "lib/../../../victim"], + ]) { + symlinkSync(target, join(entry, path)); const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); - expect(r.err).toContain("cache entry path cannot be packed"); + expect(r.err).toContain("error: bun pm cache pack failed: cache entry has symlink that may point outside it: "); + expect(r.err).toContain(`/${path}"`); + expect(r.out).toBe(""); expect(r.code).toBe(1); + rmSync(join(entry, path)); + } - expect(readFileSync(join(box.project, "cache.pack"), "utf8")).toBe("the previous pack"); - expect( - readdirSync(box.project) - .filter(name => name.includes("pack")) - .sort(), - ).toEqual(["cache.pack", "package.json"]); - }, - ); + writeFileSync(join(entry, "back\\slash"), ""); + const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); + expect(r.err).toContain("cache entry path cannot be packed"); + expect(r.code).toBe(1); + + expect(readFileSync(join(box.project, "cache.pack"), "utf8")).toBe("the previous pack"); + expect( + readdirSync(box.project) + .filter(name => name.includes("pack")) + .sort(), + ).toEqual(["cache.pack", "package.json"]); + }); test.concurrent( "replaces the previous pack, makes the directory, and takes relative to where it was typed", diff --git a/test/cli/install/bun-pm-diff.test.ts b/test/cli/install/bun-pm-diff.test.ts index fcf62f324209..21f97597fc91 100644 --- a/test/cli/install/bun-pm-diff.test.ts +++ b/test/cli/install/bun-pm-diff.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isDebug, isRoot, isWindows, normalizeBunSnapshot, tempDir, tls } from "harness"; +import { bunEnv, bunExe, isDebug, isRoot, normalizeBunSnapshot, tempDir, tls } from "harness"; import { chmodSync, existsSync, readFileSync, readdirSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -1105,7 +1105,7 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { }); // ConPTY rewrites the process's output. - test.skipIf(isWindows)("NO_COLOR on a terminal: the plain patch, with control characters drawn", async () => { + test("NO_COLOR on a terminal: the plain patch, with control characters drawn", async () => { const added = `exports.b = 2; //${ESC}[2K${ESC}[1A ${CSI_C1}31m ${RLO}\x7f\b
`; using dir = tempDir("pm-diff-plain-tty", { "a/index.js": "exports.a = 1;\n", @@ -1488,47 +1488,44 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { expect(exitCode).toBe(0); }); - test.skipIf(process.platform === "win32")( - "mode changes: newly executable files are shown and called out", - async () => { - using dir = tempDir("pm-diff-mode", { - "a/run.sh": "#!/bin/sh\necho hi\n", - "b/run.sh": "#!/bin/sh\necho hi\n", - "b/install.js": "console.log(1)\n", + test("mode changes: newly executable files are shown and called out", async () => { + using dir = tempDir("pm-diff-mode", { + "a/run.sh": "#!/bin/sh\necho hi\n", + "b/run.sh": "#!/bin/sh\necho hi\n", + "b/install.js": "console.log(1)\n", + }); + chmodSync(join(String(dir), "a/run.sh"), 0o644); + chmodSync(join(String(dir), "b/run.sh"), 0o755); + chmodSync(join(String(dir), "b/install.js"), 0o755); + const run = async (env: Record) => { + await using p = Bun.spawn({ + cmd: [bunExe(), "pm", "diff", "./a", "./b"], + cwd: String(dir), + env: { ...bunEnv, ...env }, + stdout: "pipe", + stderr: "pipe", }); - chmodSync(join(String(dir), "a/run.sh"), 0o644); - chmodSync(join(String(dir), "b/run.sh"), 0o755); - chmodSync(join(String(dir), "b/install.js"), 0o755); - const run = async (env: Record) => { - await using p = Bun.spawn({ - cmd: [bunExe(), "pm", "diff", "./a", "./b"], - cwd: String(dir), - env: { ...bunEnv, ...env }, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, , exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); - return { stdout, exitCode }; - }; - const tty = await run({ NO_COLOR: undefined, FORCE_COLOR: "1", COLUMNS: "120" }); - const text = tty.stdout.replace(/\x1b\[[0-9;]*[mK]/g, ""); - expect(text).toMatch(/\nrun\.sh ─+ mode 644 → 755\n/); - expect(text).toMatch(/\ninstall\.js ─+ executable 755 new \+1\n/); - expect(text).toContain("▲ now executable: run.sh (644 → 755)\n"); - expect(text).toContain("▲ new executable file install.js (755)\n"); - expect(tty.exitCode).toBe(0); - // Patch mode spells it the way git does. - const plain = await run({ NO_COLOR: "1" }); - expect(plain.stdout).toContain("diff --git a/run.sh b/run.sh\nold mode 100644\nnew mode 100755\n"); - expect(plain.stdout).toContain("diff --git a/install.js b/install.js\nnew file mode 100755\n"); - expect(plain.exitCode).toBe(0); - }, - ); + const [stdout, , exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); + return { stdout, exitCode }; + }; + const tty = await run({ NO_COLOR: undefined, FORCE_COLOR: "1", COLUMNS: "120" }); + const text = tty.stdout.replace(/\x1b\[[0-9;]*[mK]/g, ""); + expect(text).toMatch(/\nrun\.sh ─+ mode 644 → 755\n/); + expect(text).toMatch(/\ninstall\.js ─+ executable 755 new \+1\n/); + expect(text).toContain("▲ now executable: run.sh (644 → 755)\n"); + expect(text).toContain("▲ new executable file install.js (755)\n"); + expect(tty.exitCode).toBe(0); + // Patch mode spells it the way git does. + const plain = await run({ NO_COLOR: "1" }); + expect(plain.stdout).toContain("diff --git a/run.sh b/run.sh\nold mode 100644\nnew mode 100755\n"); + expect(plain.stdout).toContain("diff --git a/install.js b/install.js\nnew file mode 100755\n"); + expect(plain.exitCode).toBe(0); + }); test("the piped patch applies with git: empty files, permission changes, file names that need quotes", async () => { const names = ["plain.txt", "with space.txt", "dir with space/x.txt", "é.txt"]; // This isn't valid in a Windows file name. - if (!isWindows) names.push("t\tab.txt", 'q"uote.txt', "back\\slash.txt", "new\nline.txt"); + names.push("t\tab.txt", 'q"uote.txt', "back\\slash.txt", "new\nline.txt"); const a: Record = { "grow.txt": "", "shrink.txt": "one\n", @@ -1611,45 +1608,42 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { ); }); - test.skipIf(process.platform === "win32")( - "folders: symlinked files are read through, symlinked folders skipped, unreadable files are an error", - async () => { - using dir = tempDir("pm-diff-links", { - "a/index.js": "module.exports = 1;\n", - "b/real/index.js": "module.exports = 2;\n", - "c/index.js": "module.exports = 3;\n", - "c/secret.js": "nope\n", + test("folders: symlinked files are read through, symlinked folders skipped, unreadable files are an error", async () => { + using dir = tempDir("pm-diff-links", { + "a/index.js": "module.exports = 1;\n", + "b/real/index.js": "module.exports = 2;\n", + "c/index.js": "module.exports = 3;\n", + "c/secret.js": "nope\n", + }); + symlinkSync("real/index.js", join(String(dir), "b/index.js")); + symlinkSync("..", join(String(dir), "b/loop")); + symlinkSync("does-not-exist.js", join(String(dir), "b/dangling.js")); + const run = async (args: string[]) => { + await using p = Bun.spawn({ + cmd: [bunExe(), "pm", "diff", ...args, "--name-only"], + cwd: String(dir), + env: { ...bunEnv, NO_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", }); - symlinkSync("real/index.js", join(String(dir), "b/index.js")); - symlinkSync("..", join(String(dir), "b/loop")); - symlinkSync("does-not-exist.js", join(String(dir), "b/dangling.js")); - const run = async (args: string[]) => { - await using p = Bun.spawn({ - cmd: [bunExe(), "pm", "diff", ...args, "--name-only"], - cwd: String(dir), - env: { ...bunEnv, NO_COLOR: "1" }, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); - return { stdout, stderr, exitCode }; - }; - const linked = await run(["./a", "./b"]); - expect(linked.stdout.split("\n").filter(l => /^[AMD] /.test(l))).toEqual(["M index.js", "A real/index.js"]); - expect(linked.exitCode).toBe(0); - chmodSync(join(String(dir), "c/secret.js"), 0o000); - try { - const denied = await run(["./a", "./c"]); - // root reads anything; everyone else must get a loud failure, not a diff with a hole in it. - if (!isRoot) { - expect(denied.stderr).toContain("secret.js"); - expect(denied.exitCode).toBe(1); - } - } finally { - chmodSync(join(String(dir), "c/secret.js"), 0o644); + const [stdout, stderr, exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); + return { stdout, stderr, exitCode }; + }; + const linked = await run(["./a", "./b"]); + expect(linked.stdout.split("\n").filter(l => /^[AMD] /.test(l))).toEqual(["M index.js", "A real/index.js"]); + expect(linked.exitCode).toBe(0); + chmodSync(join(String(dir), "c/secret.js"), 0o000); + try { + const denied = await run(["./a", "./c"]); + // root reads anything; everyone else must get a loud failure, not a diff with a hole in it. + if (!isRoot) { + expect(denied.stderr).toContain("secret.js"); + expect(denied.exitCode).toBe(1); } - }, - ); + } finally { + chmodSync(join(String(dir), "c/secret.js"), 0o644); + } + }); test("tarball shapes: .tar, a root folder not called package/, ~/ paths, and `./x.tar 1.0.1`", async () => { using dir = tempDir("pm-diff-tars", {}); diff --git a/test/cli/install/bun-pm-pkg.test.ts b/test/cli/install/bun-pm-pkg.test.ts index 19cd13a938f4..41a96cf7116d 100644 --- a/test/cli/install/bun-pm-pkg.test.ts +++ b/test/cli/install/bun-pm-pkg.test.ts @@ -1,7 +1,7 @@ import { spawn } from "bun"; import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isWindows, MAX_PATH_BYTES, tempDir } from "harness"; +import { bunEnv, bunExe, MAX_PATH_BYTES, tempDir } from "harness"; import { join } from "path"; async function runPmPkg(args: string[], cwd: string, expectSuccess = true) { @@ -271,7 +271,7 @@ describe.concurrent("bun pm pkg", () => { }); // argv holds any bytes on POSIX. `printf` builds the key `e`, 0xC3, `z`, which is not UTF-8. - it.skipIf(isWindows)("should print U+FFFD for a non-UTF-8 key when getting multiple properties", async () => { + it("should print U+FFFD for a non-UTF-8 key when getting multiple properties", async () => { using dir = tempDir("pm-pkg-get-bytes", { "package.json": Buffer.concat([Buffer.from('{"e'), Buffer.from([0xc3]), Buffer.from('z":1,"s":"x}"}')]), }); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index 20dd76928c8c..d90feac5b113 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -1900,7 +1900,7 @@ describe.concurrent("bun pm cache rm", () => { }); // Serial: `unprivilegedSpawnOptions` may change and restore the modes of directories above the test's own. - test.serial.skipIf(isWindows)("a cache directory in a read-only parent is emptied", async () => { + test.serial("a cache directory in a read-only parent is emptied", async () => { using dir = tempDir("pm-cache-rm-read-only-parent", { ...sideFiles, ...projectFiles, @@ -2399,7 +2399,7 @@ it("bun pm cache prune removes packages that bun install cached and doesn't touc // does not fit is unusable in the same way as a directory the OS refuses to create, and bun falls // back to node_modules/.cache of the project like it does for those. On Windows the buffer holds // more than an environment variable or argument can carry. -describe.concurrent.skipIf(isWindows)("cache directory setting longer than the path buffer", () => { +describe.concurrent("cache directory setting longer than the path buffer", () => { // Longer than the buffer on every POSIX platform (the Linux one is the largest). const tooLong = "/" + Buffer.alloc(2 * 4096, "a").toString(); @@ -2694,7 +2694,7 @@ test.concurrent.each([ // The file: target becomes the resolution, so a dependent can put control characters in it (an // alias containing them is rejected before anything is installed). Windows does not allow them in // file names, so the package cannot be installed there in the first place. -test.skipIf(isWindows)("bun pm untrusted escapes control characters in the package path and resolution", async () => { +test("bun pm untrusted escapes control characters in the package path and resolution", async () => { using dir = tempDir("pm-untrusted-control-chars-path", { "package.json": JSON.stringify({ name: "foo", @@ -2729,7 +2729,7 @@ test.skipIf(isWindows)("bun pm untrusted escapes control characters in the packa // $XDG_CACHE_HOME or $HOME) and the global bin directory (`$BUN_INSTALL/bin`, otherwise `.bun/bin` // under the same two) are each joined in a path buffer of MAX_PATH_BYTES: 4096 bytes on Linux, 1024 // on macOS. On Windows the buffer holds more than an environment variable can, so this is POSIX only. -describe.concurrent.skipIf(isWindows)("global directories longer than the path buffer", () => { +describe.concurrent("global directories longer than the path buffer", () => { const PATH_BUFFER_BYTES = isLinux ? 4096 : 1024; const LONG_VALUE = "/" + Buffer.alloc(8192, "a").toString(); @@ -2890,7 +2890,7 @@ describe.concurrent("the global bin directory and $PATH", () => { // A package.json that is readable but not writable (a read-only checkout, a file owned by // another user) must only stop the commands that actually rewrite it. -describe.skipIf(isWindows)("read-only package.json", () => { +describe("read-only package.json", () => { // Nothing here may reach a registry. const offlineEnv = { ...env, npm_config_registry: "http://127.0.0.1:1/" }; diff --git a/test/cli/install/bun-prune.test.ts b/test/cli/install/bun-prune.test.ts index e92cc053d564..e2356d51ac13 100644 --- a/test/cli/install/bun-prune.test.ts +++ b/test/cli/install/bun-prune.test.ts @@ -1156,7 +1156,7 @@ test.concurrent( }, ); -test.concurrent.skipIf(isWindows)("a symlinked .bin directory is never cleaned through", async () => { +test.concurrent("a symlinked .bin directory is never cleaned through", async () => { const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } }); const nm = join(dir, "node_modules"); expect(existsSync(join(nm, ".bin"))).toBeFalse(); @@ -2274,51 +2274,48 @@ test.concurrent("isolated + publicHoistPattern: hoisted links follow their store expect(await file(join(nm, "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0" }); }); -test.concurrent.skipIf(isWindows || isRoot)( - "a failed deletion is reported, the rest is removed, exit code 1", - async () => { - const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } }); - const nm = join(dir, "node_modules"); - const junkA = plant(dir, "node_modules/junk-a"); - const inner = plant(dir, "node_modules/junk-b/inner"); - const junkB = join(nm, "junk-b"); - chmodSync(junkB, 0o555); - const failure = /^error: failed to remove node_modules\/junk-b: E[A-Z]+ \(.+\)$/; - try { - // The failure is explained before the verdict, and the verdict counts it. - const { lines: merged, exitCode } = await pruneMerged(dir); - expect(merged).toStrictEqual([ - BANNER, - "", - "- junk-a", - expect.stringMatching(failure), - "1 package removed, 1 failed (checked 3 installed packages)", - ]); - expect(exitCode).toBe(1); - expect(existsSync(junkA)).toBeFalse(); - expect(existsSync(inner)).toBeTrue(); - - // --silent still reports what could not be removed; the exit code alone would hide which entry it was. - const silent = await prune(dir, "--silent"); - expect(silent.stdout).toBe(""); - expect(normalizeBunSnapshot(silent.stderr)).toMatch(failure); - expect(silent.exitCode).toBe(1); - expect(existsSync(inner)).toBeTrue(); - } finally { - chmodSync(junkB, 0o755); - } +test.concurrent.skipIf(isRoot)("a failed deletion is reported, the rest is removed, exit code 1", async () => { + const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } }); + const nm = join(dir, "node_modules"); + const junkA = plant(dir, "node_modules/junk-a"); + const inner = plant(dir, "node_modules/junk-b/inner"); + const junkB = join(nm, "junk-b"); + chmodSync(junkB, 0o555); + const failure = /^error: failed to remove node_modules\/junk-b: E[A-Z]+ \(.+\)$/; + try { + // The failure is explained before the verdict, and the verdict counts it. + const { lines: merged, exitCode } = await pruneMerged(dir); + expect(merged).toStrictEqual([ + BANNER, + "", + "- junk-a", + expect.stringMatching(failure), + "1 package removed, 1 failed (checked 3 installed packages)", + ]); + expect(exitCode).toBe(1); + expect(existsSync(junkA)).toBeFalse(); + expect(existsSync(inner)).toBeTrue(); - const { stdout, exitCode } = await prune(dir); - expect(out(stdout)).toMatchInlineSnapshot(` + // --silent still reports what could not be removed; the exit code alone would hide which entry it was. + const silent = await prune(dir, "--silent"); + expect(silent.stdout).toBe(""); + expect(normalizeBunSnapshot(silent.stderr)).toMatch(failure); + expect(silent.exitCode).toBe(1); + expect(existsSync(inner)).toBeTrue(); + } finally { + chmodSync(junkB, 0o755); + } + + const { stdout, exitCode } = await prune(dir); + expect(out(stdout)).toMatchInlineSnapshot(` "bun prune () - junk-b 1 package removed (checked 2 installed packages)" `); - expect(exitCode).toBe(0); - expect(existsSync(junkB)).toBeFalse(); - }, -); + expect(exitCode).toBe(0); + expect(existsSync(junkB)).toBeFalse(); +}); // pnpm#4770 / #5092 / #10275: `prepare` must not run after --production removed the tools it needs. test.concurrent("never runs the project's lifecycle scripts", async () => { @@ -3683,12 +3680,12 @@ test.concurrent.each(linkers)("%s: the bin of a removed package is relinked to t }); const nm = join(dir, "node_modules"); const target = () => readlinkSync(join(nm, ".bin", "what-bin")).replaceAll("\\", "/"); - if (!isWindows) expect(target()).toBe("../a-what-bin/what-bin.js"); + expect(target()).toBe("../a-what-bin/what-bin.js"); const { exitCode } = await prune(dir, "--production", "--linker", linker); expect(existsSync(join(nm, "a-what-bin"))).toBeFalse(); expectBinInstalled(nm, "what-bin"); - if (!isWindows) expect(target()).toBe("../what-bin/what-bin.js"); + expect(target()).toBe("../what-bin/what-bin.js"); expect(exitCode).toBe(0); }); @@ -3822,7 +3819,7 @@ test.concurrent( expectBinInstalled(join(store, entry, "node_modules"), "skip-test-cmd"); } const installed = nodeModulesTree(dir, { storeBins: true }); - if (!isWindows) { + { const ownBin = `node_modules/.bun/test-postinstall-skip@1.0.0/node_modules/.bin/skip-test-cmd`; expect(installed[ownBin]).toBe( `-> node_modules/.bun/${native}/node_modules/test-postinstall-skip-native/bin/cmd.js`, diff --git a/test/cli/install/bun-publish.test.ts b/test/cli/install/bun-publish.test.ts index c39f7786d8ba..fd50f3569ec9 100644 --- a/test/cli/install/bun-publish.test.ts +++ b/test/cli/install/bun-publish.test.ts @@ -1568,7 +1568,7 @@ describe.concurrent("--dry-run", () => { // publish only reads package.json, so a file that is readable but not writable (a // read-only checkout, a file owned by another user) must not stop it. - test.skipIf(isWindows)("read-only package.json", async () => { + test("read-only package.json", async () => { using dir = tempDir("publish-read-only", { "package.json": JSON.stringify({ name: "dry-run-read-only", version: "4.4.4" }), "index.js": "", @@ -1830,7 +1830,7 @@ describe.concurrent("tarball path", () => { // command line, and a path that fills the buffer exactly is refused by the open rather than the // join, which on Windows currently reports it as ENOSYS (it takes ENAMETOOLONG's number from the // C runtime, where it is a different one than in bun's errno table). - test.skipIf(isWindows).each([ + test.each([ ["longer than the path buffer", () => nameOfBytes(5000)], // Leaves no room for the terminating NUL. ["as long as the path buffer once resolved", (dir: string) => nameResolvingTo(PATH_BUFFER_BYTES, dir)], @@ -4191,7 +4191,7 @@ describe.concurrent.each(unprintable)("%s is printed escaped", (_, raw, spelled) const dryRunEnv = { ...env, npm_config_registry: "http://someuser:hunter2@127.0.0.1:1/" }; const anyUnprintable = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/; - test.skipIf(isWindows)("in a file name of the directory", async () => { + test("in a file name of the directory", async () => { using dir = tempDir("publish-file-name", { "package.json": JSON.stringify({ name: "publish-file-name", version: "1.0.0" }), [`a${raw}b.js`]: "x", diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index 47ee427898dc..a8beb5e7f1c2 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -513,10 +513,9 @@ for (const linker of ["hoisted", "isolated"] as const) { // The global bin directory can be a folder like ~/.local/bin, where a dead link may belong to another tool. const foreign = isWindows ? [] : ["foreign-absolute", "foreign-relative"]; - if (!isWindows) { - symlinkSync(join(String(dir), "gone", "cli.js"), join(globalBin, "foreign-absolute")); - symlinkSync(join("..", "gone", "cli.js"), join(globalBin, "foreign-relative")); - } + + symlinkSync(join(String(dir), "gone", "cli.js"), join(globalBin, "foreign-absolute")); + symlinkSync(join("..", "gone", "cli.js"), join(globalBin, "foreign-relative")); const { stderr, exitCode } = await runGlobal("remove", "what-bin"); expect(stderr).not.toContain("error:"); @@ -725,7 +724,7 @@ it.concurrent("bun r is an alias of bun remove", async () => { }); // Serial: `unprivilegedSpawnOptions` may change and restore the modes of directories above the test's own. -it.serial.skipIf(isWindows)("bun remove that cannot save bun.lock leaves node_modules as it was", async () => { +it.serial("bun remove that cannot save bun.lock leaves node_modules as it was", async () => { const pkg = JSON.stringify({ name: "root", dependencies: { foo: "file:./foo", bar: "file:./bar" } }); using dir = tempDir("bun-remove-lockfile-not-saved", { ...local("foo"), ...local("bar"), "package.json": pkg }); expect(await run(String(dir), "install")).toMatchObject({ exitCode: 0 }); diff --git a/test/cli/install/bun-run-bunfig.test.ts b/test/cli/install/bun-run-bunfig.test.ts index f8aecb74304c..1c3e310ba863 100644 --- a/test/cli/install/bun-run-bunfig.test.ts +++ b/test/cli/install/bun-run-bunfig.test.ts @@ -55,7 +55,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => { }); describe.each(["bun", "system", "default"])(`run.shell = "%s"`, shellStr => { - if (isWindows && shellStr === "system") return; // windows always uses the bun shell now + // windows always uses the bun shell now const shell = shellStr === "default" ? (isWindows ? "bun" : "system") : shellStr; const command_not_found = isWindows && shell === "system" ? "is not recognized as an internal or external command" : "command not found"; diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 1c8918227511..a68b7f365a53 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -127,7 +127,7 @@ describe.concurrent("bun run", () => { expect(exitCode).toBe(1); }); - it.skipIf(isWindows)("exit code message works above 128", async () => { + it("exit code message works above 128", async () => { using dir = tempDir("bun-run-exitcode", {}); await using proc = Bun.spawn({ cmd: [bunExe(), "run", "bash", "-c", "ulimit -c 0; exit 200"], @@ -145,7 +145,7 @@ describe.concurrent("bun run", () => { describe.each(["--silent", "not silent"])("%s", silentOption => { const silent = silentOption === "--silent"; - it.skipIf(isWindows)("exit signal works", async () => { + it("exit signal works", async () => { using dir = tempDir("bun-run-signal", {}); { await using proc = Bun.spawn({ @@ -379,7 +379,7 @@ describe.concurrent("bun run", () => { expect(stderr).toContain(`Could not change directory to "${cwdArg}"`); // Windows leaves the verdict on a path this long to SetCurrentDirectoryW. - if (!isWindows) expect(stderr).toContain("ENAMETOOLONG"); + expect(stderr).toContain("ENAMETOOLONG"); expect(stdout).toBe(""); expect(proc.signalCode).toBeNull(); expect(exitCode).toBe(1); @@ -772,7 +772,7 @@ describe.concurrent("bun run", () => { expect(exitCode).toBe(0); }); - it.skipIf(isWindows)("doesn't change an `npm run` that's only quoted text", async () => { + it("doesn't change an `npm run` that's only quoted text", async () => { const { dir, stdout, exitCode } = await runStart("echo 'step: npm run build -w app' && echo 'ok; done'"); using _ = dir; expect(stdout).toContain("-w app"); @@ -1265,7 +1265,7 @@ describe.concurrent("bun run", () => { // `/bun` — exactly the shim it was about to rewrite. The symlink // becomes self-referencing and `/usr/bin/env node` fails with ELOOP // ("Too many levels of symbolic links"). - it.skipIf(isWindows)("nested --bun does not create a self-referencing node/bun shim", async () => { + it("nested --bun does not create a self-referencing node/bun shim", async () => { // Reproduce the reporter's exact invocation: `bun run --bun bun run --bun ` // where: // - the literal "bun" must be resolved via PATH so the inner process's @@ -1556,7 +1556,7 @@ describe.concurrent("bun run", () => { // https://github.com/oven-sh/bun/issues/7504: `/tmp/bun-node-` was one directory for // all users. The first user to run `--bun` owned it (mode 0700), and every other user's // `--bun` silently got the real `node`. - it.skipIf(isWindows)("--bun creates a per-user node shim directory", async () => { + it("--bun creates a per-user node shim directory", async () => { using dir = tempDir("bun-run-node-shim-uid", { "package.json": JSON.stringify({ name: "shim-uid", diff --git a/test/cli/install/bun-upgrade.test.ts b/test/cli/install/bun-upgrade.test.ts index eacecd6981fa..b0e1096252f2 100644 --- a/test/cli/install/bun-upgrade.test.ts +++ b/test/cli/install/bun-upgrade.test.ts @@ -427,12 +427,10 @@ it.concurrent("recreates the staging directory in the temp dir instead of reusin // download (bun.zip) again. expect(readdirSync(join(stagingRootPath, "9.9.9"))).toEqual([]); - if (!isWindows) { - // The staging directory must be freshly created with no group/other - // access. The planted one was created with the default mode, so a reused - // directory fails here. - expect(statSync(join(stagingRootPath, "9.9.9")).mode & 0o077).toBe(0); - } + // The staging directory must be freshly created with no group/other + // access. The planted one was created with the default mode, so a reused + // directory fails here. + expect(statSync(join(stagingRootPath, "9.9.9")).mode & 0o077).toBe(0); // The bogus archive must not be installed; the upgrade fails cleanly. expect(exitCode).toBe(1); diff --git a/test/cli/install/bun-workspaces-self-contained.test.ts b/test/cli/install/bun-workspaces-self-contained.test.ts index a8c2d56d16d8..6fbecd607fdc 100644 --- a/test/cli/install/bun-workspaces-self-contained.test.ts +++ b/test/cli/install/bun-workspaces-self-contained.test.ts @@ -129,13 +129,13 @@ describe.each([ expect(existsSync(join(desktopNm, "@barn", "moo", "package.json"))).toBeTrue(); expect(readlinkSync(join(desktopNm, "shared"))).toContain("shared"); // … as real files, not hardlinks into the cache - if (!isWindows) { - expect(lstatSync(join(desktopNm, "bar")).isSymbolicLink()).toBeFalse(); - expect(lstatSync(join(desktopNm, "bar", "package.json")).isSymbolicLink()).toBeFalse(); - expect(statSync(join(desktopNm, "bar", "package.json")).nlink).toBe(1); - // control: the root's copy of the same package *is* hardlinked from the cache - expect(statSync(join(package_dir, "node_modules", "bar", "package.json")).nlink).toBeGreaterThan(1); - } + + expect(lstatSync(join(desktopNm, "bar")).isSymbolicLink()).toBeFalse(); + expect(lstatSync(join(desktopNm, "bar", "package.json")).isSymbolicLink()).toBeFalse(); + expect(statSync(join(desktopNm, "bar", "package.json")).nlink).toBe(1); + // control: the root's copy of the same package *is* hardlinked from the cache + expect(statSync(join(package_dir, "node_modules", "bar", "package.json")).nlink).toBeGreaterThan(1); + // even though `bar` also exists at the root for the root package / other workspaces expect(existsSync(join(package_dir, "node_modules", "bar", "package.json"))).toBeTrue(); // the other workspace hoists as usual @@ -155,9 +155,8 @@ describe.each([ expect(again.err).not.toContain("error:"); expect(again.code).toBe(0); expect(await readdirSorted(desktopNm)).toEqual(["@barn", "bar", "baz", "shared"]); - if (!isWindows) { - expect(statSync(join(desktopNm, "bar", "package.json")).nlink).toBe(1); - } + + expect(statSync(join(desktopNm, "bar", "package.json")).nlink).toBe(1); }); }); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 4a2a612072b0..19cf9754b53c 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -4299,9 +4299,8 @@ test.concurrent("a copyfile install over a workspace's hardlinked files does not const nestedPkgJson = join(packageDir, "packages", "pkg1", "node_modules", "no-deps", "package.json"); await install("hardlink"); expect(readJson(nestedPkgJson)).toEqual({ name: "no-deps", version: "2.0.0" }); - if (!isWindows) { - expect(statSync(nestedPkgJson).nlink).toBeGreaterThan(1); - } + + expect(statSync(nestedPkgJson).nlink).toBeGreaterThan(1); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await install("copyfile"); @@ -4311,10 +4310,9 @@ test.concurrent("a copyfile install over a workspace's hardlinked files does not name: "no-deps", version: "1.0.0", }); - if (!isWindows) { - // a new file, not the cache file's inode - expect(statSync(nestedPkgJson).nlink).toBe(1); - } + + // a new file, not the cache file's inode + expect(statSync(nestedPkgJson).nlink).toBe(1); const cacheDir = join(packageDir, ".bun-cache"); const cached = (await readdirSorted(cacheDir)).filter(name => name.startsWith("no-deps@2.0.0@")); @@ -4325,7 +4323,7 @@ test.concurrent("a copyfile install over a workspace's hardlinked files does not for (const linker of ["hoisted", "isolated"]) { // symlinkSync to a directory needs Developer Mode or elevation on Windows. - test.concurrent.skipIf(isWindows)(`workspace globs match a symlinked member directory (${linker})`, async () => { + test.concurrent(`workspace globs match a symlinked member directory (${linker})`, async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; // Its own version of no-deps is installed in its own node_modules, behind the link. @@ -4378,7 +4376,7 @@ for (const [link, target] of [ ["packages/s", "real/deeper/s"], ["packages", "real/deeper"], ]) { - test.concurrent.skipIf(isWindows)(`isolated: a member finds its dependencies when ${link} is a symlink`, async () => { + test.concurrent(`isolated: a member finds its dependencies when ${link} is a symlink`, async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; await Promise.all([ @@ -4555,7 +4553,7 @@ describe("a workspace outside the project", () => { }); }); - test.concurrent.skipIf(isWindows)("a folder with a colon in its name is inside", async () => { + test.concurrent("a folder with a colon in its name is inside", async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; const project = await setup(packageDir, { workspaces: ["a:b/pkg"] }); @@ -4572,7 +4570,7 @@ describe("a workspace outside the project", () => { }); }); -describe.skipIf(isWindows)("in a workspace that's a link to a folder somewhere else", () => { +describe("in a workspace that's a link to a folder somewhere else", () => { /** `cwd` and `pwd` are relative to the directory that holds `repo` and `other`. */ async function install(cwd: string, pwd: string) { using ctx = await setupTest(); @@ -4747,7 +4745,7 @@ describe.skipIf(isWindows)("in a workspace that's a link to a folder somewhere e }); }); -test.concurrent.skipIf(isWindows)("`**` in a workspace glob does not go through a symlinked directory", async () => { +test.concurrent("`**` in a workspace glob does not go through a symlinked directory", async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; await Promise.all([ @@ -5034,7 +5032,7 @@ describe("links of a renamed or removed workspace", () => { // the isolated linker looped forever. On POSIX a symlinked root package.json // made workspace discovery run in the symlink target's directory. describe("aliased project root", () => { - test.concurrent.skipIf(isWindows)("workspaces install when the root package.json is a symlink", async () => { + test.concurrent("workspaces install when the root package.json is a symlink", async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; await Promise.all([ diff --git a/test/cli/install/bunx-directories-bin.test.ts b/test/cli/install/bunx-directories-bin.test.ts index fd2702b6d456..968adc05fe03 100644 --- a/test/cli/install/bunx-directories-bin.test.ts +++ b/test/cli/install/bunx-directories-bin.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -17,7 +17,7 @@ async function run(cwd: string, args: string[], env: Record) { } // The bins are shell scripts, which the bin links cannot run on Windows. -test.concurrent.skipIf(isWindows)("bunx runs the bin of a directories.bin package", async () => { +test.concurrent("bunx runs the bin of a directories.bin package", async () => { using dir = tempDir("bunx-directories-bin", { "package.json": JSON.stringify({ name: "app", dependencies: { tool: "file:./tool" } }), "tool/package.json": JSON.stringify({ name: "tool", version: "1.0.0", directories: { bin: "bins" } }), @@ -49,7 +49,7 @@ const rejected: [label: string, value: (dir: string) => string, picked: string][ ]; for (const [label, value, picked] of rejected) { - test.concurrent.skipIf(isWindows)(`bunx ignores a directories.bin that is ${label}`, async () => { + test.concurrent(`bunx ignores a directories.bin that is ${label}`, async () => { using dir = tempDir("bunx-directories-bin-rejected", { "package.json": JSON.stringify({ name: "app", dependencies: { tool: "file:./tool" } }), "tool/package.json": "", diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 66ee8cf71751..4c7fd34be668 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -384,7 +384,7 @@ it.concurrent("should choose the tagged versions instead of the PATH versions wh "bunx-decoy-bin", isWindows ? { "semver.cmd": "@echo semver from PATH\r\n" } : { "semver": '#!/bin/sh\necho "semver from PATH"\n' }, ); - if (!isWindows) chmodSync(join(String(decoyBinDir), "semver"), 0o755); + chmodSync(join(String(decoyBinDir), "semver"), 0o755); const processes = semverVersions.map(version => { return spawn({ @@ -1656,7 +1656,7 @@ it.concurrent.skipIf(!isWindows)("completions: writes bunx.cmd (not bunx.exe) wh // directory where other users can create files, which the temp directory is. // `.bunx-` is the trust boundary: a directory of this user with mode 0700. // Windows keeps %TEMP%\bunx--@: %TEMP% is per user. -describe.skipIf(isWindows)("cache directory", () => { +describe("cache directory", () => { const uid = process.getuid?.() ?? 0; const ran = "tool 1.1.0 with dep 1.0.0\n"; const bunxEntries = (dir: string) => @@ -2115,7 +2115,7 @@ describe("a cached tree", () => { }); // On Windows the entry in .bin is a shim that exists on its own. - it.concurrent.skipIf(isWindows)("relinks a tree whose bin file is missing", async () => { + it.concurrent("relinks a tree whose bin file is missing", async () => { using registry = await localRegistry(...toolWithDep()); using box = sandbox(registry); expect(await box.run([spec])).toMatchObject({ stdout: ran, exitCode: 0 }); @@ -2447,7 +2447,7 @@ require("child_process").execFileSync(process.env.BUN_EXE, ["install"], { cwd: p }); // Like on a file system without locks. - it.concurrent.skipIf(isWindows)("runs without the lock when it cannot be taken", async () => { + it.concurrent("runs without the lock when it cannot be taken", async () => { using registry = await localRegistry(...toolWithDep()); using box = sandbox(registry); mkdirSync(box.tree("tool@latest") + ".lock", { recursive: true }); @@ -2488,7 +2488,7 @@ describe("a package in node_modules", () => { }); }); - it.concurrent.skipIf(isWindows)("a command in node_modules/.bin that cannot run is not downloaded", async () => { + it.concurrent("a command in node_modules/.bin that cannot run is not downloaded", async () => { using registry = await localRegistry(fixturePackage("gen", "1.0.0", "gen")); using box = sandbox(registry); addLocalBin(box.cwd, "gen", `console.log("mine");`); @@ -2503,7 +2503,7 @@ describe("a package in node_modules", () => { }); // The project has the package, so bunx runs that copy or says why it cannot. It does not download another one. - describe.skipIf(isWindows)("a bin that cannot run", () => { + describe("a bin that cannot run", () => { type Entry = "linked" | "removed" | "dangling" | "no .bin"; /** Takes the execute bit from the bin of `name`, and puts its entry in node_modules/.bin into the state `entry`. */ function takeExecuteBit(cwd: string, name: string, bin: string, entry: Entry) { @@ -2766,7 +2766,7 @@ describe("a package in node_modules", () => { // Of two packages with one bin name, one gets the entry in .bin. The install makes the file of the other // executable too. On Windows the entry is a shim that does not say whose it is, and bunx starts it. - it.concurrent.todoIf(isWindows)("after an install, the package that did not get the entry in .bin", async () => { + it.concurrent("after an install, the package that did not get the entry in .bin", async () => { using registry = await localRegistry( fixturePackage("aaa-first", "1.0.0", "tool"), fixturePackage("zzz-last", "1.0.0", "tool", echoBin("zzz-last 1.0.0")), @@ -2794,7 +2794,7 @@ describe("a package in node_modules", () => { }); // The file is not there when the bins are linked. The install makes it executable after the script. - it.concurrent.todoIf(isWindows)( + it.concurrent( "after an install, a package without the entry in .bin whose install script writes the bin", async () => { const gen = (version: string): FixturePackage => ({ @@ -2828,7 +2828,7 @@ describe("a package in node_modules", () => { }, ); - it.concurrent.todoIf(isWindows)("after an install, a workspace package whose bin name a dependency has", async () => { + it.concurrent("after an install, a workspace package whose bin name a dependency has", async () => { using registry = await localRegistry( fixturePackage("aaa-first", "1.0.0", "tool"), fixturePackage("mycli", "9.9.9", "tool"), @@ -2999,7 +2999,7 @@ describe("a package in node_modules", () => { }); // On Windows the entry is no link, so it does not say whose it is. - it.concurrent.skipIf(isWindows).each([[["@corp/tool"]], [["-p", "@corp/tool", "tool"]]])( + it.concurrent.each([[["@corp/tool"]], [["-p", "@corp/tool", "tool"]]])( "bunx %j runs the bin of that package where another one has the entry in .bin", async args => { using box = sandbox(); @@ -3091,7 +3091,7 @@ describe("flags", () => { const expected = { stdout: `local ${flags}\n`, stderr: "", exitCode: 0, requests: [] }; expect(await box.run([...flags.split(" "), "local-tool", ...flags.split(" ")])).toEqual(expected); - if (isWindows) return; + symlinkSync(bunExe(), join(box.dir, isDebug ? "bun-debug" : "bun")); const env = { ...box.env, PATH: box.dir + delimiter + box.env.PATH }; expect(await box.run(["run", "--silent", "tool"], { env, bunx: [bunExe()] })).toEqual(expected); @@ -3182,23 +3182,20 @@ describe("flags", () => { }); // BUN_OPTIONS is for every command. - it.concurrent.skipIf(isWindows)( - "flags in BUN_OPTIONS that bunx doesn't know are skipped, including their values", - async () => { - using registry = await localRegistry(); - using box = sandbox(registry); - addLocalBin(box.cwd, "local-tool", `console.log("local", ...process.argv.slice(2));`); - symlinkSync(bunExe(), join(box.dir, "bunx")); + it.concurrent("flags in BUN_OPTIONS that bunx doesn't know are skipped, including their values", async () => { + using registry = await localRegistry(); + using box = sandbox(registry); + addLocalBin(box.cwd, "local-tool", `console.log("local", ...process.argv.slice(2));`); + symlinkSync(bunExe(), join(box.dir, "bunx")); - const env = { ...box.env, BUN_OPTIONS: "--smol --preload ./preloaded.ts" }; - expect(await box.run(["local-tool", "--nope"], { env, bunx: [join(box.dir, "bunx")] })).toEqual({ - stdout: "local --nope\n", - stderr: "", - exitCode: 0, - requests: [], - }); - }, - ); + const env = { ...box.env, BUN_OPTIONS: "--smol --preload ./preloaded.ts" }; + expect(await box.run(["local-tool", "--nope"], { env, bunx: [join(box.dir, "bunx")] })).toEqual({ + stdout: "local --nope\n", + stderr: "", + exitCode: 0, + requests: [], + }); + }); it.concurrent.each(["--help", "-h"])("%s prints the flags", async flag => { using box = sandbox(); @@ -3509,7 +3506,7 @@ describe("paths that do not fit in the path buffer", () => { // The install cache is so deep that /node_modules//package.json, which bunx reads for the // name of the bin, ends right at the size of the buffer, while /node_modules/.bin/ always fits. // One byte under, it fits together with its NUL. On Windows no existing directory overflows the buffer. - it.concurrent.skipIf(isWindows).each([ + it.concurrent.each([ ["one byte under the size of the path buffer", -1], ["exactly the size of the path buffer", 0], ["64 bytes over the size of the path buffer", 64], diff --git a/test/cli/install/escape-control-characters.test.ts b/test/cli/install/escape-control-characters.test.ts index 259df70ef514..dea9fbd387f3 100644 --- a/test/cli/install/escape-control-characters.test.ts +++ b/test/cli/install/escape-control-characters.test.ts @@ -434,7 +434,7 @@ describe.concurrent.each(Object.entries(messages))( ); // Windows has no such file names. -test.concurrent.skipIf(isWindows).each(characters)("--verbose, a file of a package: %s", async (_, raw, spelled) => { +test.concurrent.each(characters)("--verbose, a file of a package: %s", async (_, raw, spelled) => { using dir = tempDir("escape-controls-file-name", { "package.json": JSON.stringify({ ...app, dependencies: { dep: "./dep.tgz" } }), }); diff --git a/test/cli/install/install-fd-leak.test.ts b/test/cli/install/install-fd-leak.test.ts index 2597456add68..e35a69065a55 100644 --- a/test/cli/install/install-fd-leak.test.ts +++ b/test/cli/install/install-fd-leak.test.ts @@ -1,7 +1,7 @@ import { Archive, spawn } from "bun"; import { beforeAll, describe, expect, test } from "bun:test"; import { rm } from "fs/promises"; -import { bunEnv, bunExe, isMacOS, isWindows, readdirSorted, tempDir } from "harness"; +import { bunEnv, bunExe, isMacOS, readdirSorted, tempDir } from "harness"; import { join } from "path"; // Installing a package with many files must not exhaust the file-descriptor @@ -16,7 +16,7 @@ const LINKERS = ["isolated", "hoisted"] as const; const BACKENDS = (["copyfile", "hardlink"] as string[]).concat(isMacOS ? (["clonefile"] as const) : []); // `ulimit` isn't available on Windows; skip the matrix there. -describe.skipIf(isWindows)("install does not leak file descriptors", () => { +describe("install does not leak file descriptors", () => { let tgz: string; beforeAll(async () => { diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 91fc99bb6fdb..67df048dce74 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -620,7 +620,7 @@ test.concurrent("can install folder dependencies", async () => { ).toBe("module.exports = 'hello from pkg-1';"); }); -test.concurrent.skipIf(isWindows)("a backslash in the name of a file or a directory of a package is kept", async () => { +test.concurrent("a backslash in the name of a file or a directory of a package is kept", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker: "isolated" } }); await Promise.all([ @@ -1040,7 +1040,7 @@ test.concurrent( ); // symlinkSync needs Developer Mode or elevation on Windows. -describe.skipIf(isWindows).each(["isolated", "hoisted"] as const)("links in a file: folder (%s)", linker => { +describe.each(["isolated", "hoisted"] as const)("links in a file: folder (%s)", linker => { const files = { "package.json": JSON.stringify({ name: "folder-links", dependencies: { lib: "file:./vendor/lib" } }), "shared/tsconfig.base.json": JSON.stringify({ shared: true }), @@ -1260,96 +1260,90 @@ describe.each(["isolated", "hoisted"] as const)("bin target modes with the %s li ["077", 0o700], ["002", 0o775], ])("umask %s", (umask, expectedMode) => { - test.concurrent.skipIf(isWindows)( - "are 0o777 & ~umask, and an install that changes nothing writes nothing", - async () => { - const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker } }); - const workspaceBin = join(packageDir, "packages", "tool", "cli.js"); - const binDir = join(packageDir, "node_modules", ".bin"); - const links = ["tool", "dep", "tb", "what-bin"].map(name => join(binDir, name)); - // A cache that the tests share hardlinks one inode of the tarball's bin into each of them. - const env = installEnv(packageDir); + test.concurrent("are 0o777 & ~umask, and an install that changes nothing writes nothing", async () => { + const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker } }); + const workspaceBin = join(packageDir, "packages", "tool", "cli.js"); + const binDir = join(packageDir, "node_modules", ".bin"); + const links = ["tool", "dep", "tb", "what-bin"].map(name => join(binDir, name)); + // A cache that the tests share hardlinks one inode of the tarball's bin into each of them. + const env = installEnv(packageDir); - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "umask-root", - workspaces: ["packages/*"], - dependencies: { - tool: "workspace:*", - dep: "file:./dep", - tb: "file:./tb-1.0.0.tgz", - "what-bin": "1.0.0", - }, - }), - ), - write( - join(packageDir, "packages", "tool", "package.json"), - JSON.stringify({ name: "tool", version: "1.0.0", bin: { tool: "./cli.js" } }), - ), - write(workspaceBin, "#!/usr/bin/env node\nconsole.log(1)\n"), - write( - join(packageDir, "dep", "package.json"), - JSON.stringify({ name: "dep", version: "1.0.0", bin: "d.js" }), - ), - write(join(packageDir, "dep", "d.js"), "#!/bin/sh\necho d\n"), - Bun.Archive.write( - join(packageDir, "tb-1.0.0.tgz"), - { - "package/package.json": JSON.stringify({ name: "tb", version: "1.0.0", bin: "t.js" }), - "package/t.js": "#!/bin/sh\necho t\n", + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "umask-root", + workspaces: ["packages/*"], + dependencies: { + tool: "workspace:*", + dep: "file:./dep", + tb: "file:./tb-1.0.0.tgz", + "what-bin": "1.0.0", }, - { compress: "gzip" }, - ), - ]); - await Promise.all([chmod(workspaceBin, 0o644), chmod(join(packageDir, "dep", "d.js"), 0o644)]); - - const want = expectedMode.toString(8); - async function installAndExpectModes() { - await using proc = spawn({ - cmd: ["sh", "-c", `umask ${umask} && exec "$0" install`, bunExe()], - cwd: packageDir, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).not.toContain("error:"); - expect([workspaceBin, ...links].map(path => (statSync(path).mode & 0o777).toString(8))).toEqual([ - want, - want, - want, - want, - want, - ]); - expect(exitCode).toBe(0); - } + }), + ), + write( + join(packageDir, "packages", "tool", "package.json"), + JSON.stringify({ name: "tool", version: "1.0.0", bin: { tool: "./cli.js" } }), + ), + write(workspaceBin, "#!/usr/bin/env node\nconsole.log(1)\n"), + write(join(packageDir, "dep", "package.json"), JSON.stringify({ name: "dep", version: "1.0.0", bin: "d.js" })), + write(join(packageDir, "dep", "d.js"), "#!/bin/sh\necho d\n"), + Bun.Archive.write( + join(packageDir, "tb-1.0.0.tgz"), + { + "package/package.json": JSON.stringify({ name: "tb", version: "1.0.0", bin: "t.js" }), + "package/t.js": "#!/bin/sh\necho t\n", + }, + { compress: "gzip" }, + ), + ]); + await Promise.all([chmod(workspaceBin, 0o644), chmod(join(packageDir, "dep", "d.js"), 0o644)]); - await installAndExpectModes(); + const want = expectedMode.toString(8); + async function installAndExpectModes() { + await using proc = spawn({ + cmd: ["sh", "-c", `umask ${umask} && exec "$0" install`, bunExe()], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("error:"); + expect([workspaceBin, ...links].map(path => (statSync(path).mode & 0o777).toString(8))).toEqual([ + want, + want, + want, + want, + want, + ]); + expect(exitCode).toBe(0); + } - // A link that is removed and made again loses its mtime. A chmod, even to the mode the file - // has, moves its ctime, which has the resolution of the kernel's clock tick: wait one out. - // The folder and the local tarball will not do: a run can install those again. - const unchanged = [workspaceBin, realpathSync(join(binDir, "what-bin"))]; - const ctimes = () => unchanged.map(path => statSync(path, { bigint: true }).ctimeNs); - for (const link of links) lutimesSync(link, 1, 1); - const before = ctimes(); - const probe = join(packageDir, "ctime-probe"); - await write(probe, ""); - do chmodSync(probe, 0o600); - while (before.some(ctime => statSync(probe, { bigint: true }).ctimeNs <= ctime)); + await installAndExpectModes(); + + // A link that is removed and made again loses its mtime. A chmod, even to the mode the file + // has, moves its ctime, which has the resolution of the kernel's clock tick: wait one out. + // The folder and the local tarball will not do: a run can install those again. + const unchanged = [workspaceBin, realpathSync(join(binDir, "what-bin"))]; + const ctimes = () => unchanged.map(path => statSync(path, { bigint: true }).ctimeNs); + for (const link of links) lutimesSync(link, 1, 1); + const before = ctimes(); + const probe = join(packageDir, "ctime-probe"); + await write(probe, ""); + do chmodSync(probe, 0o600); + while (before.some(ctime => statSync(probe, { bigint: true }).ctimeNs <= ctime)); + await installAndExpectModes(); + expect(links.map(link => lstatSync(link).mtimeMs)).toEqual([1000, 1000, 1000, 1000]); + expect(ctimes()).toEqual(before); + + // It isn't executable. The isolated linker of older versions left files like this behind. + for (const mode of [0o644, 0o777]) { + await Promise.all(links.map(link => chmod(realpathSync(link), mode))); await installAndExpectModes(); - expect(links.map(link => lstatSync(link).mtimeMs)).toEqual([1000, 1000, 1000, 1000]); - expect(ctimes()).toEqual(before); - - // It isn't executable. The isolated linker of older versions left files like this behind. - for (const mode of [0o644, 0o777]) { - await Promise.all(links.map(link => chmod(realpathSync(link), mode))); - await installAndExpectModes(); - } - }, - ); + } + }); }); }); @@ -2258,10 +2252,9 @@ describe.concurrent("lifecycle scripts of workspace members", () => { await runBunInstall(installEnv(packageDir), packageDir); expect(await bins()).toEqual({ root: names, member: names, registryPackage: names }); - if (!isWindows) { - // The script writes the file without the execute bit. The linker sets the bit when it links the bin. - expect(statSync(join(packageDir, "packages", "no-deps", "dist", "no-deps-cli")).mode & 0o100).toBe(0o100); - } + + // The script writes the file without the execute bit. The linker sets the bit when it links the bin. + expect(statSync(join(packageDir, "packages", "no-deps", "dist", "no-deps-cli")).mode & 0o100).toBe(0o100); await runBunInstall(installEnv(packageDir), packageDir, { savesLockfile: false }); @@ -5983,7 +5976,7 @@ describe.concurrent("bin links of a failed optional dependency (hoisted)", () => }); // Here, the `.bin` of w2 is a link to the root's `.bin`, which has the link of the root's q. - test.concurrent.skipIf(isWindows)("nothing is removed through a `.bin` that is a link", async () => { + test.concurrent("nothing is removed through a `.bin` that is a link", async () => { const { packageDir } = await registry.createTestDir({ bunfigOpts: { linker: "hoisted" }, files: { @@ -7047,7 +7040,7 @@ describe.concurrent("package trees deeper than the path buffer", () => { // On Windows the buffer holds as much as the filesystem does, so no tree that exists overflows it // with a store-relative path; the last test overflows with the absolute path instead. - describe.skipIf(isWindows)("a folder dependency", () => { + describe("a folder dependency", () => { test("fails with ENAMETOOLONG when a directory does not fit", async () => { using dir = projectWithDeepFolderDependency("isolated-deep-dir", chainDirs(PATH_BUFFER_LEN + 256)); expectPackageFailure(await bunInstall(String(dir)), "pkg"); @@ -7074,7 +7067,7 @@ describe.concurrent("package trees deeper than the path buffer", () => { }); // Folder dependencies are always hardlinked; --backend selects how packages from the cache are linked. - describe.skipIf(isWindows)("a cached package", () => { + describe("a cached package", () => { test.each(["hardlink", "copyfile"])("fails with ENAMETOOLONG and leaves nothing (%s)", async backend => { const { dir, cachedPackage, entryNodeModules } = await projectWithCachedPackage(`isolated-cached-${backend}`); using _ = dir; @@ -7509,7 +7502,7 @@ describe.concurrent("global virtual store", () => { expect(await readdirSorted(dirname(gvsTarget))).toEqual([basename(gvsTarget)]); }); - test.skipIf(isWindows)("--force replaces an entry atomically, so it's never missing", async () => { + test("--force replaces an entry atomically, so it's never missing", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: gvsBunfigOpts }); await write( packageJson, diff --git a/test/cli/install/migration/pnpm-lock-migration.test.ts b/test/cli/install/migration/pnpm-lock-migration.test.ts index 2e6b137dc011..eb7c8cc06c24 100644 --- a/test/cli/install/migration/pnpm-lock-migration.test.ts +++ b/test/cli/install/migration/pnpm-lock-migration.test.ts @@ -1,6 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunExe, bunEnv as harnessEnv, isRoot, isWindows, tempDir } from "harness"; +import { bunExe, bunEnv as harnessEnv, isRoot, tempDir } from "harness"; import { join } from "path"; import { startNpmjs } from "./migration-harness"; @@ -811,7 +811,7 @@ importers: }); // A 0444 package.json does not stop root from writing it, and the mode bits mean something else on Windows. - test.skipIf(isWindows || isRoot).each([ + test.skipIf(isRoot).each([ ["without a lockfile", {}], [ "while migrating pnpm-lock.yaml", diff --git a/test/cli/install/migration/pnpm-lock-v9.test.ts b/test/cli/install/migration/pnpm-lock-v9.test.ts index 0cb4ddf4edec..a1769071ac60 100644 --- a/test/cli/install/migration/pnpm-lock-v9.test.ts +++ b/test/cli/install/migration/pnpm-lock-v9.test.ts @@ -5,7 +5,6 @@ import { bunExe, githubTarball, hostedGitRepository, - isWindows, nodeModulesPackages, tempDir, unprivilegedSpawnOptions, @@ -4166,35 +4165,32 @@ importers: // The copied fields are written before bun.lock, so a read-only package.json fails the migration outright // rather than leaving a bun.lock whose overrides the next install cannot see in package.json. - test.concurrent.skipIf(isWindows)( - "a read-only package.json fails the migration before bun.lock is written", - async () => { - const packageJson = JSON.stringify({ name: "overrides-read-only", pnpm: { overrides: { plain: "1.0.0" } } }); - using dir = tempDir("pnpm-v9-overrides-read-only", { - "package.json": packageJson, - "pnpm-lock.yaml": overridesLockfile(" plain: 1.0.0"), - }); - chmodSync(join(String(dir), "package.json"), 0o444); - using unprivileged = unprivilegedSpawnOptions(String(dir)); - - await using proc = Bun.spawn({ - cmd: [bunExe(), "pm", "migrate"], - cwd: String(dir), - env: { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(String(dir), ".bun-cache") }, - stdout: "pipe", - stderr: "pipe", - ...unprivileged, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + test.concurrent("a read-only package.json fails the migration before bun.lock is written", async () => { + const packageJson = JSON.stringify({ name: "overrides-read-only", pnpm: { overrides: { plain: "1.0.0" } } }); + using dir = tempDir("pnpm-v9-overrides-read-only", { + "package.json": packageJson, + "pnpm-lock.yaml": overridesLockfile(" plain: 1.0.0"), + }); + chmodSync(join(String(dir), "package.json"), 0o444); + using unprivileged = unprivilegedSpawnOptions(String(dir)); - expect(stderr).toContain("error: failed to write package.json: EACCES"); - expect(stderr).not.toContain("copied"); - expect(stdout).toBe(""); - expect(exitCode).toBe(1); - expect(await Bun.file(join(String(dir), "package.json")).text()).toBe(packageJson); - expect(existsSync(join(String(dir), "bun.lock"))).toBe(false); - }, - ); + await using proc = Bun.spawn({ + cmd: [bunExe(), "pm", "migrate"], + cwd: String(dir), + env: { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(String(dir), ".bun-cache") }, + stdout: "pipe", + stderr: "pipe", + ...unprivileged, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toContain("error: failed to write package.json: EACCES"); + expect(stderr).not.toContain("copied"); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + expect(await Bun.file(join(String(dir), "package.json")).text()).toBe(packageJson); + expect(existsSync(join(String(dir), "bun.lock"))).toBe(false); + }); }); // The settings moved out of `pnpm` / pnpm-workspace.yaml are edited into package.json in memory while the diff --git a/test/cli/install/migration/pnpm-migration.test.ts b/test/cli/install/migration/pnpm-migration.test.ts index 47e6a70871e4..16d59aafd288 100644 --- a/test/cli/install/migration/pnpm-migration.test.ts +++ b/test/cli/install/migration/pnpm-migration.test.ts @@ -1,6 +1,6 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunExe, bunEnv as env, isLinux, isWindows, nodeModulesPackages, tempDir } from "harness"; +import { bunExe, bunEnv as env, isLinux, nodeModulesPackages, tempDir } from "harness"; import { join } from "path"; import { TestRegistry } from "registry"; @@ -311,7 +311,7 @@ snapshots: // The link resolves to `${dir}/packages/a/${link}`. A path of the buffer size leaves no room // for the NUL that ends it, so that is the first length that does not fit. - test.skipIf(isWindows).each([ + test.each([ ["one byte below", -1, "migrated lockfile from pnpm-lock.yaml", 0], ["exactly", 0, linkTooLong, 1], ["one byte above", 1, linkTooLong, 1], diff --git a/test/cli/install/migration/yarn-berry.test.ts b/test/cli/install/migration/yarn-berry.test.ts index e14681baeee3..8bd0683ecba7 100644 --- a/test/cli/install/migration/yarn-berry.test.ts +++ b/test/cli/install/migration/yarn-berry.test.ts @@ -5,7 +5,6 @@ import { bunEnv, bunExe, isLinux, - isWindows, libcFamily, nodeModulesPackages, normalizeBunSnapshot, @@ -509,31 +508,28 @@ describe("yarn berry migration", () => { }); // Otherwise bun.lock would hold patches and ranges the next install cannot see in package.json. - test.concurrent.skipIf(isWindows)( - "a read-only package.json fails the migration before bun.lock is written", - async () => { - const dir = await fixture("patch"); - const before = await Bun.file(join(dir, "package.json")).text(); - chmodSync(join(dir, "package.json"), 0o444); - { - using unprivileged = unprivilegedSpawnOptions(dir); - const { stderr, exitCode } = await spawnBun(dir, {}, unprivileged, ["pm", "migrate"]); - - expect(stderr).toContain("error: failed to write package.json: EACCES"); - expect(stderr).not.toContain(editedLine); - expect(await Bun.file(join(dir, "package.json")).text()).toBe(before); - expect(existsSync(join(dir, "bun.lock"))).toBeFalse(); - expect(exitCode).toBe(1); - - // nothing is half done: once the file can be written the same command migrates - chmodSync(join(dir, "package.json"), 0o644); - const again = await spawnBun(dir, {}, unprivileged, ["pm", "migrate"]); - expect(again.stderr).not.toContain("error:"); - expect(await Bun.file(join(dir, "package.json")).json()).toEqual(migratedPackageJson); - expect(again.exitCode).toBe(0); - } - }, - ); + test.concurrent("a read-only package.json fails the migration before bun.lock is written", async () => { + const dir = await fixture("patch"); + const before = await Bun.file(join(dir, "package.json")).text(); + chmodSync(join(dir, "package.json"), 0o444); + { + using unprivileged = unprivilegedSpawnOptions(dir); + const { stderr, exitCode } = await spawnBun(dir, {}, unprivileged, ["pm", "migrate"]); + + expect(stderr).toContain("error: failed to write package.json: EACCES"); + expect(stderr).not.toContain(editedLine); + expect(await Bun.file(join(dir, "package.json")).text()).toBe(before); + expect(existsSync(join(dir, "bun.lock"))).toBeFalse(); + expect(exitCode).toBe(1); + + // nothing is half done: once the file can be written the same command migrates + chmodSync(join(dir, "package.json"), 0o644); + const again = await spawnBun(dir, {}, unprivileged, ["pm", "migrate"]); + expect(again.stderr).not.toContain("error:"); + expect(await Bun.file(join(dir, "package.json")).json()).toEqual(migratedPackageJson); + expect(again.exitCode).toBe(0); + } + }); }); test.concurrent("resolutions that rewrote a descriptor are followed", async () => { diff --git a/test/cli/install/npmrc.test.ts b/test/cli/install/npmrc.test.ts index 9ec486cdaaa5..965e9c07ce65 100644 --- a/test/cli/install/npmrc.test.ts +++ b/test/cli/install/npmrc.test.ts @@ -300,7 +300,7 @@ registry = http://localhost:${registry.port}/ expect(result.exitCode).toBe(0); }); - it.skipIf(isWindows)("bun install prints nothing when $NPM_CONFIG_USERCONFIG is a directory", async () => { + it("bun install prints nothing when $NPM_CONFIG_USERCONFIG is a directory", async () => { using dir = tempDir("npmrc-userconfig-directory", { ...pkg, "home/.keep": "", "elsewhere/ci.npmrc/.keep": "" }); const directory = join(String(dir), "elsewhere", "ci.npmrc"); const result = await bun(String(dir), { NPM_CONFIG_USERCONFIG: directory }, "install"); @@ -376,7 +376,7 @@ registry = http://localhost:${registry.port}/ // other POSIX platforms; on Windows it is larger than any environment variable can be), so // the longest directory whose "/.npmrc" and NUL terminator still fit is MAX_PATH_BYTES - 8 // bytes. A longer directory cannot contain an openable .npmrc and counts as having none. - describe.skipIf(isWindows)("$HOME longer than the path buffer", () => { + describe("$HOME longer than the path buffer", () => { const MAX_PATH_BYTES = isLinux ? 4096 : 1024; const LONGEST_HOME = MAX_PATH_BYTES - 8; @@ -5235,7 +5235,7 @@ describe.concurrent("`ca` and `cafile` are one setting: a layer that sets either // Like npm, Bun skips a config file that it can't read, e.g. a root-owned ~/.npmrc in a Docker image that runs as another // user. Not concurrent: `unprivilegedSpawnOptions` may change and restore the modes of directories above the test's own. -describe.skipIf(isWindows)("unreadable config files are skipped", () => { +describe("unreadable config files are skipped", () => { /** Runs bun in `/project` with `/home` as the home directory, as a user that file modes apply to. */ async function run( cmd: string[], diff --git a/test/cli/install/redacted-config-logs.test.ts b/test/cli/install/redacted-config-logs.test.ts index a5f631a9cd34..f3058563f1b7 100644 --- a/test/cli/install/redacted-config-logs.test.ts +++ b/test/cli/install/redacted-config-logs.test.ts @@ -1,6 +1,6 @@ import { iniInternals } from "bun:internal-for-testing"; import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, hostedGitRepository, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, hostedGitRepository, tempDir } from "harness"; import { join } from "path"; test("registry url password is sent as Basic auth and left out of request error output", async () => { @@ -1221,7 +1221,7 @@ describe.concurrent("the userinfo of a git URL is masked as a whole", () => { }); // git names the URL it wants a password for, and bun passes on what git says. - test.skipIf(isWindows)("git's own output", async () => { + test("git's own output", async () => { await using server = Bun.serve({ port: 0, hostname: "127.0.0.1", @@ -1242,7 +1242,7 @@ describe.concurrent("the userinfo of a git URL is masked as a whole", () => { }); // When no form of the URL can be cloned, each is named above what git said of it. - test.skipIf(isWindows)("every form of a URL that cannot be cloned", async () => { + test("every form of a URL that cannot be cloned", async () => { const url = (scheme: string, userinfo: string) => `${scheme}://${userinfo}@example.com/org/lib.git`; await using server = Bun.serve({ port: 0, diff --git a/test/cli/install/shebang-normalize.test.ts b/test/cli/install/shebang-normalize.test.ts index a3749401cdf7..13cfb0592aab 100644 --- a/test/cli/install/shebang-normalize.test.ts +++ b/test/cli/install/shebang-normalize.test.ts @@ -4,7 +4,7 @@ import { mkdir, readFile, stat, writeFile } from "fs/promises"; import { bunExe, bunEnv as env, isWindows, runBunInstall, tempDir, tmpdirSync } from "harness"; import { join } from "path"; -test.skipIf(isWindows)("bin linking normalizes CRLF in shebang", async () => { +test("bin linking normalizes CRLF in shebang", async () => { const testDir = tmpdirSync(); const pkgDir = join(testDir, "pkg"); const consumerDir = join(testDir, "consumer"); diff --git a/test/cli/install/symlink-path-traversal.test.ts b/test/cli/install/symlink-path-traversal.test.ts index 744a3b289d6e..71775f886423 100644 --- a/test/cli/install/symlink-path-traversal.test.ts +++ b/test/cli/install/symlink-path-traversal.test.ts @@ -146,7 +146,7 @@ function createTarball( // Skip on Windows - symlink extraction is POSIX-only const isWindows = process.platform === "win32"; -describe.concurrent.skipIf(isWindows)("symlink path traversal protection", () => { +describe.concurrent("symlink path traversal protection", () => { setDefaultTimeout(60000); it("rejects symlink targets that climb above the package root before re-entering a 'packages' directory (streaming extraction)", async () => { @@ -770,159 +770,92 @@ describe.concurrent.skipIf(isWindows)("symlink path traversal protection", () => } }); -it.skipIf(isWindows)( - "rejects symlink targets that climb through other symlinks from the same archive (.bun-tag write stays inside the package)", - async () => { - // A tarball can ship symlinks whose targets each normalize to a path - // inside the package (`l1 -> .`, `l2 -> l1/..`, `l3 -> l2/..`, ...). - // Lexically every hop is "safe", but the kernel resolves each link before - // applying `..`, so each hop climbs one directory until it clamps at `/`. - // A final `.bun-tag -> lN/` entry then - // makes the post-extraction `.bun-tag` marker write (O_CREAT|O_TRUNC) land - // on an arbitrary file. The extractor must reject symlink targets with a - // `..` component that follows a named component, and must not follow a - // pre-existing `.bun-tag` when writing the marker. - const victimPath = join(systemTmpDir, `bun-tag-victim-${Math.random().toString(36).slice(2, 10)}.txt`); - await writeFile(victimPath, "original-content"); - - const chainLength = 30; - const entries: Parameters[0] = [ - { name: "test-package/", type: "dir" }, - { - name: "test-package/package.json", - type: "file", - content: JSON.stringify({ name: "test-package", version: "1.0.0" }), - }, - { name: "test-package/l1", type: "symlink", linkname: "." }, - ]; - for (let i = 2; i <= chainLength; i++) { - // Normalizes to "" (inside the package), but resolves one directory - // above wherever l(i-1) resolves to. - entries.push({ name: `test-package/l${i}`, type: "symlink", linkname: `l${i - 1}/..` }); - } - // After `chainLength` hops the chain is clamped at `/`, so this resolves to - // the absolute victim path while still normalizing to a path inside the - // package directory. - entries.push({ - name: "test-package/.bun-tag", - type: "symlink", - linkname: `l${chainLength}/${victimPath.replace(/^\/+/, "")}`, - }); - const tarball = createTarball(entries); - - const server = Bun.serve({ - port: 0, - fetch(req) { - const url = new URL(req.url); - if (url.pathname.includes("/tarball/") || url.pathname.endsWith(".tar.gz")) { - return new Response(tarball, { headers: { "Content-Type": "application/gzip" } }); - } - if (url.pathname.includes("/repos/")) { - return Response.json({ default_branch: "main" }); - } - return new Response("Not Found", { status: 404 }); - }, - }); - - try { - using dir = tempDir("bun-tag-symlink-chain-test", {}); - const installDir = String(dir); - - await writeFile( - join(installDir, "package.json"), - JSON.stringify({ - name: "test-app", - version: "1.0.0", - dependencies: { "test-package": "github:user/repo#main" }, - }), - ); - - const proc = spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env: { - ...env, - GITHUB_API_URL: `http://localhost:${server.port}`, - BUN_INSTALL_CACHE_DIR: join(installDir, ".bun-cache"), - }, - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - // The file outside the extraction directory must be untouched: same - // content, not truncated, not replaced with the github tag string. - expect(await Bun.file(victimPath).text()).toBe("original-content"); +it("rejects symlink targets that climb through other symlinks from the same archive (.bun-tag write stays inside the package)", async () => { + // A tarball can ship symlinks whose targets each normalize to a path + // inside the package (`l1 -> .`, `l2 -> l1/..`, `l3 -> l2/..`, ...). + // Lexically every hop is "safe", but the kernel resolves each link before + // applying `..`, so each hop climbs one directory until it clamps at `/`. + // A final `.bun-tag -> lN/` entry then + // makes the post-extraction `.bun-tag` marker write (O_CREAT|O_TRUNC) land + // on an arbitrary file. The extractor must reject symlink targets with a + // `..` component that follows a named component, and must not follow a + // pre-existing `.bun-tag` when writing the marker. + const victimPath = join(systemTmpDir, `bun-tag-victim-${Math.random().toString(36).slice(2, 10)}.txt`); + await writeFile(victimPath, "original-content"); + + const chainLength = 30; + const entries: Parameters[0] = [ + { name: "test-package/", type: "dir" }, + { + name: "test-package/package.json", + type: "file", + content: JSON.stringify({ name: "test-package", version: "1.0.0" }), + }, + { name: "test-package/l1", type: "symlink", linkname: "." }, + ]; + for (let i = 2; i <= chainLength; i++) { + // Normalizes to "" (inside the package), but resolves one directory + // above wherever l(i-1) resolves to. + entries.push({ name: `test-package/l${i}`, type: "symlink", linkname: `l${i - 1}/..` }); + } + // After `chainLength` hops the chain is clamped at `/`, so this resolves to + // the absolute victim path while still normalizing to a path inside the + // package directory. + entries.push({ + name: "test-package/.bun-tag", + type: "symlink", + linkname: `l${chainLength}/${victimPath.replace(/^\/+/, "")}`, + }); + const tarball = createTarball(entries); + + const server = Bun.serve({ + port: 0, + fetch(req) { + const url = new URL(req.url); + if (url.pathname.includes("/tarball/") || url.pathname.endsWith(".tar.gz")) { + return new Response(tarball, { headers: { "Content-Type": "application/gzip" } }); + } + if (url.pathname.includes("/repos/")) { + return Response.json({ default_branch: "main" }); + } + return new Response("Not Found", { status: 404 }); + }, + }); - // The legitimate package contents are still installed. - const pkgDir = join(installDir, "node_modules", "test-package"); - await access(join(pkgDir, "package.json")); + try { + using dir = tempDir("bun-tag-symlink-chain-test", {}); + const installDir = String(dir); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); - } finally { - server.stop(); - await rm(victimPath, { force: true }); - } - }, - 60000, -); - -it.skipIf(isWindows)( - "does not change permissions of a file reached through a symlinked bin target", - async () => { - // After creating `node_modules/.bin/`, the installer chmods the bin - // target to make it executable. If the bin target inside the package is - // itself a symlink (git/file/workspace dependencies can ship one — the npm - // tarball extractor never materializes one), chmod follows it and changes - // the mode of whatever file it points at, including files outside - // node_modules. The chmod must be skipped when the bin target is a symlink. - using dir = tempDir("bin-target-symlink-test", { - // Pin the hoisted linker so the bin link lands at node_modules/.bin and - // the chmod runs against the package's own bin target. - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-symlink-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-symlinked-bin", + await writeFile( + join(installDir, "package.json"), + JSON.stringify({ + name: "test-app", version: "1.0.0", - bin: { "dep-with-symlinked-bin": "./payload" }, + dependencies: { "test-package": "github:user/repo#main" }, }), - "victim.txt": "do not make me executable", - }); - const installDir = String(dir); - - const victimPath = join(installDir, "victim.txt"); - await chmod(victimPath, 0o600); - // The bin target is a symlink whose destination is outside the package - // directory. - await symlink(join("..", "..", "victim.txt"), join(installDir, "packages", "dep", "payload")); + ); const proc = spawn({ cmd: [bunExe(), "install"], cwd: installDir, stdout: "pipe", stderr: "pipe", - env, + env: { + ...env, + GITHUB_API_URL: `http://localhost:${server.port}`, + BUN_INSTALL_CACHE_DIR: join(installDir, ".bun-cache"), + }, }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - // The file the symlink points at keeps its original permissions; the - // installer must not chmod through a symlinked bin target. - expect((await stat(victimPath)).mode & 0o777).toBe(0o600); + // The file outside the extraction directory must be untouched: same + // content, not truncated, not replaced with the github tag string. + expect(await Bun.file(victimPath).text()).toBe("original-content"); - // The bin link itself is still created. - const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-bin"); - expect((await lstat(binLink)).isSymbolicLink()).toBe(true); + // The legitimate package contents are still installed. + const pkgDir = join(installDir, "node_modules", "test-package"); + await access(join(pkgDir, "package.json")); if (exitCode !== 0) { console.error("Install failed with exit code:", exitCode); @@ -930,153 +863,200 @@ it.skipIf(isWindows)( console.error("stderr:", stderr); } expect(exitCode).toBe(0); - }, - 60000, -); - -it.skipIf(isWindows)( - "does not change permissions of a directory reached through a symlinked bin target with a trailing slash", - async () => { - // Same as above, but the bin target is written as "payload/". The kernel - // follows a symlink named with a trailing slash even when chmod is told not - // to follow symlinks, so the installer has to drop the slash before it - // stats, links and chmods the target. - using dir = tempDir("bin-target-symlink-trailing-slash-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-symlink-slash-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-symlinked-dir-bin", - version: "1.0.0", - bin: { "dep-with-symlinked-dir-bin": "./payload/" }, - }), - "victim-dir/keep": "", - }); - const installDir = String(dir); + } finally { + server.stop(); + await rm(victimPath, { force: true }); + } +}, 60000); + +it("does not change permissions of a file reached through a symlinked bin target", async () => { + // After creating `node_modules/.bin/`, the installer chmods the bin + // target to make it executable. If the bin target inside the package is + // itself a symlink (git/file/workspace dependencies can ship one — the npm + // tarball extractor never materializes one), chmod follows it and changes + // the mode of whatever file it points at, including files outside + // node_modules. The chmod must be skipped when the bin target is a symlink. + using dir = tempDir("bin-target-symlink-test", { + // Pin the hoisted linker so the bin link lands at node_modules/.bin and + // the chmod runs against the package's own bin target. + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-symlink-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-symlinked-bin", + version: "1.0.0", + bin: { "dep-with-symlinked-bin": "./payload" }, + }), + "victim.txt": "do not make me executable", + }); + const installDir = String(dir); - const victimDir = join(installDir, "victim-dir"); - await chmod(victimDir, 0o700); - await symlink(join("..", "..", "victim-dir"), join(installDir, "packages", "dep", "payload")); + const victimPath = join(installDir, "victim.txt"); + await chmod(victimPath, 0o600); + // The bin target is a symlink whose destination is outside the package + // directory. + await symlink(join("..", "..", "victim.txt"), join(installDir, "packages", "dep", "payload")); - await using proc = spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const proc = spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); - expect((await stat(victimDir)).mode & 0o777).toBe(0o700); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - // Without the slash this target links (see above), so with it the result is the same. - const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-dir-bin"); - expect(await readlink(binLink)).toBe(join("..", "dep-with-symlinked-dir-bin", "payload")); + // The file the symlink points at keeps its original permissions; the + // installer must not chmod through a symlinked bin target. + expect((await stat(victimPath)).mode & 0o777).toBe(0o600); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); - }, - 60000, -); - -it.skipIf(isWindows)( - "skips a package bin entry whose name contains a NUL byte and links the remaining entries", - async () => { - using dir = tempDir("bin-name-nul-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-name-nul-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-nul-bin", - version: "1.0.0", - bin: { ["extra" + String.fromCharCode(0) + "ignoredtail"]: "./cli.js", "good-bin": "./cli.js" }, - }), - "packages/dep/cli.js": `#!/usr/bin/env node\nconsole.log("ok");\n`, - }); - const installDir = String(dir); + // The bin link itself is still created. + const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-bin"); + expect((await lstat(binLink)).isSymbolicLink()).toBe(true); - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); +}, 60000); + +it("does not change permissions of a directory reached through a symlinked bin target with a trailing slash", async () => { + // Same as above, but the bin target is written as "payload/". The kernel + // follows a symlink named with a trailing slash even when chmod is told not + // to follow symlinks, so the installer has to drop the slash before it + // stats, links and chmods the target. + using dir = tempDir("bin-target-symlink-trailing-slash-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-symlink-slash-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-symlinked-dir-bin", + version: "1.0.0", + bin: { "dep-with-symlinked-dir-bin": "./payload/" }, + }), + "victim-dir/keep": "", + }); + const installDir = String(dir); - expect((await readdir(join(installDir, "node_modules", ".bin"))).sort()).toEqual(["good-bin"]); + const victimDir = join(installDir, "victim-dir"); + await chmod(victimDir, 0o700); + await symlink(join("..", "..", "victim-dir"), join(installDir, "packages", "dep", "payload")); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); - }, - 60000, -); - -it.skipIf(isWindows)( - "does not link a bin target that resolves outside the package through a symlinked directory", - async () => { - using dir = tempDir("bin-target-symlinked-dir-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-dir-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-linked-dir-bin", - version: "1.0.0", - bin: { "linked-dir-tool": "lnk/tool.js" }, - }), - }); - const installDir = await realpath(String(dir)); + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - const outsideDir = `${installDir}/abcdefghijkl${installDir}/packages/dep/y`; - await mkdir(outsideDir, { recursive: true }); - const toolPath = join(outsideDir, "tool.js"); - await writeFile(toolPath, `#!/usr/bin/env node\nconsole.log("ok");\n`); - await chmod(toolPath, 0o600); - await symlink(outsideDir, join(installDir, "packages", "dep", "lnk")); + expect((await stat(victimDir)).mode & 0o777).toBe(0o700); - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // Without the slash this target links (see above), so with it the result is the same. + const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-dir-bin"); + expect(await readlink(binLink)).toBe(join("..", "dep-with-symlinked-dir-bin", "payload")); - expect((await stat(toolPath)).mode & 0o777).toBe(0o600); - expect(await readdir(join(installDir, "node_modules", ".bin")).catch(() => [])).toEqual([]); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); +}, 60000); + +it("skips a package bin entry whose name contains a NUL byte and links the remaining entries", async () => { + using dir = tempDir("bin-name-nul-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-name-nul-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-nul-bin", + version: "1.0.0", + bin: { ["extra" + String.fromCharCode(0) + "ignoredtail"]: "./cli.js", "good-bin": "./cli.js" }, + }), + "packages/dep/cli.js": `#!/usr/bin/env node\nconsole.log("ok");\n`, + }); + const installDir = String(dir); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); - }, - 60000, -); + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect((await readdir(join(installDir, "node_modules", ".bin"))).sort()).toEqual(["good-bin"]); + + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); +}, 60000); + +it("does not link a bin target that resolves outside the package through a symlinked directory", async () => { + using dir = tempDir("bin-target-symlinked-dir-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-dir-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-linked-dir-bin", + version: "1.0.0", + bin: { "linked-dir-tool": "lnk/tool.js" }, + }), + }); + const installDir = await realpath(String(dir)); + + const outsideDir = `${installDir}/abcdefghijkl${installDir}/packages/dep/y`; + await mkdir(outsideDir, { recursive: true }); + const toolPath = join(outsideDir, "tool.js"); + await writeFile(toolPath, `#!/usr/bin/env node\nconsole.log("ok");\n`); + await chmod(toolPath, 0o600); + await symlink(outsideDir, join(installDir, "packages", "dep", "lnk")); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect((await stat(toolPath)).mode & 0o777).toBe(0o600); + expect(await readdir(join(installDir, "node_modules", ".bin")).catch(() => [])).toEqual([]); + + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); +}, 60000); // The cache folder and the bun.lock entry of a `github:` package are named after the root // directory of its tarball: the first component of the first entry's path, whatever that // entry is. A tarball with a Content-Length and a chunked one go through different readers. -describe.concurrent.skipIf(isWindows)("the root directory of a GitHub tarball", () => { +describe.concurrent("the root directory of a GitHub tarball", () => { const readers = ["buffered", "streaming"] as const; async function install(reader: (typeof readers)[number], root: string, first: string[] = []) { @@ -1305,7 +1285,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { }); // People do link the project's own `node_modules` (a Docker volume, Nix, Bazel). - it.skipIf(isWindows)("still installs through a link at the project's own node_modules", async () => { + it("still installs through a link at the project's own node_modules", async () => { using root = tempDir("nm-root", {}); const volume = join(String(root), "volume"); const repo = join(String(root), "repo"); @@ -1350,7 +1330,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { } // People link every `node_modules` of a monorepo to a volume or a cache disk, not only the root's. - it.skipIf(isWindows)("still installs through a link at a workspace member's node_modules", async () => { + it("still installs through a link at a workspace member's node_modules", async () => { using root = tempDir("nm-member", {}); const volume = join(String(root), "volume"); await mkdir(volume); @@ -1367,7 +1347,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { }); // `lodash` is found installed through the link before `zod` is written. - it.skipIf(isWindows)("keeps what's behind a workspace's link when one package changes", async () => { + it("keeps what's behind a workspace's link when one package changes", async () => { using root = tempDir("nm-member-change", {}); const repo = await workspaceWithNestedDep(String(root)); for (const major of [1, 2, 3]) { @@ -1399,7 +1379,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { expect(exitCode).toBe(0); }); - it.skipIf(isWindows)("keeps a workspace member's link when it replaces the isolated linker's tree", async () => { + it("keeps a workspace member's link when it replaces the isolated linker's tree", async () => { using root = tempDir("nm-member-switch", {}); const repo = await workspaceWithNestedDep(String(root)); expect((await runBun(repo, ["install", "--ignore-scripts", "--linker", "isolated"])).exitCode).toBe(0); @@ -1474,7 +1454,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { // A `bin` key is a free-form file name. Through a link at `.bin`, a regular file of that // name was unlinked and replaced by a link that dangles there. - it.skipIf(isWindows)("does not replace files in the target of node_modules/.bin", async () => { + it("does not replace files in the target of node_modules/.bin", async () => { using dir = tempDir("nm-bin-files", { "package.json": JSON.stringify({ name: "bin-dir-symlink-app", @@ -1512,7 +1492,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { // The links are planted after the install, so only `bun remove` itself is between its deletes and the victim. It // deletes the leftover `@scope/` and the dangling links in `.bin`. - it.skipIf(isWindows)("bun remove does not delete through node_modules/@scope or .bin", async () => { + it("bun remove does not delete through node_modules/@scope or .bin", async () => { using dir = tempDir("nm-remove-planted", { "package.json": JSON.stringify({ name: "remove-scope-symlink-app", @@ -1659,7 +1639,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { }); // The victim has every file an installed package is known by. Linking a bin sets the mode of its file. - it.skipIf(isWindows).each([ + it.each([ ["node_modules", ["@x", "dep"]], ["node_modules/@scope", ["dep"]], ["node_modules/@scope/", []], @@ -1706,7 +1686,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { // People link every `node_modules` of a monorepo to a volume or a cache disk, not only the root's. // A first install resets the `node_modules` of the members, but not with `--filter`. - it.skipIf(isWindows).each([ + it.each([ ["", []], [" with --filter", ["--filter", "evil"]], ] as const)("still installs through a link at a workspace member's node_modules%s", async (_, args) => { @@ -1792,7 +1772,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { } // People do link the project's own `node_modules` (a Docker volume, Nix, Bazel). - it.skipIf(isWindows)("still installs through a link at the project's own node_modules", async () => { + it("still installs through a link at the project's own node_modules", async () => { using root = tempDir("nm-root", {}); const volume = join(String(root), "volume"); await mkdir(volume); @@ -1976,7 +1956,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { // A link at the `node_modules` of the project or of a workspace is followed: people keep theirs on a // volume or a cache disk. Not all that is behind such a link need be bun's. -describe.concurrent.skipIf(isWindows)("the target of a link at node_modules", () => { +describe.concurrent("the target of a link at node_modules", () => { const MINE: Record = { "notes.txt": KEEP, ".hidden": KEEP, diff --git a/test/cli/lint/config-files.test.ts b/test/cli/lint/config-files.test.ts index fd4e36209e43..ca650745abf9 100644 --- a/test/cli/lint/config-files.test.ts +++ b/test/cli/lint/config-files.test.ts @@ -1280,7 +1280,7 @@ test.concurrent.each([ }); // Same as ESLint 10.12.0 and oxlint 1.87.0, which load a config in Node. On Windows the preload still runs. -test.skipIf(isWindows).each([ +test.each([ ["eslint.config.mjs", (rules: string) => `export default [{ rules: ${rules} }];`], ["oxlint.config.ts", (rules: string) => `export default { categories: { correctness: "off" }, rules: ${rules} };`], ])("a `preload` in bunfig.toml doesn't run before %s", async (name, config) => { @@ -1360,7 +1360,7 @@ describe.concurrent("`process.argv` in a config file is bun lint's command line" }); // It isn't passed on the child process's command line or in its environment, because there's no room for it twice. - test.skipIf(isWindows)("works with a one-megabyte command line", async () => { + test("works with a one-megabyte command line", async () => { const names = Array.from({ length: 15_000 }, (_, index) => `a${index}`.padEnd(40, "b")); const args = [...names.flatMap(it => ["--global", it]), "a.js"]; const rules = `{ eqeqeq: process.argv.length === ${1 + before.length + args.length} ? "error" : "off" }`; @@ -5819,7 +5819,7 @@ describe.concurrent("a config that asks for something unsupported", () => { }); // Windows doesn't allow these file names. - test.skipIf(isWindows)("escapes control characters in printed file names", async () => { + test("escapes control characters in printed file names", async () => { const files = { "eslint.config.mjs": `export default [ { files: ["**/*.one"], plugins: { p: { languages: { l: {} } } }, language: "p/l" }, @@ -6597,7 +6597,7 @@ describe.skipIf(!hasRunner)("Windows paths, on every platform", () => { // This is what ESLint 10.12.0, ESLint 8.57.1 and oxlint 1.87.0 do with the same files. Without a config file, bun lint // behaves like oxlint. -describe.skipIf(isWindows).concurrent("a symlink that's dangling or points to itself or a parent directory", () => { +describe.concurrent("a symlink that's dangling or points to itself or a parent directory", () => { const flavors: Record> = { "eslint.config.js": { "eslint.config.js": `export default [${JSON.stringify(noVar)}];` }, ".eslintrc.json": { ".eslintrc.json": rc(noVar) }, @@ -7092,7 +7092,7 @@ describe.concurrent("a repository's info/exclude, without an oxlint config", () }); // Uses a file with mode 000, which root could still open. -describe.skipIf(isWindows || process.getuid?.() === 0).concurrent("a file that can't be opened", () => { +describe.skipIf(process.getuid?.() === 0).concurrent("a file that can't be opened", () => { const files = { "a.js": code, "locked.js": code, "sub/b.js": code }; const before = (dir: string) => chmodSync(join(dir, "locked.js"), 0); diff --git a/test/cli/lint/conformance.test.ts b/test/cli/lint/conformance.test.ts index a4d6969e4971..6668114b5789 100644 --- a/test/cli/lint/conformance.test.ts +++ b/test/cli/lint/conformance.test.ts @@ -75,7 +75,7 @@ test.skipIf(!hasRunner).each(suites)( // No more cases are skipped than expected. A case that needs a rule defined in upstream's test file only counts as // skipped if the sample includes it. const skipped = (text: string) => Number(/, (\d+) skipped\n/.exec(text)?.[1]); - if (!isWindows) expect(skipped(stdout)).toBeLessThanOrEqual(skipped(expected)); + expect(skipped(stdout)).toBeLessThanOrEqual(skipped(expected)); // No more messages are missing oxlint's help text than expected. const missingHelp = (text: string) => Number(/\n(\d+) messages lack the help/.exec(text)?.[1] ?? 0); expect(missingHelp(stdout)).toBeLessThanOrEqual(missingHelp(expected)); diff --git a/test/cli/lint/explain.test.ts b/test/cli/lint/explain.test.ts index bd78c171ff9e..c80a3f6fb3a8 100644 --- a/test/cli/lint/explain.test.ts +++ b/test/cli/lint/explain.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "fs"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "path"; const pagesDir = join(import.meta.dir, "../../../docs/lint/rules"); @@ -222,7 +222,7 @@ describe.concurrent("--rules", () => { expect(exitCode).toBe(0); }); - test.skipIf(isWindows)("groups rules by plugin and adds descriptions in a terminal", async () => { + test("groups rules by plugin and adds descriptions in a terminal", async () => { const [{ output, exitCode }, rules] = await Promise.all([lintInTerminal(["--rules"]), listRules()]); const lines = output.split("\n"); const count = (scope: string) => rules.filter(it => it.scope === scope).length; @@ -374,7 +374,7 @@ describe.concurrent("--explain", () => { expect(Bun.stripANSI(colored.stdout)).toContain("no-var"); }); - test.skipIf(isWindows)("the URL is a hyperlink in a terminal", async () => { + test("the URL is a hyperlink in a terminal", async () => { const { output, exitCode } = await lintInTerminal(["--explain", "no-var"], { NO_COLOR: undefined }); expect(output).toContain(`\x1b]8;;${base}eslint/no-var\x1b\\`); expect(exitCode).toBe(0); diff --git a/test/cli/lint/git.test.ts b/test/cli/lint/git.test.ts index eb0e7fe36196..077f5af844f2 100644 --- a/test/cli/lint/git.test.ts +++ b/test/cli/lint/git.test.ts @@ -290,7 +290,7 @@ describe.concurrent.each(commands)("bun %s", command => { await commit(join(root, "inner")); write(root, "inner/a.js", before); await git(join(root, "inner"), "add", "-A"); - if (!isWindows) symlinkSync("a.js", join(root, "link.js")); + symlinkSync("a.js", join(root, "link.js")); write(root, "b.js", before); await git(root, "add", "-A"); @@ -301,45 +301,42 @@ describe.concurrent.each(commands)("bun %s", command => { }); // On macOS /tmp is such a link, and so is $PWD in a shell that got to the repository through one. - test.skipIf(isWindows)( - "files that are named through a link to the repository are selected, written and staged", - async () => { - using repo = await repository({ "src/a.js": tidy, "src/partly.js": tidy, "b.js": tidy }); - const { root } = repo; - using outside = tempDir("bun-lint-git-link", {}); - // The link is above the repository. `bun fmt` doesn't take an argument that is itself a link, like Prettier. - symlinkSync(join(root, ".."), join(String(outside), "link")); - const link = join(realpathSync(String(outside)), "link", basename(root)); - for (const name of ["src/a.js", "src/partly.js"]) write(root, name, before); - await git(root, "add", "-A"); - write(root, "src/partly.js", before + "\n"); - write(root, "b.js", before); - - // The names are printed relative to the real working directory, and go through the link. - const found = async (cwd: string, args: string[]) => { - const { stdout, stderr, exitCode } = await bun(cwd, [command, ...args, "--list-files"]); - expect({ exitCode, stderr: exitCode === 0 ? "" : stderr }).toEqual({ exitCode: 0, stderr: "" }); - const names = stdout.split("\n").filter(it => it !== ""); - return names.map(it => relative(root, realpathSync(resolve(root, it)))).sort(); - }; - for (const cwd of [root, link]) { - for (const flag of ["--staged", "--changed"]) { - const also = flag === "--changed" ? ["b.js"] : []; - expect(await found(cwd, [flag, link])).toEqual([...also, "src/a.js", "src/partly.js"]); - expect(await found(cwd, [flag, join(link, "src")])).toEqual(["src/a.js", "src/partly.js"]); - expect(await found(cwd, [flag, join(link, "src/a.js")])).toEqual(["src/a.js"]); - } + test("files that are named through a link to the repository are selected, written and staged", async () => { + using repo = await repository({ "src/a.js": tidy, "src/partly.js": tidy, "b.js": tidy }); + const { root } = repo; + using outside = tempDir("bun-lint-git-link", {}); + // The link is above the repository. `bun fmt` doesn't take an argument that is itself a link, like Prettier. + symlinkSync(join(root, ".."), join(String(outside), "link")); + const link = join(realpathSync(String(outside)), "link", basename(root)); + for (const name of ["src/a.js", "src/partly.js"]) write(root, name, before); + await git(root, "add", "-A"); + write(root, "src/partly.js", before + "\n"); + write(root, "b.js", before); + + // The names are printed relative to the real working directory, and go through the link. + const found = async (cwd: string, args: string[]) => { + const { stdout, stderr, exitCode } = await bun(cwd, [command, ...args, "--list-files"]); + expect({ exitCode, stderr: exitCode === 0 ? "" : stderr }).toEqual({ exitCode: 0, stderr: "" }); + const names = stdout.split("\n").filter(it => it !== ""); + return names.map(it => relative(root, realpathSync(resolve(root, it)))).sort(); + }; + for (const cwd of [root, link]) { + for (const flag of ["--staged", "--changed"]) { + const also = flag === "--changed" ? ["b.js"] : []; + expect(await found(cwd, [flag, link])).toEqual([...also, "src/a.js", "src/partly.js"]); + expect(await found(cwd, [flag, join(link, "src")])).toEqual(["src/a.js", "src/partly.js"]); + expect(await found(cwd, [flag, join(link, "src/a.js")])).toEqual(["src/a.js"]); } + } - const { stderr, exitCode } = await bun(root, [...writing[command], "--staged", join(link, "src")]); - expect(stderr).toContain("src/partly.js has both staged and unstaged changes"); - expect(exitCode).toBe(2); - expect(read(root, "src/a.js")).toBe(after[command]); - expect(read(root, "src/partly.js")).toBe(before + "\n"); - // a.js was staged again, so only what was unstaged before still is. - expect(await git(root, "diff", "--name-only")).toBe("b.js\nsrc/partly.js\n"); - }, - ); + const { stderr, exitCode } = await bun(root, [...writing[command], "--staged", join(link, "src")]); + expect(stderr).toContain("src/partly.js has both staged and unstaged changes"); + expect(exitCode).toBe(2); + expect(read(root, "src/a.js")).toBe(after[command]); + expect(read(root, "src/partly.js")).toBe(before + "\n"); + // a.js was staged again, so only what was unstaged before still is. + expect(await git(root, "diff", "--name-only")).toBe("b.js\nsrc/partly.js\n"); + }); test("a fully staged file is staged again after it's rewritten", async () => { using repo = await repository({ "a.js": tidy, "sub/b.js": tidy, "c.js": tidy }); @@ -444,7 +441,7 @@ describe.concurrent.each(commands)("bun %s", command => { expect(await git(root, "show", ":sub/bad.js")).toBe("\n" + before); }); - test.skipIf(isWindows)("works in a pre-commit hook, on the index of that commit", async () => { + test("works in a pre-commit hook, on the index of that commit", async () => { using repo = await repository({ "a.js": tidy, "b.js": tidy }); const { root } = repo; const hook = join(root, ".git/hooks/pre-commit"); @@ -463,7 +460,7 @@ describe.concurrent.each(commands)("bun %s", command => { expect(await git(root, "status", "--porcelain")).toBe(""); }); - test.skipIf(isWindows)("git never runs programs named in the repository's config", async () => { + test("git never runs programs named in the repository's config", async () => { using repo = await repository({ "a.js": tidy, "b.js": tidy, ".gitattributes": "*.js diff=mine\n" }); const { root } = repo; const program = join(root, "program.sh"); @@ -643,33 +640,25 @@ describe.concurrent("bun lint --staged reads the staged version of a partly stag expect(exitCode).toBe(1); }); - test.skipIf(isWindows)( - "for a file passed through a symlinked path, and a package.json with a byte order mark", - async () => { - const module = { - "package.json": '\uFEFF{ "type": "module" }', - "tsconfig.json": '{ "compilerOptions": { "module": "nodenext", "lib": ["es2015"], "types": [] } }', - }; - using repo = await repository({ ...module, "a.ts": tidy }); - const { root } = repo; - write(root, "a.ts", "export async function f(a: number) {\n await a;\n return import.meta;\n}\n"); - write(root, "package.json", module["package.json"] + "\n"); - await git(root, "add", "-A"); - write(root, "a.ts", tidy); - write(root, "package.json", module["package.json"]); - using links = tempDir("bun-lint-git-link", {}); - symlinkSync(root, join(String(links), "link")); - const { stdout, exitCode } = await bun(root, [ - "lint", - "--staged", - ...flags, - join(String(links), "link", "a.ts"), - ]); - expect(stdout).toContain("a.ts:2:3: "); - expect(stdout).toMatch(/^1 problem$/m); - expect(exitCode).toBe(1); - }, - ); + test("for a file passed through a symlinked path, and a package.json with a byte order mark", async () => { + const module = { + "package.json": '\uFEFF{ "type": "module" }', + "tsconfig.json": '{ "compilerOptions": { "module": "nodenext", "lib": ["es2015"], "types": [] } }', + }; + using repo = await repository({ ...module, "a.ts": tidy }); + const { root } = repo; + write(root, "a.ts", "export async function f(a: number) {\n await a;\n return import.meta;\n}\n"); + write(root, "package.json", module["package.json"] + "\n"); + await git(root, "add", "-A"); + write(root, "a.ts", tidy); + write(root, "package.json", module["package.json"]); + using links = tempDir("bun-lint-git-link", {}); + symlinkSync(root, join(String(links), "link")); + const { stdout, exitCode } = await bun(root, ["lint", "--staged", ...flags, join(String(links), "link", "a.ts")]); + expect(stdout).toContain("a.ts:2:3: "); + expect(stdout).toMatch(/^1 problem$/m); + expect(exitCode).toBe(1); + }); }); test("for a file that no tsconfig.json includes", async () => { @@ -797,7 +786,7 @@ describe.concurrent("bun lint --staged lints what will be committed", () => { expect(await git(repo.root, "show", ":gone.js")).toBe(before); }); - test.skipIf(isWindows)("a staged file that was replaced by a symlink on disk is linted", async () => { + test("a staged file that was replaced by a symlink on disk is linted", async () => { using repo = await repository({ "target.js": clean }); write(repo.root, "f.js", broken); await git(repo.root, "add", "-A"); @@ -946,20 +935,17 @@ test.concurrent("bun lint --staged reads a conflicted file from disk", async () expect(exitCode).toBe(0); }); -test.skipIf(isWindows).concurrent.each(commands)( - "bun %s --staged skips a file that is staged as a symlink", - async command => { - using repo = await repository({ "target.js": tidy, "a.js": tidy, "b.js": tidy }); - const { root } = repo; - rmSync(join(root, "a.js")); - symlinkSync("target.js", join(root, "a.js")); - write(root, "b.js", before); - await git(root, "add", "-A"); - rmSync(join(root, "a.js")); - write(root, "a.js", tidy); - expect(await selected(root, root, [command, "--staged"])).toEqual(["b.js"]); - }, -); +test.concurrent.each(commands)("bun %s --staged skips a file that is staged as a symlink", async command => { + using repo = await repository({ "target.js": tidy, "a.js": tidy, "b.js": tidy }); + const { root } = repo; + rmSync(join(root, "a.js")); + symlinkSync("target.js", join(root, "a.js")); + write(root, "b.js", before); + await git(root, "add", "-A"); + rmSync(join(root, "a.js")); + write(root, "a.js", tidy); + expect(await selected(root, root, [command, "--staged"])).toEqual(["b.js"]); +}); test.concurrent("bun fmt --staged doesn't count a file it skips", async () => { using repo = await repository({ "partly.js": tidy, "whole.js": tidy, "tidy.js": tidy }); diff --git a/test/cli/lint/help.test.ts b/test/cli/lint/help.test.ts index f8958ff7a439..ace206018b96 100644 --- a/test/cli/lint/help.test.ts +++ b/test/cli/lint/help.test.ts @@ -491,7 +491,7 @@ note: package.json has a "${command}" script, so bun ${command} runs that script }, ); - test.skipIf(isWindows)("`..` after a symlink resolves to the directory that contains the symlink", async () => { + test("`..` after a symlink resolves to the directory that contains the symlink", async () => { const files = { "project/package.json": script["package.json"], "else/where/deep/b.ts": source }; const before = (dir: string) => symlinkSync("../else/where/deep", join(dir, "project/link")); expect(await whatRan(files, ["--cwd=link/..", command], "project", { before })).toBe("the script"); @@ -629,7 +629,7 @@ note: package.json has a "${command}" script, so bun ${command} runs that script expect(await whatRan({}, [command], ".", { before })).toBe("the script"); }); - describe.skipIf(isWindows)("a package.json that isn't a readable file", () => { + describe("a package.json that isn't a readable file", () => { const root = { "package.json": `{ "scripts": { "${command}": "echo the script" } }` }; const link = (to: string) => (dir: string) => symlinkSync(to, join(dir, "sub/package.json")); const pipe = (dir: string) => expect(Bun.spawnSync(["mkfifo", join(dir, "sub/package.json")]).exitCode).toBe(0); @@ -685,7 +685,7 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} }); }); - test.skipIf(isWindows)("a backslash in a directory name isn't a separator", async () => { + test("a backslash in a directory name isn't a separator", async () => { const { stdout, exitCode } = await bun( { "a\\b/package.json": script["package.json"] }, [command, "--help"], @@ -697,7 +697,7 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} // Skipped for `bun check`. Like TypeScript, it treats a backslash as a separator everywhere, so it can't find files // in a directory like this. - describe.skipIf(isWindows || command === "check")("a backslash in a directory name with --cwd", () => { + describe.skipIf(command === "check")("a backslash in a directory name with --cwd", () => { // The directory that `a\b` would resolve to if the backslash were a separator. const other = { "a/b/other.ts": source, "a/b/sub/other.ts": source }; @@ -724,13 +724,13 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} expect(await whatRan(files, args, "apps/web", { from: "." })).toBe("the command"); }); - test.skipIf(isWindows || command === "check")("in `a\\b`, --cwd=missing doesn't resolve to a/b/missing", async () => { + test.skipIf(command === "check")("in `a\\b`, --cwd=missing doesn't resolve to a/b/missing", async () => { const result = await bun({ "a/b/missing/a.ts": source }, ["--cwd=missing", command, ...flags, "a.ts"], "a\\b"); expect(result.stderr).toContain('Could not change directory to "missing"'); expect(result.exitCode).toBe(1); }); - test.skipIf(isWindows)("an absolute --cwd works when the working directory was deleted", async () => { + test("an absolute --cwd works when the working directory was deleted", async () => { using dir = tempDir("bun-no-script", { "project/a.ts": source, "deleted/a.ts": "" }); const args = [`--cwd=${join(String(dir), "project")}`, command, ...flags, "a.ts"]; await using proc = Bun.spawn({ @@ -744,7 +744,7 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} expect(exitCode).toBe(1); }); - test.skipIf(isWindows)("an absolute --cwd can contain `//`, `.` and `..`", async () => { + test("an absolute --cwd can contain `//`, `.` and `..`", async () => { using dir = tempDir("bun-no-script", { "project/a.ts": source }); const cwd = `/..//${String(dir).slice(1)}/./missing/../project//`; await using proc = Bun.spawn({ diff --git a/test/cli/lint/init.test.ts b/test/cli/lint/init.test.ts index 75fb5519a009..c896ad7513b6 100644 --- a/test/cli/lint/init.test.ts +++ b/test/cli/lint/init.test.ts @@ -1,6 +1,6 @@ // Tests for `bun lint --init` and `bun fmt --init`. import { afterAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { chmodSync, existsSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { endChildren, spawn } from "../children"; @@ -153,7 +153,7 @@ describe.concurrent.each(["lint", "fmt"] as const)("bun %s --init", command => { expect(readFileSync(join(String(dir), "bunfig.toml"), "utf8")).toBe("a = 1\nb = \n"); }); - const cannotBeKeptFromWriting = isWindows || process.getuid?.() === 0; + const cannotBeKeptFromWriting = process.getuid?.() === 0; test.skipIf(cannotBeKeptFromWriting)("a read-only bunfig.toml is an error", async () => { using dir = tempDir("bun-init-section", { "bunfig.toml": "a = 1\n" }); @@ -203,7 +203,7 @@ describe.concurrent.each(["lint", "fmt"] as const)("bun %s --init", command => { } }); - describe.skipIf(isWindows)("a bunfig.toml that is a symlink", () => { + describe("a bunfig.toml that is a symlink", () => { const project = { "project/.git/HEAD": "ref: refs/heads/main\n", "project/a.js": "a;\n" }; test("isn't written if it points outside the repository", async () => { diff --git a/test/cli/lint/lint.test.ts b/test/cli/lint/lint.test.ts index f3c250536739..791423c0d527 100644 --- a/test/cli/lint/lint.test.ts +++ b/test/cli/lint/lint.test.ts @@ -1419,7 +1419,7 @@ describe.concurrent("bun lint", () => { }); }); - test.skipIf(isWindows)("a named pipe whose name ends in .js is skipped", async () => { + test("a named pipe whose name ends in .js is skipped", async () => { const { stdout, exitCode } = await lint({ "eslint.config.js": basic, "a.js": bad }, ["-f", "unix", "."], { before: dir => expect(Bun.spawnSync(["mkfifo", join(dir, "pipe.js")]).exitCode).toBe(0), }); @@ -1429,7 +1429,7 @@ describe.concurrent("bun lint", () => { }); // Opening a pipe like this blocks until a writer opens it. - test.skipIf(isWindows).each([".gitignore", ".git/info/exclude", ".eslintignore", "package.json"])( + test.each([".gitignore", ".git/info/exclude", ".eslintignore", "package.json"])( "doesn't hang when %s is a link to a named pipe without a writer", async name => { const { raw } = await lint({ "project/a.js": bad, "project/.git/info/a": "" }, ["-f", "json", "."], { @@ -1443,7 +1443,7 @@ describe.concurrent("bun lint", () => { }, ); - describe.skipIf(isWindows)("a named pipe passed to a flag", () => { + describe("a named pipe passed to a flag", () => { const makePipes = (dir: string) => expect(Bun.spawnSync(["mkfifo", join(dir, "pipe"), join(dir, "pipe.json")]).exitCode).toBe(0); @@ -1543,7 +1543,7 @@ describe.concurrent("bun lint", () => { }); // git stores a link to `/dev/zero` as is, so a repository can contain one. - test.skipIf(isWindows).each([ + test.each([ ["eslint.config.js", basic], [".oxlintrc.json", "{}"], ])("a link to a device whose name ends in .js is skipped: %s", async (name, text) => { @@ -7256,18 +7256,15 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, }); // ESLint doesn't print a report in that case. - test.skipIf(process.getuid?.() === 0 || isWindows)( - "still prints the report if a file can't be written", - async () => { - const files = { "eslint.config.js": basic, "a.js": "var a = 1;\nexport { a };\n", "b.js": "debugger;\n" }; - const before = (dir: string) => chmodSync(join(dir, "a.js"), 0o444); - const result = await lint(files, ["--fix", "-f", "unix", "a.js", "b.js"], { reads: ["a.js"], before }); - expect(result.files["a.js"]).toBe(files["a.js"]); - expect(result.stdout).toContain("[Error/no-debugger]"); - expect(result.stderr).toMatch(/Cannot write .*a\.js: (EACCES|EPERM)/); - expect(result.exitCode).toBe(2); - }, - ); + test.skipIf(process.getuid?.() === 0)("still prints the report if a file can't be written", async () => { + const files = { "eslint.config.js": basic, "a.js": "var a = 1;\nexport { a };\n", "b.js": "debugger;\n" }; + const before = (dir: string) => chmodSync(join(dir, "a.js"), 0o444); + const result = await lint(files, ["--fix", "-f", "unix", "a.js", "b.js"], { reads: ["a.js"], before }); + expect(result.files["a.js"]).toBe(files["a.js"]); + expect(result.stdout).toContain("[Error/no-debugger]"); + expect(result.stderr).toMatch(/Cannot write .*a\.js: (EACCES|EPERM)/); + expect(result.exitCode).toBe(2); + }); const files = { "eslint.config.js": basic, "a.js": bad, "b.js": "export const b = 1;\n" }; @@ -7989,7 +7986,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, expect(result.files).toEqual({ "a.js": `\uFEFF${bad}` }); }); - test.skipIf(isWindows)("writes through a symlink instead of replacing it", async () => { + test("writes through a symlink instead of replacing it", async () => { const result = await lint(files, ["--fix", "link.js"], { reads: ["a.js", "link.js"], before: dir => symlinkSync("a.js", join(dir, "link.js")), @@ -8027,7 +8024,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, }); // A new file would get fewer permission bits, unless the umask is 0. - test.skipIf(isWindows)("keeps the file's mode regardless of the umask", async () => { + test("keeps the file's mode regardless of the umask", async () => { using dir = tempDir("bun-lint", { ...files, "b.js": files["a.js"] }); chmodSync(join(String(dir), "a.js"), 0o664); chmodSync(join(String(dir), "b.js"), 0o777); @@ -8098,7 +8095,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, }); // ESLint doesn't need write access to the directory. - test.skipIf(isWindows || isRoot)("fixes a file in a read-only directory", async () => { + test.skipIf(isRoot)("fixes a file in a read-only directory", async () => { using dir = tempDir("bun-lint", { "eslint.config.js": files["eslint.config.js"], "src/a.js": files["a.js"] }); chmodSync(join(String(dir), "src"), 0o555); try { @@ -8582,7 +8579,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, const prunes = flag === "--prune-suppressions"; const missingFileError = prunes ? "The suppressions file does not exist" : symlinkError; - test.skipIf(isWindows)("refuses to write through a symlink that points outside the repository", async () => { + test("refuses to write through a symlink that points outside the repository", async () => { const options = { cwd: "project", before: link("project/f", "outside/f"), reads: ["outside/f"], mayFail: true }; const [toFile, toNothing] = await Promise.all([ lint({ ...tree, "outside/f": before }, flags("f"), options), @@ -8632,7 +8629,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, expect(result.exitCode).toBe(exitCode); }); - test.skipIf(isWindows)("writes through a symlink that points inside the repository and keeps it", async () => { + test("writes through a symlink that points inside the repository and keeps it", async () => { using dir = tempDir("bun-lint", { ...tree, [prunes ? "project/real/f" : "project/real/other"]: before }); link("project/f", "project/real/f")(String(dir)); const result = await run(join(String(dir), "project"), flags("f")); @@ -8642,7 +8639,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, }); }); - test.skipIf(isWindows)("refuses the default suppressions file if it links outside the repository", async () => { + test("refuses the default suppressions file if it links outside the repository", async () => { const name = "eslint-suppressions.json"; const options = { cwd: "project", @@ -8663,7 +8660,7 @@ export default { meta: { name: "q" }, rules: { marks, marked } };\n`, } }); - describe.skipIf(isWindows)("--output-file is a pipe or a device", () => { + describe("--output-file is a pipe or a device", () => { const withOutside = { ...tree, "outside/other": "" }; /** Runs `bun lint -f unix -o ` in `dir`'s project through `sh`. `"$@"` in `script` is the command. */ @@ -11860,41 +11857,35 @@ describe.concurrent("a script, file or executable named lint", () => { }); // `bun run` adds that directory to PATH. - test.skipIf(isWindows)( - "a lint executable next to package.json wins over the linter, even from a subdirectory", - async () => { - using dir = tempDir("bun-lint-name", { - ...files, - "package.json": "{}", - "lint": `#!/bin/sh\necho the executable\n`, - "sub/b.js": "debugger;\n", - }); - chmodSync(join(String(dir), "lint"), 0o755); - const cwd = join(String(dir), "sub"); - await using proc = spawn({ cmd: [bunExe(), "lint"], env, cwd, stdout: "pipe", stderr: "pipe" }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout.trim()).toBe("the executable"); - expect(exitCode).toBe(0); - }, - ); + test("a lint executable next to package.json wins over the linter, even from a subdirectory", async () => { + using dir = tempDir("bun-lint-name", { + ...files, + "package.json": "{}", + "lint": `#!/bin/sh\necho the executable\n`, + "sub/b.js": "debugger;\n", + }); + chmodSync(join(String(dir), "lint"), 0o755); + const cwd = join(String(dir), "sub"); + await using proc = spawn({ cmd: [bunExe(), "lint"], env, cwd, stdout: "pipe", stderr: "pipe" }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe("the executable"); + expect(exitCode).toBe(0); + }); // git stores a symlink to `/dev/zero` unchanged. `bun run` stops at such a file and finds no script in it. - test.skipIf(isWindows)( - "a package.json that's a symlink to a device has no script, and the parent directory's package.json isn't used", - async () => { - using dir = tempDir("bun-lint-name", { - ...files, - "sub/eslint.config.js": files["eslint.config.js"], - "sub/b.js": "debugger;\n", - }); - symlinkSync("/dev/null", join(String(dir), "sub/package.json")); - const cmd = [bunExe(), "lint", "-f", "unix"]; - await using proc = spawn({ cmd, env, cwd: join(String(dir), "sub"), stdout: "pipe", stderr: "pipe" }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout).toContain("b.js:1:1: "); - expect(exitCode).toBe(1); - }, - ); + test("a package.json that's a symlink to a device has no script, and the parent directory's package.json isn't used", async () => { + using dir = tempDir("bun-lint-name", { + ...files, + "sub/eslint.config.js": files["eslint.config.js"], + "sub/b.js": "debugger;\n", + }); + symlinkSync("/dev/null", join(String(dir), "sub/package.json")); + const cmd = [bunExe(), "lint", "-f", "unix"]; + await using proc = spawn({ cmd, env, cwd: join(String(dir), "sub"), stdout: "pipe", stderr: "pipe" }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toContain("b.js:1:1: "); + expect(exitCode).toBe(1); + }); test("bun --cwd lint lint treats the first lint as the directory", async () => { const { "package.json": __, ...rest } = files; diff --git a/test/cli/run/as-node.test.ts b/test/cli/run/as-node.test.ts index 4d291b398033..5f93b3cff766 100644 --- a/test/cli/run/as-node.test.ts +++ b/test/cli/run/as-node.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { join } from "path"; -import { bunEnv, bunExe, fakeNodeRun, isMacOS, isWindows, tempDir } from "../../harness"; +import { bunEnv, bunExe, fakeNodeRun, isMacOS, tempDir } from "../../harness"; describe("fake node cli", () => { test("the node cli actually works", () => { @@ -32,7 +32,7 @@ describe("fake node cli", () => { expect(fakeNodeRun(temp, "run").stdout).toBe("pass"); }); // A Windows command line is too short for a path longer than the path buffer. - test.skipIf(isWindows)("reports module not found for a relative script path longer than PATH_MAX", () => { + test("reports module not found for a relative script path longer than PATH_MAX", () => { const long = Array(21).fill(Buffer.alloc(200, "a").toString()).join("/") + ".js"; using temp = tempDir("fake-node-long", {}); const { exitCode, stdout, stderr } = Bun.spawnSync({ diff --git a/test/cli/run/cpu-prof.test.ts b/test/cli/run/cpu-prof.test.ts index 75df02a8a199..14d402a8fb2c 100644 --- a/test/cli/run/cpu-prof.test.ts +++ b/test/cli/run/cpu-prof.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readdirSync, readFileSync } from "fs"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "path"; // Every workload below is time-bounded for 100ms. On Windows JSC's @@ -130,7 +130,7 @@ describe.concurrent("--cpu-prof", () => { // POSIX 5000 bytes exceeds the fixed path buffer (Linux 4096, macOS 1024); // 2500 + 2500 exercises the combined bound with components that fit // individually on Linux. - test.skipIf(isWindows).each([ + test.each([ ["--cpu-prof-dir", ["--cpu-prof-dir", Buffer.alloc(5000, "d").toString()]], ["--cpu-prof-name", ["--cpu-prof-name", Buffer.alloc(5000, "n").toString()]], [ @@ -446,7 +446,7 @@ describe.concurrent("--cpu-prof", () => { }); // "%80" decodes to a byte that isn't valid UTF-8. Windows decodes it to U+FFFD. - test.skipIf(isWindows)("--cpu-prof-md prints a source's URL when the URL can't be converted to a path", async () => { + test("--cpu-prof-md prints a source's URL when the URL can't be converted to a path", async () => { using dir = tempDir("cpu-prof-md-source-url", { "test.js": ` const loop = require("node:vm").runInThisContext( diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 4b0f401c9d90..9ef05a3f5857 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -781,7 +781,7 @@ describe.concurrent(".env that is not a regular file", () => { // The resolver's directory listing drops a FIFO entry, so the loader only // sees one through a symlink. Without O_NONBLOCK the open blocks until a // writer appears. - test.skipIf(isWindows)("FIFO behind a symlink", async () => { + test("FIFO behind a symlink", async () => { using dir = tempDir("dotenv-fifo", files); mkfifo(path.join(String(dir), "fifo")); fs.symlinkSync("fifo", path.join(String(dir), ".env")); @@ -796,7 +796,7 @@ describe.concurrent(".env that is not a regular file", () => { expect(script.exitCode).toBe(0); }); - test.skipIf(isWindows)("unix socket behind a symlink", async () => { + test("unix socket behind a symlink", async () => { using dir = tempDir("dotenv-sock", files); using listener = Bun.listen({ unix: path.join(String(dir), "sock"), @@ -842,7 +842,7 @@ describe.concurrent("--env-file that is not a regular file", () => { // `sh` makes a real pipe. `Bun.spawn({ stdin: "pipe" })` is a socket pair, // which `/dev/stdin` cannot reopen on Linux. - test.skipIf(isWindows)("a pipe through /dev/stdin", async () => { + test("a pipe through /dev/stdin", async () => { using dir = tempDir("dotenv-arg-stdin", files); const script = await run(String(dir), [ @@ -856,7 +856,7 @@ describe.concurrent("--env-file that is not a regular file", () => { expect(script.exitCode).toBe(0); }); - test.skipIf(isWindows)("a pipe in a comma list, and the process env still wins", async () => { + test("a pipe in a comma list, and the process env still wins", async () => { using dir = tempDir("dotenv-arg-stdin-list", files); const script = await run( @@ -874,7 +874,7 @@ describe.concurrent("--env-file that is not a regular file", () => { expect(script.exitCode).toBe(0); }); - test.skipIf(isWindows)("process substitution", async () => { + test("process substitution", async () => { using dir = tempDir("dotenv-arg-procsub", files); const script = await run(String(dir), ["bash", "-c", '"$0" --env-file=<(echo BUNTEST_SUBST=1) index.ts', bunExe()]); @@ -885,7 +885,7 @@ describe.concurrent("--env-file that is not a regular file", () => { // The writer blocks in open(2) until bun opens the FIFO for reading, and bun // blocks in open(2) until a writer arrives. Either order works, as with `cat`. - test.skipIf(isWindows)("a FIFO", async () => { + test("a FIFO", async () => { using dir = tempDir("dotenv-arg-fifo", files); mkfifo(path.join(String(dir), "fifo")); await using writer = Bun.spawn({ @@ -1141,7 +1141,7 @@ test("NODE_ENV default is not propogated in bun run", () => { }); for (const shell of ["system", "bun"]) { - const isWindowsCMD = isWindows && shell === "system"; + const isWindowsCMD = false; const env = { ENV_FILE_NAME: "N/A", @@ -1359,7 +1359,7 @@ test.skipIf(!canUseRunuser)("process.env is preserved when cwd lacks read permis // loader's whole-file read used to `reserve_exact` it and abort the process in // `handle_alloc_error` before any user code ran. It must surface as a // recoverable error. ASAN-only: ASAN rejects the 1 TiB request deterministically. -test.skipIf(!isASAN || isWindows)(".env with a huge lying st_size does not abort the process", async () => { +test.skipIf(!isASAN)(".env with a huge lying st_size does not abort the process", async () => { await using dir = tempDir("dotenv-huge-sparse", { ".env": "", "app.js": `console.log("reached user code");`, diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 031bd4ee5686..383f46fd23c1 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -609,7 +609,7 @@ describe("bun", () => { { signal: constants.signals.SIGUSR1, label: "SIGUSR1" }, ...(isLinux ? [{ signal: 40, label: "UNKNOWN" }] : []), ]; - test.skipIf(isWindows).each(signaled)("exit code of a script killed by signal $signal", ({ signal, label }) => { + test.each(signaled)("exit code of a script killed by signal $signal", ({ signal, label }) => { using dir = tempDir("testworkspace", { dep0: { "package.json": JSON.stringify({ name: "dep0", scripts: { script: `kill -${signal} $$` } }), @@ -755,7 +755,7 @@ describe("bun", () => { }); // The terminal renderer is TTY-only on Windows (see runElideLinesTest). - test.skipIf(isWindows)("terminal output reports how long a successful script took", () => { + test("terminal output reports how long a successful script took", () => { using dir = tempDir("filter-done-in", { packages: { dep0: { @@ -850,7 +850,7 @@ describe("bun", () => { describe("selectors", () => { // A Windows command line is too short for a path longer than the path buffer. - test.skipIf(isWindows)("rejects a path longer than the path buffer", () => { + test("rejects a path longer than the path buffer", () => { const long = "./" + Array(21).fill(Buffer.alloc(200, "a").toString()).join("/"); runInCwdFailure(graph_root, long, "present", /error: --filter "\.\/a.*" path is too long/); }); @@ -1448,7 +1448,7 @@ describe("output timing", () => { // On abort (here: SIGINT), exit alone finishes a script; waiting for pipe // EOF would hang on the detached child that still holds go's stdout. - test.skipIf(isWindows)("SIGINT does not wait for a child holding the script's pipes", async () => { + test("SIGINT does not wait for a child holding the script's pipes", async () => { using dir = tempDir("filter-abort-late", { "package.json": JSON.stringify({ name: "ws-abort", @@ -1565,14 +1565,14 @@ describe("auto-discovered bunfig.toml [run] section", () => { // Elision only happens on a terminal. On POSIX FORCE_COLOR=1 turns the // terminal renderer on for a pipe; on Windows it stays off (see runElideLinesTest). - test.skipIf(isWindows)("[run] elide-lines enables elision for --filter", () => { + test("[run] elide-lines enables elision for --filter", () => { using dir = workspace("filter-bunfig-elide", "[run]\nelide-lines = 15\n"); const r = run(String(dir), ["run", "--filter", "dep0", "lines"], { FORCE_COLOR: "1", NO_COLOR: "0" }); expect(r.stdout).toMatch(/\[5 lines elided\]/); expect(r.exitCode).toBe(0); }); - test.skipIf(isWindows)("--elide-lines on the CLI wins over [run] elide-lines", () => { + test("--elide-lines on the CLI wins over [run] elide-lines", () => { using dir = workspace("filter-bunfig-cli-elide", "[run]\nelide-lines = 15\n"); const r = run(String(dir), ["run", "--elide-lines", "0", "--filter", "dep0", "lines"], { FORCE_COLOR: "1", @@ -1617,7 +1617,7 @@ test.concurrent.each([ }); // Windows paths can't be as long as MAX_PATH_BYTES. -describe.concurrent.skipIf(isWindows)("workspace paths longer than the path buffer", () => { +describe.concurrent("workspace paths longer than the path buffer", () => { const workspace = { "package.json": JSON.stringify({ name: "ws", workspaces: ["packages/*"] }), "packages/ok/package.json": JSON.stringify({ name: "ok", scripts: { present: "echo ok-ran" } }), diff --git a/test/cli/run/garbage-env.test.ts b/test/cli/run/garbage-env.test.ts index 1ec09ceb0bc3..96a39f0641bf 100644 --- a/test/cli/run/garbage-env.test.ts +++ b/test/cli/run/garbage-env.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { bunExe, isPosix, tempDir } from "harness"; +import { bunExe, tempDir } from "harness"; import path from "path"; -describe.if(isPosix)("garbage env", () => { +describe("garbage env", () => { test("garbage env", async () => { const cfile = path.join(import.meta.dirname, "garbage-env.c"); // Compile into a temp dir so the binary never lands in the repo root. diff --git a/test/cli/run/markdown-entrypoint.test.ts b/test/cli/run/markdown-entrypoint.test.ts index b27b69446d60..565be36032c9 100644 --- a/test/cli/run/markdown-entrypoint.test.ts +++ b/test/cli/run/markdown-entrypoint.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; import { readdirSync, readFileSync, rmSync, symlinkSync } from "fs"; -import { bunEnv, bunExe, isASAN, isDebug, isPosix, tempDir } from "harness"; +import { bunEnv, bunExe, isASAN, isDebug, tempDir } from "harness"; import { join } from "path"; // Tracks exit code from the last runMd() call so individual tests can @@ -347,7 +347,7 @@ describe("bun ", () => { expect(openers).toBeGreaterThanOrEqual(2); }); - test.skipIf(!isPosix || !(isDebug || isASAN))( + test.skipIf(!(isDebug || isASAN))( "prefetched remote images are staged into fresh temp files and never written through a pre-existing entry at the staged path", async () => { const payload = Buffer.alloc(256, "z"); diff --git a/test/cli/run/multi-run.test.ts b/test/cli/run/multi-run.test.ts index 32cc82ed21ad..2d1a349a9a6f 100644 --- a/test/cli/run/multi-run.test.ts +++ b/test/cli/run/multi-run.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { realpathSync } from "fs"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; import path from "path"; // Helper: spawn bun with multi-run flags, returns { stdout, stderr, exitCode } @@ -1009,7 +1009,7 @@ describe.concurrent("timing edge cases", () => { // Same shape, sh syntax: "orphan": "sleep 30 & echo ..." must not block // --parallel completion until the backgrounded sleep exits. - test.skipIf(isWindows)("a backgrounded helper does not block --parallel completion", async () => { + test("a backgrounded helper does not block --parallel completion", async () => { using dir = tempDir("mr-bg-helper", { "package.json": JSON.stringify({ scripts: { @@ -1294,7 +1294,7 @@ describe("abort: failure kills long-running processes", () => { expect(elapsed).toBeLessThan(15000); }); - test.skipIf(isWindows)("parallel: signaled process shows Signaled message", async () => { + test("parallel: signaled process shows Signaled message", async () => { using dir = tempDir("mr-abort-signal", { "package.json": JSON.stringify({ scripts: { diff --git a/test/cli/run/no-orphans.test.ts b/test/cli/run/no-orphans.test.ts index 8f0606f8b32c..f8de819e2e81 100644 --- a/test/cli/run/no-orphans.test.ts +++ b/test/cli/run/no-orphans.test.ts @@ -201,7 +201,7 @@ test.concurrent.skipIf(!isSupported)( // Linux this is covered by Bun setting linux_pdeathsig on every spawn when // no-orphans mode is enabled (prctl in the vfork child before exec). On macOS // it's covered by the libproc descendant walk in the exit handler. -test.concurrent.skipIf(!isPosix)( +test.concurrent( "BUN_FEATURE_FLAG_NO_ORPHANS=1: non-Bun grandchildren are reaped when bun dies with its parent", async () => { const { sh, bunPid, grandchildPid } = await spawnTree("1", "child-nonbun.js"); @@ -219,7 +219,7 @@ test.concurrent.skipIf(!isPosix)( // Same as above but the grandchild is spawned with uid/gid. The credential // change clears the pdeathsig the spawn armed (prctl(2)), so this proves it // gets re-armed after setgid/setuid. Needs root to setuid. -test.concurrent.skipIf(!isPosix || !isRoot)( +test.concurrent.skipIf(!isRoot)( "BUN_FEATURE_FLAG_NO_ORPHANS=1: a non-Bun grandchild spawned with uid/gid is reaped", async () => { const { sh, bunPid, grandchildPid } = await spawnTree("1", "child-nonbun-uid.js"); @@ -266,7 +266,7 @@ describe.concurrent.each([ { via: "--no-orphans", argv: ["--no-orphans"], bunfig: false, env: {} }, { via: "bunfig [run] noOrphans = true", argv: [], bunfig: true, env: {} }, ])("clean exit reaps descendants", ({ via, argv, bunfig, env: extraEnv }) => { - test.concurrent.skipIf(!isPosix)(via, async () => { + test.concurrent(via, async () => { using dir = tempDir("no-orphans-clean-exit", { ...(bunfig && { "bunfig.toml": "[run]\nnoOrphans = true\n" }), "grandchild.js": `process.stdout.write("r"); setInterval(()=>{}, 1000);`, @@ -331,7 +331,7 @@ describe.concurrent.each([ }, }, ])("bun run --no-orphans $label: supervisor SIGKILLed", ({ runArgs, files }) => { - test.concurrent.skipIf(!isPosix)("bun run and the script exit", async () => { + test.concurrent("bun run and the script exit", async () => { using dir = tempDir("no-orphans-run", files as DirectoryTree); const env: NodeJS.Dict = { ...bunEnv }; delete env.BUN_FEATURE_FLAG_NO_ORPHANS; @@ -387,7 +387,7 @@ describe.concurrent.each([ // Linux: PR_SET_CHILD_SUBREAPER claims the orphan, procfs walk finds it. // macOS: NoOrphansTracker's p_puniqueid spawn-graph finds it. const hasPerl = Bun.which("perl") != null; -test.concurrent.skipIf(!isPosix || !hasPerl)( +test.concurrent.skipIf(!hasPerl)( "bun run --no-orphans: perl setsid+double-fork daemon (no Bun in chain) is reaped", async () => { using dir = tempDir("no-orphans-perl", { @@ -582,7 +582,7 @@ test.concurrent.skipIf(!isLinux || !hasPerl)( // the grandchild on its own clean exit. Uses a non-Bun grandchild so the test // doesn't depend on env-var inheritance — proves the descendant walk runs from // the `bun run` process itself. -test.concurrent.skipIf(!isPosix)("bun run --no-orphans `, - "entry.ts": `console.log("v0!");`, - "fixture.ts": ` +test.skipIf(isWindows)( + "dev server closes the descriptor it opens to bundle an edited file again", + async () => { + using dir = tempDir("bake-reload-descriptors", { + "index.html": ``, + "entry.ts": `console.log("v0!");`, + "fixture.ts": ` import { readdirSync, writeFileSync } from "node:fs"; import html from "./index.html"; @@ -168,20 +170,22 @@ test("dev server closes the descriptor it opens to bundle an edited file again", console.log(openDescriptors() - before < 10 ? "closed" : "left open"); await server.stop(true); `, - }); + }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "fixture.ts"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "inherit", - }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + await using proc = Bun.spawn({ + cmd: [bunExe(), "fixture.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout).toEndWith("closed\n"); - expect(exitCode).toBe(0); -}, 60_000); + expect(stdout).toEndWith("closed\n"); + expect(exitCode).toBe(0); + }, + 60_000, +); test("a request that crosses maxRequestBodySize while it waits for a bundle that fails", async () => { using dir = tempDir("dev-server-deferred-limit", { diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 45e98f6fb97c..6771c0f96261 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync } from "fs"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import path from "path"; import { minimalFramework, tempDirWithBakeDeps } from "../bake-harness"; @@ -832,7 +832,7 @@ export default function IndexPage() { // A debug build with BUN_JSC_validateExceptionChecks=1 aborts on the first JSC call with an unchecked exception. // Windows resolves the import()'s absolute path as a relative path. - test("the module loader hooks check for exceptions", async () => { + test.skipIf(isWindows)("the module loader hooks check for exceptions", async () => { using dir = tempDir("bake-production-exception-checks", { "server.ts": ` export async function prerender(meta) { diff --git a/test/bake/serve-plugins-dev-server.test.ts b/test/bake/serve-plugins-dev-server.test.ts index 6d52c1b223f5..0a73aae7138c 100644 --- a/test/bake/serve-plugins-dev-server.test.ts +++ b/test/bake/serve-plugins-dev-server.test.ts @@ -5,7 +5,7 @@ // null (or garbage, depending on build mode) and the DevServer is never told that // plugin loading finished, so the request it deferred to `next_bundle` waits forever. import { expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; const indexHtml = /* html */ ` @@ -444,7 +444,7 @@ test.concurrent("releases a framework request that the client aborts while it wa }); // Windows refuses to rename over a file that the watcher has open. -test("responds to a framework request queued behind the reload that bundles its page", async () => { +test.skipIf(isWindows)("responds to a framework request queued behind the reload that bundles its page", async () => { using dir = tempDir("serve-plugins-devserver-queued-framework", { "bunfig.toml": `[serve.static]\nplugins = ["./plugin.ts"]\n`, "plugin.ts": ` diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index ebdea40f7035..015646fb4a13 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -517,7 +517,7 @@ describe("Bun.build", () => { }); }); - test("fails the build when the bundler thread can't start", async () => { + test.skipIf(isWindows)("fails the build when the bundler thread can't start", async () => { const script = ` const fs = require("fs"); const fds = []; @@ -2538,12 +2538,14 @@ test("sourcemap sourcesContent is valid JSON when source contains C0 control cha // Bun.build's link step waited for the shared thread pool to go *idle* rather than for its // own tasks, so any unrelated pool work extended the build by its full duration — a // node:fs read parked on a FIFO nobody writes made every later build hang forever. -test("Bun.build does not wait for unrelated thread-pool work", async () => { - using dir = tempDir("build-pool-wait", { - "a.ts": `import { b } from "./b"; import "./s.css"; console.log(b);`, - "b.ts": `export const b = 1;`, - "s.css": `body { color: red }`, - "run.js": ` +test.skipIf(isWindows)( + "Bun.build does not wait for unrelated thread-pool work", + async () => { + using dir = tempDir("build-pool-wait", { + "a.ts": `import { b } from "./b"; import "./s.css"; console.log(b);`, + "b.ts": `export const b = 1;`, + "s.css": `body { color: red }`, + "run.js": ` const { join } = require("path"); const dir = process.argv[2]; const fs = require("fs"); @@ -2580,19 +2582,21 @@ test("Bun.build does not wait for unrelated thread-pool work", async () => { console.log(readDone, readErr ? readErr.code : "ok"); process.exit(0); `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "run.js", String(dir)], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toStartWith("second build took "); - expect(stdout).toBe("true true false\ntrue ok\n"); - expect(exitCode).toBe(0); -}, 30_000); + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "run.js", String(dir)], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toStartWith("second build took "); + expect(stdout).toBe("true true false\ntrue ok\n"); + expect(exitCode).toBe(0); + }, + 30_000, +); describe.concurrent("Bun.build with a path longer than the path buffer", () => { const components = Math.ceil((MAX_PATH_BYTES * 1.25) / 201); diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index c65327d5e22d..5b6bd9141931 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -1,6 +1,18 @@ import { bytecodeOrderNames } from "bun:internal-for-testing"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isArm64, isASAN, isDebug, isLinux, isMacOS, isMusl, isWindows, tempDir } from "harness"; +import { + bunEnv, + bunExe, + isArm64, + isASAN, + isDebug, + isLinux, + isMacOS, + isMusl, + isPosix, + isWindows, + tempDir, +} from "harness"; import { chmodSync, closeSync, @@ -2749,40 +2761,44 @@ if (process.platform === "android") { // removed AFTER the process starts, which `Bun.spawn`'s `cwd` can't do, so a // shell wrapper `cd`s in, `rmdir`s, then execs the binary (how a user hits it). describe("compiled binary in a deleted cwd", () => { - test("exits cleanly instead of crashing", async () => { - using dir = tempDir("build-compile-deleted-cwd", { - "app.js": `console.log("should-not-run");`, - }); - const outfile = join(String(dir), "app"); + test.if(isPosix)( + "exits cleanly instead of crashing", + async () => { + using dir = tempDir("build-compile-deleted-cwd", { + "app.js": `console.log("should-not-run");`, + }); + const outfile = join(String(dir), "app"); - await using build = Bun.spawn({ - cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [, buildStderr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); - expect(buildStderr).not.toContain("error:"); - expect(buildExit).toBe(0); + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", join(String(dir), "app.js"), "--outfile", outfile], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, buildStderr, buildExit] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect(buildStderr).not.toContain("error:"); + expect(buildExit).toBe(0); - // A fresh directory to stand in and delete — NOT `dir`, which holds the - // compiled binary we still need to exec. - using cwdDir = tempDir("build-compile-gone-cwd", {}); - const gone = String(cwdDir); + // A fresh directory to stand in and delete — NOT `dir`, which holds the + // compiled binary we still need to exec. + using cwdDir = tempDir("build-compile-gone-cwd", {}); + const gone = String(cwdDir); - await using proc = Bun.spawn({ - cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${outfile}"`], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + await using proc = Bun.spawn({ + cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${outfile}"`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe(""); - expect(stderr).toContain("The current working directory was deleted"); - expect(exitCode).toBe(1); - }, 60_000); + expect(stdout).toBe(""); + expect(stderr).toContain("The current working directory was deleted"); + expect(exitCode).toBe(1); + }, + 60_000, + ); }); // Every region of the embedded module graph (file contents, names, bytecode, the diff --git a/test/bundler/compile-corrupted-embed.test.ts b/test/bundler/compile-corrupted-embed.test.ts index 0f01ea029d93..3919c6621144 100644 --- a/test/bundler/compile-corrupted-embed.test.ts +++ b/test/bundler/compile-corrupted-embed.test.ts @@ -168,8 +168,9 @@ test.skipIf(isMacOS)( expect(await run(exe)).toEqual({ stdout: "hello from standalone\n", stderr: "", exitCode: 0 }); await expectGracefulFallback(corruptedCopy(exe, "length", isWindows ? peStompLength : elfStompLength)); - - await expectGracefulFallback(corruptedCopy(exe, "vaddr", path => elfStompVaddr(path, 0x4141414141414141n))); + if (!isWindows) { + await expectGracefulFallback(corruptedCopy(exe, "vaddr", path => elfStompVaddr(path, 0x4141414141414141n))); + } }, TIMEOUT, ); diff --git a/test/bundler/metafile.test.ts b/test/bundler/metafile.test.ts index b2fd542768b7..8953f2ac6bb9 100644 --- a/test/bundler/metafile.test.ts +++ b/test/bundler/metafile.test.ts @@ -862,7 +862,7 @@ describe("Bun.build metafile option variants", () => { }); // CLI tests for --metafile-md -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isWindows } from "harness"; describe("bun build --metafile-md", () => { test("generates markdown metafile with default name", async () => { @@ -1418,7 +1418,7 @@ describe("bun build --metafile-md", () => { describe("bun build --metafile", () => { // Bun.spawn replaces a lone surrogate in a JS string with U+FFFD before it reaches argv, // so the raw bytes have to come from the shell. Windows argv is UTF-16 and cannot carry them. - test("escapes a lone surrogate and an invalid byte in an output path", async () => { + test.skipIf(isWindows)("escapes a lone surrogate and an invalid byte in an output path", async () => { using dir = tempDir("metafile-wtf8", { "index.js": `console.log(1);`, }); diff --git a/test/cli/bake/bake.test.ts b/test/cli/bake/bake.test.ts index 01c3f5759f3c..4f3374419990 100644 --- a/test/cli/bake/bake.test.ts +++ b/test/cli/bake/bake.test.ts @@ -451,10 +451,10 @@ test.concurrent.each([ const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); // One save can start the process twice on Windows, and each start prints its own port. - - expect(await get(url, "/missing")).toBe("200 1"); - return; - + if (!isWindows) { + expect(await get(url, "/missing")).toBe("200 1"); + return; + } const deadline = Date.now() + 10_000; let response = ""; while (response !== "200 1" && Date.now() < deadline) { @@ -617,7 +617,7 @@ test.concurrent("bun bake accepts a frozen export of app/server.ts", async () => }); // Bun.serve's `unix` option takes a socket path, which this test only makes on POSIX. -test.concurrent("bun bake keeps a unix socket from the export of app/server.ts", async () => { +test.concurrent.skipIf(isWindows)("bun bake keeps a unix socket from the export of app/server.ts", async () => { using dir = tempDir("bake-command-unix", {}); const socket = join(String(dir), "s.sock"); await Bun.write( @@ -864,32 +864,35 @@ test.concurrent("bun bake exits when a start for a bake.config.ts change finds a }); // Windows has no SIGUSR2. -test.concurrent("bun --watch-kill-signal ... bake --check sends that signal before a restart", async () => { - using dir = tempDir("bake-command-kill-signal", { - "tsconfig.json": checkedTsconfig, - "app/server.ts": ` +test.concurrent.skipIf(isWindows)( + "bun --watch-kill-signal ... bake --check sends that signal before a restart", + async () => { + using dir = tempDir("bake-command-kill-signal", { + "tsconfig.json": checkedTsconfig, + "app/server.ts": ` declare const Response: any; declare const process: any; declare const console: any; process.on("SIGUSR2", () => console.log("SIGUSR2 before restart")); export default { fetch: () => new Response("v1") }; `, - }); + }); - // `--check` starts the process again after a change, which sends the signal: SIGTERM without the flag. - await using proc = spawnBake(String(dir), ["--watch-kill-signal", "SIGUSR2", "bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/url: /); + // `--check` starts the process again after a change, which sends the signal: SIGTERM without the flag. + await using proc = spawnBake(String(dir), ["--watch-kill-signal", "SIGUSR2", "bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/url: /); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/SIGUSR2 before restart/); -}); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/SIGUSR2 before restart/); + }, +); // Windows has no SIGUSR2. -test.concurrent( +test.concurrent.skipIf(isWindows)( "bun --watch-kill-signal ... bake sends that signal before it starts again for bake.config.ts", async () => { using dir = tempDir("bake-command-kill-signal-config", { @@ -975,35 +978,38 @@ test.concurrent("bun bake reads a bake.config.ts whose default export is a promi // On Windows, a manager process starts each restart with its own environment, so a restart cannot // tell it is one, and exits on this error as at the first start. -test.concurrent("bun bake --check waits for a fix when a restart finds a page it cannot serve", async () => { - using dir = tempDir("bake-command-check-restart-error", { - "tsconfig.json": checkedTsconfig, - "app/pages/index.html": page, - "app/server.ts": `declare const Response: any;\nexport default { fetch: () => new Response("v1") };\n`, - }); +test.concurrent.skipIf(isWindows)( + "bun bake --check waits for a fix when a restart finds a page it cannot serve", + async () => { + using dir = tempDir("bake-command-check-restart-error", { + "tsconfig.json": checkedTsconfig, + "app/pages/index.html": page, + "app/server.ts": `declare const Response: any;\nexport default { fetch: () => new Response("v1") };\n`, + }); - await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/url: /); + await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/url: /); - // `--check` starts the process again on a change: the error must not end the session. - const slug = join(String(dir), "app/pages/[slug].html"); - await Bun.write(slug, page); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/a page cannot have/); + // `--check` starts the process again on a change: the error must not end the session. + const slug = join(String(dir), "app/pages/[slug].html"); + await Bun.write(slug, page); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/a page cannot have/); - rmSync(slug); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, - ); - const [, url] = await output.waitFor(/a page cannot have[\s\S]*url: (http:\/\/\S+)/); + rmSync(slug); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, + ); + const [, url] = await output.waitFor(/a page cannot have[\s\S]*url: (http:\/\/\S+)/); - expect(await get(url, "/missing")).toBe("200 v3"); -}); + expect(await get(url, "/missing")).toBe("200 v3"); + }, +); test.concurrent.each([ { name: "nothing to serve", files: { "README.md": "" }, error: "Nothing to serve in" }, @@ -1025,33 +1031,36 @@ test.concurrent.each([ }); // On Windows, a restart cannot tell it is one, as above. -test.concurrent("bun bake --check waits on a restart error also when the first start had a type error", async () => { - using dir = tempDir("bake-command-check-typeerror-restart", { - "tsconfig.json": checkedTsconfig, - "app/pages/[slug].html": page, - "app/server.ts": `declare const Response: any;\nconst n: number = "x";\nexport default { fetch: () => new Response("v1") };\n`, - }); +test.concurrent.skipIf(isWindows)( + "bun bake --check waits on a restart error also when the first start had a type error", + async () => { + using dir = tempDir("bake-command-check-typeerror-restart", { + "tsconfig.json": checkedTsconfig, + "app/pages/[slug].html": page, + "app/server.ts": `declare const Response: any;\nconst n: number = "x";\nexport default { fetch: () => new Response("v1") };\n`, + }); - await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); - const output = collectOutput(proc); - await output.waitFor(/TS2322/); + await using proc = spawnBake(String(dir), ["bake", "--check", "--port=0"]); + const output = collectOutput(proc); + await output.waitFor(/TS2322/); - // The fix restarts the process, which then finds the page it cannot serve: it must wait. - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, - ); - await output.waitFor(/a page cannot have/); + // The fix restarts the process, which then finds the page it cannot serve: it must wait. + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v2") };\n`, + ); + await output.waitFor(/a page cannot have/); - rmSync(join(String(dir), "app/pages/[slug].html")); - await Bun.write( - join(String(dir), "app/server.ts"), - `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, - ); - const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); + rmSync(join(String(dir), "app/pages/[slug].html")); + await Bun.write( + join(String(dir), "app/server.ts"), + `declare const Response: any;\nexport default { fetch: () => new Response("v3") };\n`, + ); + const [, url] = await output.waitFor(/url: (http:\/\/\S+)/); - expect(await get(url, "/missing")).toBe("200 v3"); -}); + expect(await get(url, "/missing")).toBe("200 v3"); + }, +); /** Links the React of the test suite into a project in `dir`. A junction needs no privilege on Windows. */ function linkReact(dir: string) { diff --git a/test/cli/check/check.test.ts b/test/cli/check/check.test.ts index df6393fe9a3e..8e8f88f30071 100644 --- a/test/cli/check/check.test.ts +++ b/test/cli/check/check.test.ts @@ -486,7 +486,7 @@ describe.concurrent("bun check", () => { }); // A dedicated thread draws the progress line, and only when a person is at a terminal. - test.skipIf(!hasTerminal)("shows progress in a terminal", async () => { + test.skipIf(isWindows || !hasTerminal)("shows progress in a terminal", async () => { using dir = project({ "index.ts": `const wrong: string = 1;\n` }); let output = ""; await using proc = spawn({ @@ -22829,7 +22829,7 @@ user${i}.ts(10,22): error TS2322: Type 'A' is not assignable to type 'never'.`, }); // Root can read every file. - const canReadEverything = isRoot; + const canReadEverything = isWindows || isRoot; test.skipIf(canReadEverything)("reports TS5083 for a source file that can't be read", async () => { using dir = project({ "a.ts": `import "./secret";\n`, "secret.ts": `export {};\n` }); chmodSync(join(String(dir), "secret.ts"), 0o000); diff --git a/test/cli/create/create-jsx.test.ts b/test/cli/create/create-jsx.test.ts index add03952174d..1702c48139e7 100644 --- a/test/cli/create/create-jsx.test.ts +++ b/test/cli/create/create-jsx.test.ts @@ -2,7 +2,7 @@ import type { Subprocess } from "bun"; import { beforeEach, describe, expect, test } from "bun:test"; import { readFileSync, statSync } from "fs"; import { cp, readdir } from "fs/promises"; -import { bunEnv, bunExe, isCI, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isCI, isWindows, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness"; import path from "path"; async function getServerUrl(process: Subprocess, all = { text: "" }) { @@ -326,7 +326,7 @@ for (const development of [true, false]) { // Windows: `bun create` never prints the "--only-missing install" line this // asserts on, so the dependency detection cannot be observed there. -test("auto-install passes detected dependencies as positionals", async () => { +test.todoIf(isWindows)("auto-install passes detected dependencies as positionals", async () => { using dir = tempDir("create-arg-separator", { "Component.tsx": `import "--trust"; @@ -469,7 +469,7 @@ function identity(file: string) { } // Windows: `bun create ` does not reach its install step there (see create-jsx.test.ts). -describe.concurrent("bun create and files that are already there", () => { +describe.concurrent.todoIf(isWindows)("bun create and files that are already there", () => { const userCss = "/* my own stylesheet */\n.card { color: red; }\n"; const packageJson = JSON.stringify({ name: "my-app", private: true }); diff --git a/test/cli/format/format.test.ts b/test/cli/format/format.test.ts index b589ed5e014b..683c0081a11d 100644 --- a/test/cli/format/format.test.ts +++ b/test/cli/format/format.test.ts @@ -6116,7 +6116,7 @@ exports.format = async text => "by Prettier"; ); // Same as Prettier 3.9.9 and oxfmt 0.72, which load a configuration in Node. On Windows it still runs. - test.each(["prettier.config.mjs", "oxfmt.config.ts"])( + test.skipIf(isWindows).each(["prettier.config.mjs", "oxfmt.config.ts"])( "a `preload` in bunfig.toml doesn't run before %s", async name => { const files = { @@ -6162,7 +6162,9 @@ exports.format = async text => "by Prettier"; }); // Opening a pipe like this blocks until a writer opens it. - test.each([".gitignore", ".git/info/exclude", ".prettierignore", ".editorconfig", ".prettierrc", "package.json"])( + test + .skipIf(isWindows) + .each([".gitignore", ".git/info/exclude", ".prettierignore", ".editorconfig", ".prettierrc", "package.json"])( "doesn't hang when %s is a link to a named pipe without a writer", async name => { const before = (dir: string) => { @@ -6174,7 +6176,7 @@ exports.format = async text => "by Prettier"; }, ); - test.each([ + test.skipIf(isWindows).each([ ["writes to it is read", "echo a.js", "b.js\n"], ["closes it without writing is read as an empty file", ":", "a.js\nb.js\n"], ])("a pipe whose writer %s", async (_, writer, expected) => { @@ -10127,7 +10129,7 @@ describe.concurrent("other things named fmt in the project", () => { expect(result.exitCode).toBe(0); }); - test("a package's executable wins over the formatter, and nothing is rewritten", async () => { + test.skipIf(isWindows)("a package's executable wins over the formatter, and nothing is rewritten", async () => { const files = { "node_modules/.bin/fmt": "#!/bin/sh\necho the executable\n", "a.js": ugly }; const result = await format(files, [], { reads: ["a.js"], diff --git a/test/cli/heap-prof.test.ts b/test/cli/heap-prof.test.ts index 5df60bb15338..f94ce71e788e 100644 --- a/test/cli/heap-prof.test.ts +++ b/test/cli/heap-prof.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join } from "path"; const testScript = `const arr = []; for (let i = 0; i < 100; i++) arr.push({ x: i, y: "hello" + i }); console.log("done");`; @@ -71,27 +71,30 @@ test("--heap-prof writes the profile when the script calls process.exit", async expectV8HeapSnapshotShape(profile); }); -test("--heap-prof writes the profile on a self-directed SIGINT with no JS handler", async () => { - using dir = tempDir("heap-prof-sigint-test", {}); +test.skipIf(process.platform === "win32")( + "--heap-prof writes the profile on a self-directed SIGINT with no JS handler", + async () => { + using dir = tempDir("heap-prof-sigint-test", {}); - await using proc = Bun.spawn({ - cmd: [bunExe(), "--heap-prof", "-e", `process.kill(process.pid, "SIGINT");`], - cwd: String(dir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "--heap-prof", "-e", `process.kill(process.pid, "SIGINT");`], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); - const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(proc.signalCode).toBe("SIGINT"); - expect(exitCode).not.toBe(0); + const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(proc.signalCode).toBe("SIGINT"); + expect(exitCode).not.toBe(0); - const glob = new Bun.Glob("*.heapprofile"); - const files = Array.from(glob.scanSync({ cwd: String(dir) })); - expect(files.length).toBe(1); - const profile = await readProfile(String(dir), files[0]); - expectV8HeapSnapshotShape(profile); -}); + const glob = new Bun.Glob("*.heapprofile"); + const files = Array.from(glob.scanSync({ cwd: String(dir) })); + expect(files.length).toBe(1); + const profile = await readProfile(String(dir), files[0]); + expectV8HeapSnapshotShape(profile); + }, +); test("--heap-prof-md generates markdown heap profile on exit", async () => { using dir = tempDir("heap-prof-md-test", {}); @@ -292,7 +295,7 @@ for (const [flag, value] of [ // POSIX 5000 bytes exceeds the fixed path buffer (Linux 4096, macOS 1024); // 2500 + 2500 exercises the combined bound with components that fit // individually on Linux. -test.each([ +test.skipIf(isWindows).each([ ["--heap-prof-dir", ["--heap-prof-dir", Buffer.alloc(5000, "d").toString()]], ["--heap-prof-name", ["--heap-prof-name", Buffer.alloc(5000, "n").toString()]], [ diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 1099fb8fa366..f8c6887f5d9c 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1086,7 +1086,7 @@ it.todoIf(!isWindows)("reloads when a deleted import is recreated", async () => }); // The Windows watcher never reads the cached listing. -it("survives a directory event for a folder whose cached listing failed to read", async () => { +it.skipIf(isWindows)("survives a directory event for a folder whose cached listing failed to read", async () => { using dir = tempDir("hot-dir-read-error", { "trace.log": "", "src/lib/a.ts": `export {};`, diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 73c5fd1cd781..62c8471b61fd 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -100,7 +100,7 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; // Ctrl-D is EOF only on a POSIX tty; the Windows console has no equivalent // key that ends a cooked-mode read. - test("bun init exits quietly on Ctrl-D at a text prompt", async () => { + test.skipIf(isWindows)("bun init exits quietly on Ctrl-D at a text prompt", async () => { await using temp = tempDir("bun-init-ctrl-d", {}); const decoder = new TextDecoder(); @@ -550,27 +550,31 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; 180_000, ); - test("`bun fmt --check` passes on the generated CLAUDE.md", async () => { - await using temp = tempDir("bun-init-claude-md", { "bin/claude": "#!/bin/sh\n" }); - fs.chmodSync(path.join(temp, "bin/claude"), 0o755); - await using init = Bun.spawn({ - cmd: [bunExe(), "init", "-y", "app"], - cwd: temp, - stdio: ["ignore", "ignore", "inherit"], - env: { ...bunEnv, PATH: `${path.join(temp, "bin")}:${bunEnv.PATH}`, CURSOR_AGENT_RULE_DISABLED: "1" }, - }); - expect(await init.exited).toBe(0); - expect(fs.readFileSync(path.join(temp, "app/CLAUDE.md"), "utf8")).toStartWith("Default to using Bun"); + test.skipIf(isWindows)( + "`bun fmt --check` passes on the generated CLAUDE.md", + async () => { + await using temp = tempDir("bun-init-claude-md", { "bin/claude": "#!/bin/sh\n" }); + fs.chmodSync(path.join(temp, "bin/claude"), 0o755); + await using init = Bun.spawn({ + cmd: [bunExe(), "init", "-y", "app"], + cwd: temp, + stdio: ["ignore", "ignore", "inherit"], + env: { ...bunEnv, PATH: `${path.join(temp, "bin")}:${bunEnv.PATH}`, CURSOR_AGENT_RULE_DISABLED: "1" }, + }); + expect(await init.exited).toBe(0); + expect(fs.readFileSync(path.join(temp, "app/CLAUDE.md"), "utf8")).toStartWith("Default to using Bun"); - await using format = Bun.spawn({ - cmd: [bunExe(), "fmt", "--check", "CLAUDE.md"], - cwd: path.join(temp, "app"), - stdio: ["ignore", "pipe", "pipe"], - env: bunEnv, - }); - const [stdout, stderr, exitCode] = await Promise.all([format.stdout.text(), format.stderr.text(), format.exited]); - expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); - }, 30_000); + await using format = Bun.spawn({ + cmd: [bunExe(), "fmt", "--check", "CLAUDE.md"], + cwd: path.join(temp, "app"), + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + const [stdout, stderr, exitCode] = await Promise.all([format.stdout.text(), format.stderr.text(), format.exited]); + expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 }); + }, + 30_000, + ); test("nested `bun install` output is inherited", async () => { // `bun init` spawns `bun install` via spawn_sync_inherit. The child must diff --git a/test/cli/inspect/HTTPServerAgent.test.ts b/test/cli/inspect/HTTPServerAgent.test.ts index f90ef342eafb..30a09bb64d1d 100644 --- a/test/cli/inspect/HTTPServerAgent.test.ts +++ b/test/cli/inspect/HTTPServerAgent.test.ts @@ -1,7 +1,7 @@ import { Subprocess, spawn } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunExe, bunEnv as env, tmpdirSync } from "harness"; +import { bunExe, bunEnv as env, isPosix, tmpdirSync } from "harness"; import { join } from "node:path"; import { InspectorSession, connect } from "./junit-reporter"; import { SocketFramer } from "./socket-framer"; @@ -78,7 +78,7 @@ class HTTPServerInspectorSession extends InspectorSession { } } -describe("HTTPServer inspector protocol", () => { +describe.if(isPosix)("HTTPServer inspector protocol", () => { let serverProcess: Subprocess; let serverUrl: URL; let session: HTTPServerInspectorSession; diff --git a/test/cli/install/bad-workspace.test.ts b/test/cli/install/bad-workspace.test.ts index edcf27d48405..3fcf4715832d 100644 --- a/test/cli/install/bad-workspace.test.ts +++ b/test/cli/install/bad-workspace.test.ts @@ -2,7 +2,7 @@ import { spawn, spawnSync } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; -import { bunEnv, bunExe, isLinux, isRoot, tempDir, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isLinux, isRoot, isWindows, tempDir, tmpdirSync } from "harness"; import { dirname, join } from "path"; let cwd: string; @@ -58,7 +58,7 @@ test.concurrent("glob entry under a missing directory reports the errno name and // npm, pnpm and yarn 1 fail here too. Root ignores the mode bits, and chmod on Windows // cannot make a directory unreadable or unsearchable. -test.concurrent.skipIf(isRoot).each([ +test.concurrent.skipIf(isWindows || isRoot).each([ ["unreadable", 0o000, "open", "secret"], ["unsearchable", 0o444, "fstatat", join("secret", "package.json")], ])("glob entry that reaches an %s directory names what it failed on", async (_, mode, syscall, failedOn) => { @@ -329,7 +329,7 @@ describe.concurrent("workspaces entries longer than the path buffer", () => { // The entry is read from `${dir}/${entry}/package.json`. One byte below the buffer size // that path still reaches the OS; from the buffer size on it is rejected before that. - test.each([ + test.skipIf(isWindows).each([ ["one byte below", -1, "Workspace not found"], ["exactly", 0, "ENAMETOOLONG reading package.json for workspace package"], ["one byte above", 1, "ENAMETOOLONG reading package.json for workspace package"], @@ -392,36 +392,39 @@ describe.concurrent("workspaces entries longer than the path buffer", () => { // Globs are walked relative to the project directory, so a match can lie deeper than the // buffer holds once the project directory is put in front of it. Windows has no such // depth: the buffer there holds more than the longest path the OS accepts. - test("glob match whose absolute package.json path does not fit fails with ENAMETOOLONG", async () => { - using dir = tempDir("bad-workspace-deep-glob-match", { "package.json": rootPackageJson(["deep/**"]) }); - // The directory itself fits PATH_MAX (so it can be created), `/package.json` - // does not fit the buffer. - const absoluteDirBytes = POSIX_PATH_BUFFER_BYTES - 6; - const names = deepDirectoryNames(absoluteDirBytes - Buffer.byteLength(String(dir)) - "/".length); - const workspaceDir = join(String(dir), ...names); - expect(Buffer.byteLength(workspaceDir)).toBe(absoluteDirBytes); - mkdirSync(workspaceDir, { recursive: true }); - // Too long to be written by its absolute path: write it relative to the parent directory. - await using writer = spawn({ - cmd: [ - bunExe(), - "-e", - `require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`, - names.at(-1)!, - ], - cwd: dirname(workspaceDir), - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - expect(await writer.stderr.text()).toBe(""); - expect(await writer.exited).toBe(0); + test.skipIf(isWindows)( + "glob match whose absolute package.json path does not fit fails with ENAMETOOLONG", + async () => { + using dir = tempDir("bad-workspace-deep-glob-match", { "package.json": rootPackageJson(["deep/**"]) }); + // The directory itself fits PATH_MAX (so it can be created), `/package.json` + // does not fit the buffer. + const absoluteDirBytes = POSIX_PATH_BUFFER_BYTES - 6; + const names = deepDirectoryNames(absoluteDirBytes - Buffer.byteLength(String(dir)) - "/".length); + const workspaceDir = join(String(dir), ...names); + expect(Buffer.byteLength(workspaceDir)).toBe(absoluteDirBytes); + mkdirSync(workspaceDir, { recursive: true }); + // Too long to be written by its absolute path: write it relative to the parent directory. + await using writer = spawn({ + cmd: [ + bunExe(), + "-e", + `require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`, + names.at(-1)!, + ], + cwd: dirname(workspaceDir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + expect(await writer.stderr.text()).toBe(""); + expect(await writer.exited).toBe(0); - const { stderr, exitCode } = await runInstall(String(dir)); + const { stderr, exitCode } = await runInstall(String(dir)); - expect(stderr).toContain(`error: ENAMETOOLONG reading package.json for workspace package "${names.join("/")}"`); - expect(exitCode).toBe(1); - }); + expect(stderr).toContain(`error: ENAMETOOLONG reading package.json for workspace package "${names.join("/")}"`); + expect(exitCode).toBe(1); + }, + ); }); async function runBun(args: string[], cwd: string, env: Record = bunEnv) { @@ -568,7 +571,7 @@ describe.concurrent.each([ // The package.json that `bun install` finds above the directory it runs in becomes the root of // the install: it says which lifecycle scripts are trusted, and its .npmrc and bunfig.toml apply. // In a directory with the mode of /tmp any local user can put one above somebody else's project. -describe.concurrent("package.json in a sticky, shared directory above the cwd", () => { +describe.concurrent.skipIf(isWindows)("package.json in a sticky, shared directory above the cwd", () => { // Its postinstall runs only if the root of the install trusts it. const dep = JSON.stringify({ name: "dep", @@ -707,7 +710,7 @@ describe.concurrent("package.json in a sticky, shared directory above the cwd", // A working directory can be PATH_MAX - 1 bytes long, and `/package.json` is 13 bytes longer. // On Windows the path buffer is longer than any path the OS accepts. -describe.concurrent("working directory close to PATH_MAX", () => { +describe.concurrent.skipIf(isWindows)("working directory close to PATH_MAX", () => { /** A project in a directory whose absolute path is exactly `cwdBytes` bytes long. */ function deepProject(cwdBytes: number) { const dir = tempDir("root-discovery-deep-cwd", { diff --git a/test/cli/install/bun-add-filter.test.ts b/test/cli/install/bun-add-filter.test.ts index 1ea7250b01ae..aca9546b44d1 100644 --- a/test/cli/install/bun-add-filter.test.ts +++ b/test/cli/install/bun-add-filter.test.ts @@ -1,7 +1,7 @@ import { file, write } from "bun"; import { afterAll, beforeAll, expect, test } from "bun:test"; import { chmod, exists, mkdir, readdir, rm } from "fs/promises"; -import { bunEnv, bunExe, isRoot, normalizeBunSnapshot, tempDir } from "harness"; +import { bunEnv, bunExe, isRoot, isWindows, normalizeBunSnapshot, tempDir } from "harness"; import { join } from "path"; import { TestRegistry } from "registry"; @@ -2322,19 +2322,22 @@ test.concurrent( ); // Windows is left out: its path buffer holds more than any path the OS accepts. -test.concurrent("a local path longer than the path buffer is rejected before anything is written", async () => { - const dir = await makeMonorepo(); - const before = await allPackageJsonTexts(dir); +test.concurrent.skipIf(isWindows)( + "a local path longer than the path buffer is rejected before anything is written", + async () => { + const dir = await makeMonorepo(); + const before = await allPackageJsonTexts(dir); - const path = "./" + Buffer.alloc(4200, "a").toString(); - const { stderr, exitCode } = await run(["add", path, "--filter", "api"], dir); - expect(stderr).toBe(`error: Dependency "${path}" has an unsafe folder path\n`); - expect(exitCode).toBe(1); + const path = "./" + Buffer.alloc(4200, "a").toString(); + const { stderr, exitCode } = await run(["add", path, "--filter", "api"], dir); + expect(stderr).toBe(`error: Dependency "${path}" has an unsafe folder path\n`); + expect(exitCode).toBe(1); - expect(await allPackageJsonTexts(dir)).toStrictEqual(before); - expect(await exists(join(dir, "bun.lock"))).toBeFalse(); - expect(await exists(join(dir, "node_modules"))).toBeFalse(); -}); + expect(await allPackageJsonTexts(dir)).toStrictEqual(before); + expect(await exists(join(dir, "bun.lock"))).toBeFalse(); + expect(await exists(join(dir, "node_modules"))).toBeFalse(); + }, +); test.concurrent("an absolute local path is written verbatim into every target", async () => { const dir = await makeMonorepo(); diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 929fbd1b4127..0168a56d61f2 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -7,6 +7,7 @@ import { bunExe, bunEnv as env, hostedGitRepository, + isWindows, readdirSorted, tempDir, tmpdirSync, @@ -616,7 +617,7 @@ it("should reject positionals longer than 2048 bytes without stack overflow", as }); // PATH_MAX is 4096 on Linux and 1024 on macOS. Windows' path buffer is larger than any command line. -it("should fail with ENAMETOOLONG for a local tarball path longer than PATH_MAX", async () => { +it.skipIf(isWindows)("should fail with ENAMETOOLONG for a local tarball path longer than PATH_MAX", async () => { const package_json = JSON.stringify({ name: "foo", version: "0.0.1" }); using dir = tempDir("bun-add-long-tarball-path", { "package.json": package_json }); const tarball = `./${Buffer.alloc(5000, "a").toString()}.tgz`; @@ -3041,7 +3042,7 @@ describe("a relative local path typed from a directory below package.json", () = }); // A Windows command line cannot carry a path longer than the path buffer. - it("a path that does not fit a path buffer once joined with the cwd is an error", async () => { + it.skipIf(isWindows)("a path that does not fit a path buffer once joined with the cwd is an error", async () => { const cwd = await makeNestedCwd(); const typed = "./" + Buffer.alloc(6000, "a").toString(); @@ -3051,7 +3052,7 @@ describe("a relative local path typed from a directory below package.json", () = expect(exitCode).toBe(1); }); - it("a path whose re-spelled form does not fit a path buffer is an error", async () => { + it.skipIf(isWindows)("a path whose re-spelled form does not fit a path buffer is an error", async () => { const app = join(package_dir, "a", "b", "c", "d", "e", "f"); const cwd = join(app, "src"); await mkdir(cwd, { recursive: true }); diff --git a/test/cli/install/bun-create.test.ts b/test/cli/install/bun-create.test.ts index 662735d0ba87..4a1c095847b4 100644 --- a/test/cli/install/bun-create.test.ts +++ b/test/cli/install/bun-create.test.ts @@ -396,7 +396,7 @@ describe.concurrent("git", () => { }); // The stub `git` logs the files each call sees. POSIX-only: it is a shell script. - it("runs every git command before the first task", async () => { + it.skipIf(!isPosix)("runs every git command before the first task", async () => { using dir = tempDir("create-git-order", { ...localdep, "bin/git": `#!/bin/sh\necho "$1" $(LC_ALL=C ls -A) >> "$GIT_LOG"\n`, @@ -463,7 +463,7 @@ describe.concurrent("git", () => { }); // With nothing to install, git's outcome was stored without the `!`. - it("reports the git repository when there is nothing to install", async () => { + it.skipIf(!isPosix)("reports the git repository when there is nothing to install", async () => { using dir = tempDir("create-git-no-deps", { "bin/git": "#!/bin/sh\nexit 0\n", "empty/.keep": "", @@ -846,7 +846,7 @@ describe.concurrent("template given as a path", () => { }); // A path on Windows holds more than a command line. - it.each(["", "a/", "a@", "@a/", "./"])( + it.skipIf(!isPosix).each(["", "a/", "a@", "@a/", "./"])( "refuses a template name longer than a path: %p and 5000 bytes", async prefix => { using dir = tempDir("create-long-template-name", { "home/.keep": "", "proj/keep.js": "mine" }); @@ -1416,7 +1416,7 @@ describe.concurrent("bun-create tasks", () => { expect(exitCode).toBe(0); }); - it("do not look for a bare command in the destination", async () => { + it.skipIf(!isPosix)("do not look for a bare command in the destination", async () => { const { exitCode, written } = await createFrom( { "bun-create": { postinstall: "local-task first" } }, { "local-task": stubs["bin/create-task"] }, @@ -1427,7 +1427,7 @@ describe.concurrent("bun-create tasks", () => { }); // The script needs its shebang to be started, so POSIX only. - it("run a path relative to the destination", async () => { + it.skipIf(!isPosix)("run a path relative to the destination", async () => { const { err, exitCode, written } = await createFrom( { "bun-create": { postinstall: "./setup.sh first second" } }, { "setup.sh": stubs["bin/create-task"] }, @@ -1486,7 +1486,7 @@ describe.each([ expect(exitCode).toBe(0); }); - it("in the name of a file that is in the way", async () => { + it.skipIf(!isPosix)("in the name of a file that is in the way", async () => { using dir = tempDir("create-conflict", { "bun-create/tpl/package.json": JSON.stringify({ name: "tpl" }), [`bun-create/tpl/a${raw}b.js`]: "template", diff --git a/test/cli/install/bun-install-backends.test.ts b/test/cli/install/bun-install-backends.test.ts index 50be153c4b02..d3109ae33193 100644 --- a/test/cli/install/bun-install-backends.test.ts +++ b/test/cli/install/bun-install-backends.test.ts @@ -161,7 +161,7 @@ function projectWithLocalPackage(name: string) { return { dir, projectDir: String(dir), packageDir: join(String(dir), "pkg") }; } -describe("install backends on POSIX", () => { +describe.skipIf(isWindows)("install backends on POSIX", () => { // The copyfile backend only materializes files (parents are created on // demand), the others also create the package's directories. const directoryBackends = ["hardlink", "symlink", ...(isMacOS ? ["clonefile_each_dir"] : [])]; diff --git a/test/cli/install/bun-install-env-file.test.ts b/test/cli/install/bun-install-env-file.test.ts index 891c5ad10f33..b93a7c663b53 100644 --- a/test/cli/install/bun-install-env-file.test.ts +++ b/test/cli/install/bun-install-env-file.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join } from "node:path"; // https://github.com/oven-sh/bun/issues/12011 @@ -464,7 +464,7 @@ describe("bun install .env loading and the security scanner", () => { }); // A pipe can be read once. Install reads it, so the scanner must not be sent to the path. - test("--env-file from a pipe", async () => { + test.skipIf(isWindows)("--env-file from a pipe", async () => { const { seen, exitCode } = await scannerEnv({ sh: `printf 'SCAN_CUSTOM=from-pipe\\n' | "$0" install --env-file=/dev/stdin`, }); diff --git a/test/cli/install/bun-install-git-deps.test.ts b/test/cli/install/bun-install-git-deps.test.ts index 7d6b51cdf6a6..9b07689f74f8 100644 --- a/test/cli/install/bun-install-git-deps.test.ts +++ b/test/cli/install/bun-install-git-deps.test.ts @@ -1642,7 +1642,7 @@ test.concurrent( // that finishes second finds the other's clone already in the cache. It used to swap in its own clone and delete the // other one, which the first install still had open. The first install's checkout then failed with "fatal: repository // '/..tmp (deleted)' does not exist". The same applies to the checkout folder. -test.concurrent( +test.concurrent.skipIf(isWindows)( "an install keeps the git cache folders that another install published first", async () => { using dir = tempDir("git-dep-publish-race", {}); @@ -2299,7 +2299,7 @@ for (const linker of ["hoisted", "isolated"] as const) { // `git checkout` fails for a repository that holds a path the platform // cannot create. Here a wrapper around git fails it once `fail-checkout` exists. - test.concurrent( + test.concurrent.skipIf(isWindows)( `${linker} linker checks out again for a locked dependency after the checkout failed for another one`, async () => { using dir = tempDir(`git-dep-${linker}-failed-checkout`, {}); @@ -2572,7 +2572,7 @@ test.concurrent("a GIT_SSH_COMMAND in the project's .env does not replace the us expect(exitCode).toBe(1); }); -test.concurrent("a PATH in the project's .env does not choose the git of the install", async () => { +test.concurrent.skipIf(isWindows)("a PATH in the project's .env does not choose the git of the install", async () => { using dir = tempDir("git-dep-dotenv-path", {}); const root = String(dir); const ran = join(root, "ran"); @@ -2658,7 +2658,7 @@ async function failingGitCalls(dependency: string) { } // The install tries the https form of a clone URL, then the ssh form. -test.concurrent.each([ +test.concurrent.skipIf(isWindows).each([ // #36931: a URL that parses is passed on as it is. The port used to become a // path segment (`ssh://git@localhost/52626/...`). ["git+ssh://git@localhost:52626/user/repo.git#v1.0.0", "git@localhost:52626/user/repo.git"], @@ -2917,23 +2917,25 @@ test.concurrent("bun install sorts the workspace dependency by its res // one of the git processes instead of stopping the install, and concurrent // clones raced on the forwarder's process-wide state. On Linux the git // processes now carry PR_SET_PDEATHSIG, so they die with bun. -test.concurrent("SIGINT during git clones stops the install and is not forwarded to git", async () => { - using dir = tempDir("git-dep-sigint", {}); - const root = String(dir); - const bin = join(root, "bin"); - mkdirSync(bin); - const running = join(root, "git-running"); - const exited = join(root, "git-exited"); - const gotSigint = join(root, "git-got-sigint"); - const lineCount = (file: string) => (existsSync(file) ? readFileSync(file, "utf8").split("\n").length - 1 : 0); - // A fake git. Each process appends a line to `running` when it starts, to - // `exited` when it ends, and blocks until the test deletes `running`. A - // SIGINT that bun forwards to one of them is recorded in `gotSigint`, which - // makes every other fake git (the concurrent clone, the retry over ssh) - // exit at once. The `git config` that comes before the first clone is not one of them. - writeFileSync( - join(bin, "git"), - `#!/bin/sh +test.concurrent.skipIf(isWindows)( + "SIGINT during git clones stops the install and is not forwarded to git", + async () => { + using dir = tempDir("git-dep-sigint", {}); + const root = String(dir); + const bin = join(root, "bin"); + mkdirSync(bin); + const running = join(root, "git-running"); + const exited = join(root, "git-exited"); + const gotSigint = join(root, "git-got-sigint"); + const lineCount = (file: string) => (existsSync(file) ? readFileSync(file, "utf8").split("\n").length - 1 : 0); + // A fake git. Each process appends a line to `running` when it starts, to + // `exited` when it ends, and blocks until the test deletes `running`. A + // SIGINT that bun forwards to one of them is recorded in `gotSigint`, which + // makes every other fake git (the concurrent clone, the retry over ssh) + // exit at once. The `git config` that comes before the first clone is not one of them. + writeFileSync( + join(bin, "git"), + `#!/bin/sh [ "$6" = config ] && exit 1 trap 'echo $$ >> ${quote(exited)}' EXIT trap ': > ${quote(gotSigint)}; exit 130' INT @@ -2942,67 +2944,68 @@ i=0 while [ -e ${quote(running)} ] && [ ! -e ${quote(gotSigint)} ] && [ $i -lt 600 ]; do sleep 0.05; i=$((i+1)); done exit 1 `, - { mode: 0o755 }, - ); - // two repositories, so two clones run at the same time - const project = writeProject(root, { - [nameOf("a")]: "git+https://localhost/scope/pkg-a.git", - [nameOf("b")]: "git+https://localhost/scope/pkg-b.git", - }); - const env: typeof gitEnv = { ...gitEnv, BUN_INSTALL_CACHE_DIR: join(root, "cache"), PATH: `${bin}:${gitEnv.PATH}` }; - delete env.BUN_FEATURE_FLAG_NO_ORPHANS; - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: project, - env, - stdout: "pipe", - stderr: "pipe", - }); - const stdout = proc.stdout.text(); - const stderr = proc.stderr.text(); - try { - const deadline = Date.now() + 20_000; - while (lineCount(running) < 2) { - if (proc.exitCode !== null || proc.signalCode !== null) { - throw new Error(`install exited before it ran git:\n${await stderr}`); - } - if (Date.now() > deadline) throw new Error(`install ran ${lineCount(running)} of 2 clones`); - await Bun.sleep(10); - } - proc.kill("SIGINT"); - await Promise.all([stdout, stderr, proc.exited]); - // Read the marker only once every fake git is gone, so that a trap that - // fires after bun exited still counts. - const gone = Date.now() + 10_000; - const pids = readFileSync(running, "utf8").trim().split("\n").map(Number); - if (isLinux) { - // The kernel kills them together with bun (PR_SET_PDEATHSIG). `running` still exists. - const alive = (pid: number) => { - try { - return !readFileSync(`/proc/${pid}/status`, "utf8").includes("State:\tZ"); - } catch { - return false; + { mode: 0o755 }, + ); + // two repositories, so two clones run at the same time + const project = writeProject(root, { + [nameOf("a")]: "git+https://localhost/scope/pkg-a.git", + [nameOf("b")]: "git+https://localhost/scope/pkg-b.git", + }); + const env: typeof gitEnv = { ...gitEnv, BUN_INSTALL_CACHE_DIR: join(root, "cache"), PATH: `${bin}:${gitEnv.PATH}` }; + delete env.BUN_FEATURE_FLAG_NO_ORPHANS; + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: project, + env, + stdout: "pipe", + stderr: "pipe", + }); + const stdout = proc.stdout.text(); + const stderr = proc.stderr.text(); + try { + const deadline = Date.now() + 20_000; + while (lineCount(running) < 2) { + if (proc.exitCode !== null || proc.signalCode !== null) { + throw new Error(`install exited before it ran git:\n${await stderr}`); } - }; - while (pids.some(alive)) { - if (Date.now() > gone) throw new Error(`fake gits outlived bun: ${pids.filter(alive)}`); + if (Date.now() > deadline) throw new Error(`install ran ${lineCount(running)} of 2 clones`); await Bun.sleep(10); } - } else { - rmSync(running, { force: true }); - while (lineCount(exited) < pids.length) { - if (Date.now() > gone) throw new Error(`${lineCount(exited)} of ${pids.length} fake gits exited`); - await Bun.sleep(10); + proc.kill("SIGINT"); + await Promise.all([stdout, stderr, proc.exited]); + // Read the marker only once every fake git is gone, so that a trap that + // fires after bun exited still counts. + const gone = Date.now() + 10_000; + const pids = readFileSync(running, "utf8").trim().split("\n").map(Number); + if (isLinux) { + // The kernel kills them together with bun (PR_SET_PDEATHSIG). `running` still exists. + const alive = (pid: number) => { + try { + return !readFileSync(`/proc/${pid}/status`, "utf8").includes("State:\tZ"); + } catch { + return false; + } + }; + while (pids.some(alive)) { + if (Date.now() > gone) throw new Error(`fake gits outlived bun: ${pids.filter(alive)}`); + await Bun.sleep(10); + } + } else { + rmSync(running, { force: true }); + while (lineCount(exited) < pids.length) { + if (Date.now() > gone) throw new Error(`${lineCount(exited)} of ${pids.length} fake gits exited`); + await Bun.sleep(10); + } } + expect({ signalCode: proc.signalCode, gitGotSigint: existsSync(gotSigint) }).toEqual({ + signalCode: "SIGINT", + gitGotSigint: false, + }); + } finally { + rmSync(running, { force: true }); } - expect({ signalCode: proc.signalCode, gitGotSigint: existsSync(gotSigint) }).toEqual({ - signalCode: "SIGINT", - gitGotSigint: false, - }); - } finally { - rmSync(running, { force: true }); - } -}); + }, +); // bun.lock resolves an optional git dependency, the cache is cold, and the // repository is gone. The install phase failed for it with either linker. An @@ -3780,7 +3783,7 @@ test.concurrent("installs that refresh one cached repository at the same time al }); // The same without relying on timing: while the first `git fetch` runs, another git holds the lock on the branch. -test.concurrent("runs git fetch once more when it could not write a ref", async () => { +test.concurrent.skipIf(isWindows)("runs git fetch once more when it could not write a ref", async () => { using dir = tempDir("git-dep-refresh-locked", {}); const root = String(dir); const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); @@ -3971,7 +3974,7 @@ type Places = { clone: string; other: string; kept: string; command: string }; // since git 2.45 const usageOfInit = Bun.spawnSync({ cmd: ["git", "init", "-h"], env: gitEnv }); const gitKnowsReftable = `${usageOfInit.stdout}${usageOfInit.stderr}`.includes("--ref-format"); -test.concurrent.each<{ what: string; change(at: Places): void }>([ +test.concurrent.skipIf(isWindows).each<{ what: string; change(at: Places): void }>([ { what: "is a link", change({ clone, other }) { @@ -4143,7 +4146,7 @@ test.concurrent.each([ await install(); // A checkout that an older version left in the cache is created again. - + if (isWindows) return; const checkout = join(cache, `@G@${branchCommits(bare).main}`); symlinkSync(join(root, target), join(checkout, "package.json")); rmSync(join(project, "node_modules"), { recursive: true }); @@ -4180,7 +4183,7 @@ test.concurrent("a package.json that is a link to a file of the repository is a await install(); // A checkout that an older version left in the cache is created again. - + if (isWindows) return; rmSync(cached); symlinkSync("real/manifest.json", cached); rmSync(join(project, "node_modules"), { recursive: true }); @@ -4330,7 +4333,7 @@ test.concurrent("a git dependency keeps its executable files and the folder of a expect(exitCode).toBe(0); }); -test.concurrent("reports the required git version when git has no `restore` command", async () => { +test.concurrent.skipIf(isWindows)("reports the required git version when git has no `restore` command", async () => { const notACommand = "git: 'restore' is not a git command. See 'git --help'."; using dir = tempDir("git-dep-old-git", { "bin/git": `#!/bin/sh @@ -4368,7 +4371,7 @@ test.concurrent("two commits of one repository in one install", async () => { // The https form of an ssh URL is tried first. A host answers "not found" for a private // repository to a login without access, so that answer does not end the search. What git // said of a form that failed is of interest only if no form works. -test.concurrent.each([ +test.concurrent.skipIf(isWindows).each([ { status: 404, body: "Repository not found.", sshWorks: true }, { status: 403, body: "Forbidden", sshWorks: true }, { status: 404, body: "Repository not found.", sshWorks: false }, @@ -4471,7 +4474,7 @@ test.concurrent("every git starts with the same settings, spelled for the platfo expect(started[0][4]).toBe(`--git-dir=${nullDevice}`); }); -test.concurrent("asks git for the user's own commands once, before the first clone", async () => { +test.concurrent.skipIf(isWindows)("asks git for the user's own commands once, before the first clone", async () => { using dir = tempDir("git-dep-config-once", { "bin/git": `#!/bin/sh\necho "$5 $6 $7" >> "$GIT_CALLS"\nexit 1\n` }); const root = String(dir); chmodSync(join(root, "bin", "git"), 0o755); @@ -4524,7 +4527,7 @@ type Place = { where: string; files?: Record; env?: Record([ +test.concurrent.skipIf(isWindows).each([ { where: "~/.gitconfig", files: { "home/.gitconfig": mySshCommand } }, { where: "~/.gitconfig, under [Core]", files: { "home/.gitconfig": "[Core]\n\tsshCommand = ROOT/mine\n" } }, { where: "~/.gitconfig, in one line", files: { "home/.gitconfig": "[core] sshCommand = ROOT/mine\n" } }, @@ -4546,7 +4549,7 @@ test.concurrent.each([ }); // No clone reads the config of the repository it is started in. -test.concurrent.each([ +test.concurrent.skipIf(isWindows).each([ { where: "nowhere" }, { where: "only in the repository of the project", @@ -4563,7 +4566,7 @@ test.concurrent.each([ expect(ran.filter(line => !line.startsWith("ssh -oStrictHostKeyChecking=accept-new "))).toEqual([]); }); -test.concurrent("git runs the user's own core.askPass from ~/.config/git/config", async () => { +test.concurrent.skipIf(isWindows)("git runs the user's own core.askPass from ~/.config/git/config", async () => { await using server = Bun.serve({ port: 0, hostname: "127.0.0.1", @@ -4579,11 +4582,13 @@ test.concurrent("git runs the user's own core.askPass from ~/.config/git/config" // dependency spell the same repository in two ways. A cold-cache install from the lockfile with the isolated linker // cloned it under both spellings and never finished. The ssh replacement runs the command on this machine. for (const user of ["git", "alice"]) { - test.concurrent( - `a cold-cache install from bun.lock clones an scp-form dependency (${user}@host:path) once`, - async () => { - using dir = tempDir("git-dep-scp", { - "ssh.sh": `#!/bin/sh + test + .skipIf(isWindows) + .concurrent( + `a cold-cache install from bun.lock clones an scp-form dependency (${user}@host:path) once`, + async () => { + using dir = tempDir("git-dep-scp", { + "ssh.sh": `#!/bin/sh echo "$*" >> "$(dirname "$0")/ssh.log" while [ $# -gt 1 ]; do case "$1" in @@ -4595,29 +4600,29 @@ done shift exec sh -c "$1" `, - }); - const root = String(dir); - chmodSync(join(root, "ssh.sh"), 0o755); - const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); - await git(bare, "symbolic-ref", "HEAD", "refs/heads/main"); - const project = writeProject(root, { dep: `${user}@localhost:${bare}` }); - const env = { GIT_SSH_COMMAND: join(root, "ssh.sh"), GIT_TERMINAL_PROMPT: "0" }; + }); + const root = String(dir); + chmodSync(join(root, "ssh.sh"), 0o755); + const bare = await makeSharedRepo(root, [{ name: "dep", branch: "main" }]); + await git(bare, "symbolic-ref", "HEAD", "refs/heads/main"); + const project = writeProject(root, { dep: `${user}@localhost:${bare}` }); + const env = { GIT_SSH_COMMAND: join(root, "ssh.sh"), GIT_TERMINAL_PROMPT: "0" }; - expect((await runInstall(project, join(root, "cache-1"), env, "--linker=isolated")).exitCode).toBe(0); - const lockfile = readFileSync(join(project, "bun.lock"), "utf8"); - expect(lockfile).toContain(`"dep@git+ssh://${user}@localhost:`); + expect((await runInstall(project, join(root, "cache-1"), env, "--linker=isolated")).exitCode).toBe(0); + const lockfile = readFileSync(join(project, "bun.lock"), "utf8"); + expect(lockfile).toContain(`"dep@git+ssh://${user}@localhost:`); - rmSync(join(project, "node_modules"), { recursive: true }); - rmSync(join(root, "ssh.log")); - const { exitCode } = await runInstall(project, join(root, "cache-2"), env, "--linker=isolated"); - expect(await installedVersionOf(project, "dep")).toBe("main"); - expect( - readFileSync(join(root, "ssh.log"), "utf8") - .split("\n") - .filter(line => line.includes("git-upload-pack")), - ).toHaveLength(1); - expect(readFileSync(join(project, "bun.lock"), "utf8")).toBe(lockfile); - expect(exitCode).toBe(0); - }, - ); + rmSync(join(project, "node_modules"), { recursive: true }); + rmSync(join(root, "ssh.log")); + const { exitCode } = await runInstall(project, join(root, "cache-2"), env, "--linker=isolated"); + expect(await installedVersionOf(project, "dep")).toBe("main"); + expect( + readFileSync(join(root, "ssh.log"), "utf8") + .split("\n") + .filter(line => line.includes("git-upload-pack")), + ).toHaveLength(1); + expect(readFileSync(join(project, "bun.lock"), "utf8")).toBe(lockfile); + expect(exitCode).toBe(0); + }, + ); } diff --git a/test/cli/install/bun-install-hardlink-fallback.test.ts b/test/cli/install/bun-install-hardlink-fallback.test.ts index 8851ccdf1d03..f0e5f33c5f2a 100644 --- a/test/cli/install/bun-install-hardlink-fallback.test.ts +++ b/test/cli/install/bun-install-hardlink-fallback.test.ts @@ -3,7 +3,7 @@ // hardlink install backend must fall back to copying files, like it already // does for EXDEV. https://github.com/oven-sh/bun/issues/36852 import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isLinux, isMusl, isWindows, runBunInstall, tempDir, unprivilegedSpawnOptions } from "harness"; +import { bunEnv, bunExe, isLinux, isMusl, runBunInstall, tempDir, unprivilegedSpawnOptions } from "harness"; import { chmodSync, cpSync, readdirSync, rmSync, statSync } from "node:fs"; import { join } from "node:path"; import { installEnv, TestRegistry } from "registry"; @@ -220,7 +220,7 @@ int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, }); // A `mkdir` that fails says nothing about hard links. Only the package that needs the directory fails. -describe.skipIf(isWindows)("hardlink backend, a directory that cannot be created", () => { +describe("hardlink backend, a directory that cannot be created", () => { const registry = new TestRegistry(); beforeAll(() => registry.start()); afterAll(() => registry.stop()); diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts index d16fed19e177..39cc1b63cf4a 100644 --- a/test/cli/install/bun-install-lifecycle-scripts.test.ts +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -2510,17 +2510,20 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("a bin whose file a s tool: { bin: { tool: "dist/tool" }, scripts: { postinstall: build }, files }, }); await expectLinked(ctx, { dependencies: { "a-tool": "1.0.0", tool: "1.0.0" }, trustedDependencies: ["tool"] }); - expect(readlinkSync(ctx.installed(".bin", "tool"))).toBe(join("..", "a-tool", "cli.js")); + if (!isWindows) expect(readlinkSync(ctx.installed(".bin", "tool"))).toBe(join("..", "a-tool", "cli.js")); }); // The file of the bin is in the tarball, so nothing is left to link after the script. - test.skipIf(linker === "isolated")("a program that a script writes in place of its link is kept", async () => { - using ctx = await setupScriptPackages(linker, { - tool: { bin: { tool: "cli.js" }, scripts: { postinstall: "rm ../.bin/tool && cp cli.js ../.bin/tool" }, files }, - }); - await expectLinked(ctx, { dependencies: { tool: "1.0.0" }, trustedDependencies: ["tool"] }); - expect(lstatSync(ctx.installed(".bin", "tool")).isFile()).toBe(true); - }); + test.skipIf(isWindows || linker === "isolated")( + "a program that a script writes in place of its link is kept", + async () => { + using ctx = await setupScriptPackages(linker, { + tool: { bin: { tool: "cli.js" }, scripts: { postinstall: "rm ../.bin/tool && cp cli.js ../.bin/tool" }, files }, + }); + await expectLinked(ctx, { dependencies: { tool: "1.0.0" }, trustedDependencies: ["tool"] }); + expect(lstatSync(ctx.installed(".bin", "tool")).isFile()).toBe(true); + }, + ); }); describe.concurrent.each(["hoisted", "isolated"] as const)("bun add --trust (%s)", linker => { @@ -2841,7 +2844,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("default node-gyp scr describe.concurrent.each(["hoisted", "isolated"] as const)("scripts that run at the same time (%s)", linker => { const names = ["a", "b", "c", "d"]; - test.each([ + test.skipIf(isWindows).each([ { title: "--concurrent-scripts=1", bunfig: "", args: ["--concurrent-scripts=1"], limit: 1 }, { title: "--concurrent-scripts=2", bunfig: "", args: ["--concurrent-scripts=2"], limit: 2 }, { title: "concurrentScripts = 1", bunfig: "concurrentScripts = 1\n", args: [], limit: 1 }, @@ -2937,65 +2940,69 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("scripts that run at 30_000, ); - test("a script that ends makes room for the next one while another keeps running", async () => { - using ctx = await setupScriptPackages( - linker, - Object.fromEntries( - names.map(name => [ - name, - { scripts: { postinstall: `echo ${name} >> "$STARTED" && read line < "$GATES/${name}"` } }, - ]), - ), - ); - await writeFile( - ctx.packageJson, - JSON.stringify({ - name: "foo", - dependencies: Object.fromEntries(names.map(name => [name, "1.0.0"])), - trustedDependencies: names, - }), - ); - expect((await ctx.bun("install", "--lockfile-only")).exitCode).toBe(0); + test.skipIf(isWindows)( + "a script that ends makes room for the next one while another keeps running", + async () => { + using ctx = await setupScriptPackages( + linker, + Object.fromEntries( + names.map(name => [ + name, + { scripts: { postinstall: `echo ${name} >> "$STARTED" && read line < "$GATES/${name}"` } }, + ]), + ), + ); + await writeFile( + ctx.packageJson, + JSON.stringify({ + name: "foo", + dependencies: Object.fromEntries(names.map(name => [name, "1.0.0"])), + trustedDependencies: names, + }), + ); + expect((await ctx.bun("install", "--lockfile-only")).exitCode).toBe(0); - const gates = join(ctx.packageDir, "gates"); - await mkdir(gates); - expect(Bun.spawnSync({ cmd: ["mkfifo", ...names.map(name => join(gates, name))] }).exitCode).toBe(0); - const pipes = new Map( - names.map(name => [name, openSync(join(gates, name), fsConstants.O_RDWR | fsConstants.O_NONBLOCK)]), - ); - const log = join(ctx.packageDir, "started.txt"); - const started = () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean) : []); + const gates = join(ctx.packageDir, "gates"); + await mkdir(gates); + expect(Bun.spawnSync({ cmd: ["mkfifo", ...names.map(name => join(gates, name))] }).exitCode).toBe(0); + const pipes = new Map( + names.map(name => [name, openSync(join(gates, name), fsConstants.O_RDWR | fsConstants.O_NONBLOCK)]), + ); + const log = join(ctx.packageDir, "started.txt"); + const started = () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean) : []); - await using proc = spawn({ - cmd: [bunExe(), "install", "--concurrent-scripts=2"], - cwd: ctx.packageDir, - env: { ...ctx.env, STARTED: log, GATES: gates }, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - try { - // The first one keeps running to the end, so each of the others has to start while it runs. - for (let count = 2; count <= names.length; count++) { - while (started().length < count) { - if (proc.exitCode !== null) throw new Error(`bun install exited with ${proc.exitCode}`); - await Bun.sleep(1); + await using proc = spawn({ + cmd: [bunExe(), "install", "--concurrent-scripts=2"], + cwd: ctx.packageDir, + env: { ...ctx.env, STARTED: log, GATES: gates }, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + try { + // The first one keeps running to the end, so each of the others has to start while it runs. + for (let count = 2; count <= names.length; count++) { + while (started().length < count) { + if (proc.exitCode !== null) throw new Error(`bun install exited with ${proc.exitCode}`); + await Bun.sleep(1); + } + expect(started()).toHaveLength(count); + writeSync(pipes.get(started()[count - 1])!, "\n"); + } + writeSync(pipes.get(started()[0])!, "\n"); + const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(err).not.toContain("error:"); + expect(exitCode).toBe(0); + } finally { + rmSync(gates, { recursive: true, force: true }); + for (const pipe of pipes.values()) { + writeSync(pipe, "\n"); + closeSync(pipe); } - expect(started()).toHaveLength(count); - writeSync(pipes.get(started()[count - 1])!, "\n"); - } - writeSync(pipes.get(started()[0])!, "\n"); - const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); - expect(err).not.toContain("error:"); - expect(exitCode).toBe(0); - } finally { - rmSync(gates, { recursive: true, force: true }); - for (const pipe of pipes.values()) { - writeSync(pipe, "\n"); - closeSync(pipe); } - } - }, 10_000); + }, + 10_000, + ); test("packages that a later install trusts, one script at a time", async () => { using ctx = await setupScriptPackages( @@ -3109,7 +3116,7 @@ describe.concurrent.each(["hoisted", "isolated"] as const)("the install after a // bun looks for the shell once. The first script removes it, whichever script that is and // however far the other packages are, so the second one has nothing to start with. - test("a script that cannot be started", async () => { + test.skipIf(isWindows)("a script that cannot be started", async () => { const waiting = ["v", "w", "x", "y", "z"]; using ctx = await setupScriptPackages( linker, @@ -7034,28 +7041,31 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { ] as [number, string][]) : []), ]; - test.each(signaled)("a postinstall killed by signal %d is reported and re-raised", async (signal, message) => { - using ctx = await setupTest(); - const { packageDir, packageJson, env } = ctx; - const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; + test.skipIf(isWindows).each(signaled)( + "a postinstall killed by signal %d is reported and re-raised", + async (signal, message) => { + using ctx = await setupTest(); + const { packageDir, packageJson, env } = ctx; + const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env; - await writeFile( - packageJson, - JSON.stringify({ name: "foo", version: "1.0.0", scripts: { postinstall: `kill -${signal} $$` } }), - ); + await writeFile( + packageJson, + JSON.stringify({ name: "foo", version: "1.0.0", scripts: { postinstall: `kill -${signal} $$` } }), + ); - await using proc = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stderr: "pipe", - env: testEnv, - }); - const [, err, exited] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env: testEnv, + }); + const [, err, exited] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(err).toContain(message); - expect(exited).toBe(128 + signal); - }); + expect(err).toContain(message); + expect(exited).toBe(128 + signal); + }, + ); }); describe.concurrent("stdout/stderr is inherited from root scripts during install", async () => { diff --git a/test/cli/install/bun-install-link-path.test.ts b/test/cli/install/bun-install-link-path.test.ts index be369895a476..01f2304ed5ae 100644 --- a/test/cli/install/bun-install-link-path.test.ts +++ b/test/cli/install/bun-install-link-path.test.ts @@ -498,7 +498,7 @@ describe.each(["hoisted", "isolated"])("link: (%s)", linker => { expect(exitCode).not.toBe(0); }); - test.concurrent("links the target's bin and doesn't change the file's mode", async () => { + test.skipIf(isWindows).concurrent("links the target's bin and doesn't change the file's mode", async () => { using dir = tempDir("link-path-bin", { ...bunfig, "project/package.json": JSON.stringify({ name: "root", dependencies: { dep: "link:./vendor/dep" } }), diff --git a/test/cli/install/bun-install-native-binlink.test.ts b/test/cli/install/bun-install-native-binlink.test.ts index 39fc3af968f0..f114c3f36762 100644 --- a/test/cli/install/bun-install-native-binlink.test.ts +++ b/test/cli/install/bun-install-native-binlink.test.ts @@ -774,62 +774,65 @@ describe.concurrent("native binlink altpath", () => { // touching the target file (no chmod, no shebang rewrite), so the attack this // guards against can't occur there, and the resolved-containment check in // `link_bin_or_create_shim` is `#[cfg(not(windows))]` accordingly. -test("skips bin entries whose target resolves outside the package directory and keeps outside files untouched", async () => { - const secretContents = "#!/usr/bin/env node\r\nconsole.log('outside file');\n"; - using dir = tempDir("binlink-resolved-target", { - "package.json": JSON.stringify({ - name: "binlink-containment-app", - version: "1.0.0", - workspaces: ["packages/*"], - dependencies: { - "resolved-escape-pkg": "workspace:*", - "contained-bin-pkg": "workspace:*", - }, - }), - "outside/secret.txt": secretContents, - "packages/resolved-escape-pkg/package.json": JSON.stringify({ - name: "resolved-escape-pkg", - version: "1.0.0", - bin: { "escape-cmd": "./payload/secret.txt" }, - }), - "packages/contained-bin-pkg/package.json": JSON.stringify({ - name: "contained-bin-pkg", - version: "1.0.0", - bin: { "contained-cmd": "bin/run.js" }, - }), - "packages/contained-bin-pkg/bin/run.js": '#!/usr/bin/env node\nconsole.log("contained-cmd ok");\n', - }); +test.skipIf(isWindows)( + "skips bin entries whose target resolves outside the package directory and keeps outside files untouched", + async () => { + const secretContents = "#!/usr/bin/env node\r\nconsole.log('outside file');\n"; + using dir = tempDir("binlink-resolved-target", { + "package.json": JSON.stringify({ + name: "binlink-containment-app", + version: "1.0.0", + workspaces: ["packages/*"], + dependencies: { + "resolved-escape-pkg": "workspace:*", + "contained-bin-pkg": "workspace:*", + }, + }), + "outside/secret.txt": secretContents, + "packages/resolved-escape-pkg/package.json": JSON.stringify({ + name: "resolved-escape-pkg", + version: "1.0.0", + bin: { "escape-cmd": "./payload/secret.txt" }, + }), + "packages/contained-bin-pkg/package.json": JSON.stringify({ + name: "contained-bin-pkg", + version: "1.0.0", + bin: { "contained-cmd": "bin/run.js" }, + }), + "packages/contained-bin-pkg/bin/run.js": '#!/usr/bin/env node\nconsole.log("contained-cmd ok");\n', + }); - const root = String(dir); - const secretPath = join(root, "outside", "secret.txt"); - // A mode without any execute bits, so a chmod performed by bin linking is observable. - chmodSync(secretPath, 0o600); - // In-package path component that resolves to a directory outside the package. - symlinkSync(join(root, "outside"), join(root, "packages", "resolved-escape-pkg", "payload")); - - await using install = spawn({ - cmd: [bunExe(), "install"], - cwd: root, - stdout: "ignore", - stdin: "ignore", - stderr: "pipe", - env: bunEnv, - }); - const [installStderr, installExit] = await Promise.all([install.stderr.text(), install.exited]); - expect(installStderr).not.toContain("error:"); - expect(installExit).toBe(0); - - // The file outside the package keeps its mode and contents. - expect(statSync(secretPath).mode & 0o777).toBe(0o600); - expect(readFileSync(secretPath, "utf8")).toBe(secretContents); - - // No `.bin` entry was created for the bin whose resolved target leaves the package. - expect(existsSync(join(root, "node_modules", ".bin", "escape-cmd"))).toBeFalse(); - - // The workspace bin that resolves inside its package is still linked and made executable, - // even though node_modules/ is a symlink into packages/. - const containedBin = join(root, "node_modules", ".bin", "contained-cmd"); - expect(existsSync(containedBin)).toBeTrue(); - expect(realpathSync(containedBin)).toBe(realpathSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js"))); - expect(statSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")).mode & 0o111).not.toBe(0); -}); + const root = String(dir); + const secretPath = join(root, "outside", "secret.txt"); + // A mode without any execute bits, so a chmod performed by bin linking is observable. + chmodSync(secretPath, 0o600); + // In-package path component that resolves to a directory outside the package. + symlinkSync(join(root, "outside"), join(root, "packages", "resolved-escape-pkg", "payload")); + + await using install = spawn({ + cmd: [bunExe(), "install"], + cwd: root, + stdout: "ignore", + stdin: "ignore", + stderr: "pipe", + env: bunEnv, + }); + const [installStderr, installExit] = await Promise.all([install.stderr.text(), install.exited]); + expect(installStderr).not.toContain("error:"); + expect(installExit).toBe(0); + + // The file outside the package keeps its mode and contents. + expect(statSync(secretPath).mode & 0o777).toBe(0o600); + expect(readFileSync(secretPath, "utf8")).toBe(secretContents); + + // No `.bin` entry was created for the bin whose resolved target leaves the package. + expect(existsSync(join(root, "node_modules", ".bin", "escape-cmd"))).toBeFalse(); + + // The workspace bin that resolves inside its package is still linked and made executable, + // even though node_modules/ is a symlink into packages/. + const containedBin = join(root, "node_modules", ".bin", "contained-cmd"); + expect(existsSync(containedBin)).toBeTrue(); + expect(realpathSync(containedBin)).toBe(realpathSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js"))); + expect(statSync(join(root, "packages", "contained-bin-pkg", "bin", "run.js")).mode & 0o111).not.toBe(0); + }, +); diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index b1101e6bbbdc..5b4ea4ca5b2c 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -3977,7 +3977,7 @@ describe.concurrent("binaries", () => { } // https://github.com/oven-sh/bun/issues/14736 - test("reinstall keeps an already-correct .bin link untouched on a read-only tree", async () => { + test.skipIf(isWindows)("reinstall keeps an already-correct .bin link untouched on a read-only tree", async () => { const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, JSON.stringify({ name: "foo", dependencies: { "what-bin": "1.0.0" } })); @@ -13886,7 +13886,7 @@ describe("global bin links when the package dir is inside the bin dir", () => { expect(await exited).toBe(0); }); - test("the bins are symlinks relative to the bin dir", async () => { + test.skipIf(isWindows)("the bins are symlinks relative to the bin dir", async () => { expect(await readlink(join(binDir, "bin-bun"))).toBe(join("global", "node_modules", "bunx-bins", "bin-bun.ts")); expect(await exists(join(binDir, "bin-bun"))).toBe(true); }); diff --git a/test/cli/install/bun-install-security-provider.test.ts b/test/cli/install/bun-install-security-provider.test.ts index d7025cb968f7..c4533bcc8996 100644 --- a/test/cli/install/bun-install-security-provider.test.ts +++ b/test/cli/install/bun-install-security-provider.test.ts @@ -1,4 +1,4 @@ -import { bunEnv, bunExe, githubTarball, runBunInstall, tempDir } from "harness"; +import { bunEnv, bunExe, githubTarball, isWindows, runBunInstall, tempDir } from "harness"; import { existsSync, rmSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -1353,7 +1353,7 @@ describe.concurrent("where the scanner is looked up", () => { expect(await install(files)).toEqual({ ran: "a-scanner", exitCode: 0 }); }); - it("a file with a quote in its name", async () => { + it.skipIf(isWindows)("a file with a quote in its name", async () => { const files = { "project/bunfig.toml": bunfig('./s"q.ts'), 'project/s"q.ts': scannerOf("the project") }; expect(await install(files)).toEqual({ ran: "the project", exitCode: 0 }); }); diff --git a/test/cli/install/bun-install-staging.test.ts b/test/cli/install/bun-install-staging.test.ts index 70e81df2ba09..75f7c68df86f 100644 --- a/test/cli/install/bun-install-staging.test.ts +++ b/test/cli/install/bun-install-staging.test.ts @@ -512,7 +512,7 @@ const otherPackageDirs = { // The script is POSIX sh; on Windows lifecycle scripts run in bun's own shell. for (const [linker, otherPackageDir] of Object.entries(otherPackageDirs)) { - test.concurrent( + test.concurrent.skipIf(isWindows)( `${linker} linker: a lifecycle script can run bun install in the project being installed`, async () => { const scripts = { install: installsInTheSameProject }; @@ -714,7 +714,7 @@ const keepsItsDirectory = [ ].join("; "); // The script is POSIX sh; on Windows lifecycle scripts run in bun's own shell. -test.concurrent( +test.concurrent.skipIf(isWindows)( "hoisted linker: the second install of a doubly aliased package leaves a running lifecycle script its directory", async () => { const depTarball = await new Bun.Archive( @@ -874,7 +874,7 @@ function createTreeDeeperThanPathMax(parent: string, scratch: string) { } // On Windows no tree that exists is longer than the buffers its paths are built in. -test.concurrent( +test.concurrent.skipIf(isWindows)( "hoisted linker: a link that fails with ENAMETOOLONG leaves nothing at the package's path", async () => { const tgz = await new Bun.Archive( @@ -1005,7 +1005,7 @@ describe("hoisted linker: packages linked by the thread pool", () => { } // On Windows no tree that exists is longer than the buffers its paths are built in. - test.concurrent("packages that can't be linked only fail themselves, in order", async () => { + test.concurrent.skipIf(isWindows)("packages that can't be linked only fail themselves, in order", async () => { const names = ["a-first", "b-deep", "c-deep", "d-last"]; using registry = serveRegistry(await packAll(names)); using dir = project("pool-enametoolong", registry, { @@ -1342,7 +1342,7 @@ describe("hoisted linker: packages linked by the thread pool", () => { // Each host has a tree with three packages that have a script. One script runs at a time, and each // waits for the bin of the last tree: it is linked when that tree is done, after which no script // joins the ones that wait, and bun sees no script end before it has gone through them. - test.concurrent("scripts start in the order in which the main thread starts them", async () => { + test.concurrent.skipIf(isWindows)("scripts start in the order in which the main thread starts them", async () => { const hosts = Array.from({ length: 7 }, (_, i) => `host-${i}`); const scripted = ["scripted-1", "scripted-2", "scripted-3"]; const nested = [...scripted, "plain-1", "plain-2", "tool"]; diff --git a/test/cli/install/bun-install-streaming-extract.test.ts b/test/cli/install/bun-install-streaming-extract.test.ts index 71e59e006f00..d254ef2f066f 100644 --- a/test/cli/install/bun-install-streaming-extract.test.ts +++ b/test/cli/install/bun-install-streaming-extract.test.ts @@ -1456,7 +1456,7 @@ describe.concurrent("sparse tar members", () => { // whose first bytes were those of the refused block, and that package // stayed in the cache for every later install. // ------------------------------------------------------------------- -test("streaming extract fails the install when the file system refuses a write", async () => { +test.skipIf(isWindows)("streaming extract fails the install when the file system refuses a write", async () => { // 6,000,000 bytes where no file can grow past 4 MiB. Random bytes keep the // tarball above the size from which a registry tarball is streamed. const big = randomBytes(6_000_000); diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 68361afa1297..f49622a93d86 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -13730,7 +13730,7 @@ it.concurrent("does not install transitive file: dependencies with overlong fold // against the buffer when package.json is parsed, but the appended bytes were not, so a // folder path within 13 bytes of the buffer size aborted the install. Windows' buffer is // larger than any path the OS accepts, so the boundary cannot be reached there. -describe.concurrent("file: dependency whose package.json path is around the path buffer size", () => { +describe.concurrent.skipIf(isWindows)("file: dependency whose package.json path is around the path buffer size", () => { const PATH_BUFFER_BYTES = isLinux ? 4096 : 1024; // A relative path of exactly `bytes` bytes made of one letter directory names, so that a @@ -16650,7 +16650,7 @@ describe.concurrent("progress bar with a long name", () => { // `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux and Android, 1024 on macOS and // the BSDs) before chdir. Values that did not leave room for the NUL terminator used to abort the // process; the buffer on Windows (~96 KiB) is larger than any command line, so only POSIX is affected. -describe.concurrent("--cwd that does not fit the path buffer", () => { +describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", () => { const PATH_MAX = isLinux || isAndroid ? 4096 : 1024; const name = (length: number) => Buffer.alloc(length, "a").toString(); diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 8f7137776bd2..30fcb1383c8a 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -848,13 +848,13 @@ it("a link: dependency whose link is in place is not linked again", async () => expect(exitCode).toBe(0); // A hard link of the symlink keeps its inode number from being used for a new one. On macOS // link() follows the symlink, and APFS gives no number out twice. - if (!isMacOS) await fsLink(link, join(String(dir), "pin")); + if (!isWindows && !isMacOS) await fsLink(link, join(String(dir), "pin")); const before = (await lstat(link)).ino; ({ stdout, exitCode } = await bun(app, "install", "--linker=hoisted")); expect(stdout).toContain("(no changes)"); expect(exitCode).toBe(0); - expect((await lstat(link)).ino).toBe(before); + if (!isWindows) expect((await lstat(link)).ino).toBe(before); // The same name and version registered from another directory: the link has to move. expect((await bun(join(String(dir), "other"), "link")).exitCode).toBe(0); @@ -1063,7 +1063,7 @@ describe("global bin dir", () => { // The name is no longer the first package's to remove. result = await run("unlink", String(first), String(bunInstall)); expect(result.out).toContain('success: unlinked package "first-owner"'); - expect(bin).toBeValidBin(linkTarget("second-owner", "cli.js")); + if (!isWindows) expect(bin).toBeValidBin(linkTarget("second-owner", "cli.js")); expect(result.exitCode).toBe(0); }); diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 9ddfa47ba62c..2e25eabe41c1 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -76,7 +76,7 @@ it("should write plaintext lockfiles", async () => { }); // won't work on windows, " is not a valid character in a filename -it("should escape names", async () => { +it.skipIf(isWindows)("should escape names", async () => { const { packageDir, packageJson } = await registry.createTestDir(); await Promise.all([ write( @@ -1401,7 +1401,7 @@ describe.concurrent.each([ ["022", "644"], ["077", "600"], ])("umask %s", (umask, expectedMode) => { - it("bun.lock and yarn.lock honor umask", async () => { + it.skipIf(isWindows)("bun.lock and yarn.lock honor umask", async () => { const { packageDir } = await registry.createTestDir({ files: { "package.json": JSON.stringify({ name: "umask", workspaces: ["pkg1"], dependencies: { pkg1: "workspace:*" } }), diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index d4a582a87d8a..89b7209b38f3 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -92,7 +92,7 @@ it("should not print anything to stderr when running bun.lockb", async () => { ).toEqual([hashed, hashed]); }); -it("bun.lockb honors umask", async () => { +it.skipIf(isWindows)("bun.lockb honors umask", async () => { const { packageDir } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false }, files: { diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 500fb849efdb..76b006d77af8 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -559,7 +559,7 @@ describe.concurrent("flags", () => { // The destination directory ends `headroom` bytes short of the path buffer, so it still fits, // but the longer "/-.tgz" appended to it does not. Windows command lines cannot // carry an argument anywhere near its path buffer size. - test("--destination with no room left for the tarball name", async () => { + test.skipIf(isWindows)("--destination with no room left for the tarball name", async () => { const pathMax = isLinux ? 4096 : 1024; const headroom = 100; const name = `pack-dest-too-long-${Buffer.alloc(2 * headroom, "n").toString()}`; @@ -624,7 +624,7 @@ describe.concurrent("flags", () => { // The destination used to be opened a second time to hash it, which on a FIFO waits for another writer forever. // This end is open before pack starts and the tarball fits the pipe's buffer, so nothing here can block; // a pack that does is killed by `timeout`. - test("--filename can be a FIFO", async () => { + test.skipIf(isWindows)("--filename can be a FIFO", async () => { using dir = tempDir("pack-fifo", { "package.json": JSON.stringify({ name: "pack-fifo", version: "1.1.1" }), "index.js": indexJs, @@ -1061,7 +1061,7 @@ describe.concurrent("flags", () => { // ./*.tgz` to pick up. `ulimit -f 0` (RLIMIT_FSIZE) makes every write to the tarball fail with // EFBIG, which unlike a permission based setup also works when the tests run as root; setting it // needs a POSIX shell. -describe("a failed pack leaves no tarball behind", () => { +describe.skipIf(isWindows)("a failed pack leaves no tarball behind", () => { const packageJson = JSON.stringify({ name: "pack-failed", version: "1.0.0" }); async function packExpectingFailure(cwd: string, { fileSizeLimit }: { fileSizeLimit: boolean }, ...args: string[]) { @@ -1266,7 +1266,7 @@ describe.concurrent("something already exists at the tarball's destination", () } }); - test("a file keeps its mode", async () => { + test.skipIf(isWindows)("a file keeps its mode", async () => { using dir = tempDir("pack-over-mode", { ...files, "pkg/pack-over-1.0.0.tgz": "an earlier tarball" }); const tarball = join(dir, "pkg", "pack-over-1.0.0.tgz"); await chmod(tarball, 0o600); @@ -5196,7 +5196,7 @@ describe.concurrent("bins", () => { // `chmod 000` keeps the user that packs out of one entry that the scan looks for. The entry is // there, so the scan must not continue as if it was not. -describe.concurrent("an entry that exists but can't be opened", () => { +describe.concurrent.skipIf(isWindows)("an entry that exists but can't be opened", () => { // Runs `bun ...cmd` in `cwd` while `locked` has no permission bits. Restores them so that the tree can be removed. async function runWithout(dir: string, locked: string, cmd: string[], cwd: string = dir) { using unprivileged = unprivilegedSpawnOptions(dir); @@ -5925,7 +5925,7 @@ describe.concurrent.each([ ])("%s is printed spelled out", (_, raw, spelled) => { const unprintable = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/; - test("in a file name", async () => { + test.skipIf(isWindows)("in a file name", async () => { using dir = tempDir("pack-file-name", { "package.json": JSON.stringify({ name: "pack-file-name", version: "1.0.0" }), [`a${raw}b.js`]: "x", diff --git a/test/cli/install/bun-patch.test.ts b/test/cli/install/bun-patch.test.ts index 6b15f86c5f63..e26a8eccc732 100644 --- a/test/cli/install/bun-patch.test.ts +++ b/test/cli/install/bun-patch.test.ts @@ -1152,7 +1152,7 @@ const bundledAndNested = { dependencies: { "bundled-transitive": "1.0.0", "one-d describe.concurrent("a step that fails leaves node_modules as it was", () => { // RLIMIT_FSIZE makes the copy of a file larger than the limit fail with EFBIG. pkg-to-patch needs // no-deps@1.0.0 and the root no-deps@2.0.0, so the hoisted linker nests the former under it. - test.each([ + test.skipIf(isWindows).each([ ["hoisted", false], ["isolated", false], ["isolated", true], @@ -1264,7 +1264,7 @@ describe.concurrent("a step that fails leaves node_modules as it was", () => { // The git that fails is a shell script. One that cannot read a folder exits 1, as it does for // differences, and prints no patch. One that is killed prints nothing at all. - test.each([ + test.skipIf(isWindows).each([ ["fails", "echo 'fatal: not today' >&2\nexit 128\n", "fatal: not today"], [ "cannot read a folder", @@ -1811,7 +1811,7 @@ describe.concurrent("--dry-run prints what it would do and doesn't change the pr // entry that `bun patch` copies and that `bun patch --commit` compares with still has the mode from the tarball. So the // commands stopped working, and a manual chmod ended up in the patch. describe.concurrent("the commands of a package that is being patched", () => { - test.each(["hoisted", "isolated"] as const)( + test.skipIf(isWindows).each(["hoisted", "isolated"] as const)( "still run, and their mode isn't included in the patch (%s linker, copyfile backend)", async linker => { const name = "dep-with-directory-bins"; @@ -1892,7 +1892,7 @@ describe.concurrent("a package with install scripts", () => { // It used to be joined into a buffer of PATH_MAX bytes, which is a panic. A Windows path can be longer. describe.concurrent("an argument that is longer than any path", () => { - test.each([ + test.skipIf(isWindows).each([ ["bun patch ", ".", ["node_modules/LONG"]], ["bun patch ", ".", ["/LONG/node_modules/no-deps"]], ["bun patch --commit ", ".", ["--commit", "node_modules/LONG"]], @@ -3548,7 +3548,7 @@ describe.concurrent("the patch that bun patch --commit writes", () => { // A `color.ui = always` or a `diff.noprefix` in /etc/gitconfig used to end up in the patch, because the variables // that turn the config files off were passed as bare names, without `=value`. The git that prints its environment is // a shell script. - test("git runs without the config of the machine and of the user", async () => { + test.skipIf(isWindows)("git runs without the config of the machine and of the user", async () => { await using project = await createProject("hoisted"); const { dir, env } = project; await bunOk(dir, env, "patch", "basic-1"); @@ -3569,7 +3569,7 @@ describe.concurrent("the patch that bun patch --commit writes", () => { // With more than 1000 files deleted and 1000 added, git stops looking for renames. It prints the whole patch and a // warning on stderr. The git that prints the warning is a shell script. - test("a warning from git does not fail the commit", async () => { + test.skipIf(isWindows)("a warning from git does not fail the commit", async () => { await using project = await createProject("hoisted"); const { dir, env } = project; using stub = tempDir("bun-patch-stub-git", { diff --git a/test/cli/install/bun-pm-cache-pack.test.ts b/test/cli/install/bun-pm-cache-pack.test.ts index 4cb606581488..fedf0991c981 100644 --- a/test/cli/install/bun-pm-cache-pack.test.ts +++ b/test/cli/install/bun-pm-cache-pack.test.ts @@ -652,37 +652,40 @@ describe("bun pm cache unpack", () => { await expectAllRefused(box, packs); }); - test.concurrent("keeps every symlink inside its package, whatever the file system folds", async () => { - // Refused where `A/UP` is `a/up`, harmless where it is not. - const packs = { - ascii: [dir("a"), link("a/up", ".."), link("A/UP/A/UP/out", "../../../..")], - unicode: [dir("ä"), link("ä/up", ".."), link("Ä/UP/Ä/UP/out", "../../../..")], - target: [dir("ä"), link("ä/up", ".."), link("x", "Ä/up/Ä/up/victim")], - }; - for (const [name, records] of Object.entries(packs)) { - using box = sandbox(); - writeFileSync(box.at("fold.pack"), packOf(pkg(evil, [packageJson, ...records]))); - const { err, code } = await bun(box, ["pm", "cache", "unpack", box.at("fold.pack")]); - if (code === 0) { - const root = realpathSync(join(box.cache, evil)); - for (const line of snapshot(root, false)) { - const [path, target] = line.split(" -> "); - if (target === undefined || !existsSync(join(root, path))) continue; - expect({ name, path, inside: (realpathSync(join(root, path)) + sep).startsWith(root + sep) }).toEqual({ - name, - path, - inside: true, - }); + test.concurrent.skipIf(isWindows)( + "keeps every symlink inside its package, whatever the file system folds", + async () => { + // Refused where `A/UP` is `a/up`, harmless where it is not. + const packs = { + ascii: [dir("a"), link("a/up", ".."), link("A/UP/A/UP/out", "../../../..")], + unicode: [dir("ä"), link("ä/up", ".."), link("Ä/UP/Ä/UP/out", "../../../..")], + target: [dir("ä"), link("ä/up", ".."), link("x", "Ä/up/Ä/up/victim")], + }; + for (const [name, records] of Object.entries(packs)) { + using box = sandbox(); + writeFileSync(box.at("fold.pack"), packOf(pkg(evil, [packageJson, ...records]))); + const { err, code } = await bun(box, ["pm", "cache", "unpack", box.at("fold.pack")]); + if (code === 0) { + const root = realpathSync(join(box.cache, evil)); + for (const line of snapshot(root, false)) { + const [path, target] = line.split(" -> "); + if (target === undefined || !existsSync(join(root, path))) continue; + expect({ name, path, inside: (realpathSync(join(root, path)) + sep).startsWith(root + sep) }).toEqual({ + name, + path, + inside: true, + }); + } + } else { + expect(err).toContain("symlink collides"); + expect(readdirSync(box.cache)).toEqual([]); } - } else { - expect(err).toContain("symlink collides"); - expect(readdirSync(box.cache)).toEqual([]); + expect(readdirSync(box.tmp)).toEqual([]); } - expect(readdirSync(box.tmp)).toEqual([]); - } - }); + }, + ); - test.concurrent( + test.concurrent.skipIf(isWindows)( "restores symlinks that stay inside, modes without setuid, and names only Windows minds", async () => { using box = sandbox(); @@ -1034,41 +1037,44 @@ describe("bun pm cache pack", () => { }, ); - test.concurrent("refuses an entry that unpack would refuse, and leaves no file behind", async () => { - using box = sandbox(); - await installedProject(box); - const entry = join( - box.cache, - entryNames(box.cache).find(name => name.startsWith("no-deps@"))!, - ); - writeFileSync(join(box.project, "cache.pack"), "the previous pack"); - - for (const [path, target] of [ - ["out", "../../victim"], - ["absolute", box.victim], - ["back", "lib/../../../victim"], - ]) { - symlinkSync(target, join(entry, path)); + test.concurrent.skipIf(isWindows)( + "refuses an entry that unpack would refuse, and leaves no file behind", + async () => { + using box = sandbox(); + await installedProject(box); + const entry = join( + box.cache, + entryNames(box.cache).find(name => name.startsWith("no-deps@"))!, + ); + writeFileSync(join(box.project, "cache.pack"), "the previous pack"); + + for (const [path, target] of [ + ["out", "../../victim"], + ["absolute", box.victim], + ["back", "lib/../../../victim"], + ]) { + symlinkSync(target, join(entry, path)); + const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); + expect(r.err).toContain("error: bun pm cache pack failed: cache entry has symlink that may point outside it: "); + expect(r.err).toContain(`/${path}"`); + expect(r.out).toBe(""); + expect(r.code).toBe(1); + rmSync(join(entry, path)); + } + + writeFileSync(join(entry, "back\\slash"), ""); const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); - expect(r.err).toContain("error: bun pm cache pack failed: cache entry has symlink that may point outside it: "); - expect(r.err).toContain(`/${path}"`); - expect(r.out).toBe(""); + expect(r.err).toContain("cache entry path cannot be packed"); expect(r.code).toBe(1); - rmSync(join(entry, path)); - } - - writeFileSync(join(entry, "back\\slash"), ""); - const r = await bun(box, ["pm", "cache", "pack", "cache.pack"]); - expect(r.err).toContain("cache entry path cannot be packed"); - expect(r.code).toBe(1); - expect(readFileSync(join(box.project, "cache.pack"), "utf8")).toBe("the previous pack"); - expect( - readdirSync(box.project) - .filter(name => name.includes("pack")) - .sort(), - ).toEqual(["cache.pack", "package.json"]); - }); + expect(readFileSync(join(box.project, "cache.pack"), "utf8")).toBe("the previous pack"); + expect( + readdirSync(box.project) + .filter(name => name.includes("pack")) + .sort(), + ).toEqual(["cache.pack", "package.json"]); + }, + ); test.concurrent( "replaces the previous pack, makes the directory, and takes relative to where it was typed", diff --git a/test/cli/install/bun-pm-diff.test.ts b/test/cli/install/bun-pm-diff.test.ts index 21f97597fc91..fcf62f324209 100644 --- a/test/cli/install/bun-pm-diff.test.ts +++ b/test/cli/install/bun-pm-diff.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isDebug, isRoot, normalizeBunSnapshot, tempDir, tls } from "harness"; +import { bunEnv, bunExe, isDebug, isRoot, isWindows, normalizeBunSnapshot, tempDir, tls } from "harness"; import { chmodSync, existsSync, readFileSync, readdirSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -1105,7 +1105,7 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { }); // ConPTY rewrites the process's output. - test("NO_COLOR on a terminal: the plain patch, with control characters drawn", async () => { + test.skipIf(isWindows)("NO_COLOR on a terminal: the plain patch, with control characters drawn", async () => { const added = `exports.b = 2; //${ESC}[2K${ESC}[1A ${CSI_C1}31m ${RLO}\x7f\b
`; using dir = tempDir("pm-diff-plain-tty", { "a/index.js": "exports.a = 1;\n", @@ -1488,44 +1488,47 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { expect(exitCode).toBe(0); }); - test("mode changes: newly executable files are shown and called out", async () => { - using dir = tempDir("pm-diff-mode", { - "a/run.sh": "#!/bin/sh\necho hi\n", - "b/run.sh": "#!/bin/sh\necho hi\n", - "b/install.js": "console.log(1)\n", - }); - chmodSync(join(String(dir), "a/run.sh"), 0o644); - chmodSync(join(String(dir), "b/run.sh"), 0o755); - chmodSync(join(String(dir), "b/install.js"), 0o755); - const run = async (env: Record) => { - await using p = Bun.spawn({ - cmd: [bunExe(), "pm", "diff", "./a", "./b"], - cwd: String(dir), - env: { ...bunEnv, ...env }, - stdout: "pipe", - stderr: "pipe", + test.skipIf(process.platform === "win32")( + "mode changes: newly executable files are shown and called out", + async () => { + using dir = tempDir("pm-diff-mode", { + "a/run.sh": "#!/bin/sh\necho hi\n", + "b/run.sh": "#!/bin/sh\necho hi\n", + "b/install.js": "console.log(1)\n", }); - const [stdout, , exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); - return { stdout, exitCode }; - }; - const tty = await run({ NO_COLOR: undefined, FORCE_COLOR: "1", COLUMNS: "120" }); - const text = tty.stdout.replace(/\x1b\[[0-9;]*[mK]/g, ""); - expect(text).toMatch(/\nrun\.sh ─+ mode 644 → 755\n/); - expect(text).toMatch(/\ninstall\.js ─+ executable 755 new \+1\n/); - expect(text).toContain("▲ now executable: run.sh (644 → 755)\n"); - expect(text).toContain("▲ new executable file install.js (755)\n"); - expect(tty.exitCode).toBe(0); - // Patch mode spells it the way git does. - const plain = await run({ NO_COLOR: "1" }); - expect(plain.stdout).toContain("diff --git a/run.sh b/run.sh\nold mode 100644\nnew mode 100755\n"); - expect(plain.stdout).toContain("diff --git a/install.js b/install.js\nnew file mode 100755\n"); - expect(plain.exitCode).toBe(0); - }); + chmodSync(join(String(dir), "a/run.sh"), 0o644); + chmodSync(join(String(dir), "b/run.sh"), 0o755); + chmodSync(join(String(dir), "b/install.js"), 0o755); + const run = async (env: Record) => { + await using p = Bun.spawn({ + cmd: [bunExe(), "pm", "diff", "./a", "./b"], + cwd: String(dir), + env: { ...bunEnv, ...env }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); + return { stdout, exitCode }; + }; + const tty = await run({ NO_COLOR: undefined, FORCE_COLOR: "1", COLUMNS: "120" }); + const text = tty.stdout.replace(/\x1b\[[0-9;]*[mK]/g, ""); + expect(text).toMatch(/\nrun\.sh ─+ mode 644 → 755\n/); + expect(text).toMatch(/\ninstall\.js ─+ executable 755 new \+1\n/); + expect(text).toContain("▲ now executable: run.sh (644 → 755)\n"); + expect(text).toContain("▲ new executable file install.js (755)\n"); + expect(tty.exitCode).toBe(0); + // Patch mode spells it the way git does. + const plain = await run({ NO_COLOR: "1" }); + expect(plain.stdout).toContain("diff --git a/run.sh b/run.sh\nold mode 100644\nnew mode 100755\n"); + expect(plain.stdout).toContain("diff --git a/install.js b/install.js\nnew file mode 100755\n"); + expect(plain.exitCode).toBe(0); + }, + ); test("the piped patch applies with git: empty files, permission changes, file names that need quotes", async () => { const names = ["plain.txt", "with space.txt", "dir with space/x.txt", "é.txt"]; // This isn't valid in a Windows file name. - names.push("t\tab.txt", 'q"uote.txt', "back\\slash.txt", "new\nline.txt"); + if (!isWindows) names.push("t\tab.txt", 'q"uote.txt', "back\\slash.txt", "new\nline.txt"); const a: Record = { "grow.txt": "", "shrink.txt": "one\n", @@ -1608,42 +1611,45 @@ describe.concurrent("bun pm diff (hostile and awkward inputs)", () => { ); }); - test("folders: symlinked files are read through, symlinked folders skipped, unreadable files are an error", async () => { - using dir = tempDir("pm-diff-links", { - "a/index.js": "module.exports = 1;\n", - "b/real/index.js": "module.exports = 2;\n", - "c/index.js": "module.exports = 3;\n", - "c/secret.js": "nope\n", - }); - symlinkSync("real/index.js", join(String(dir), "b/index.js")); - symlinkSync("..", join(String(dir), "b/loop")); - symlinkSync("does-not-exist.js", join(String(dir), "b/dangling.js")); - const run = async (args: string[]) => { - await using p = Bun.spawn({ - cmd: [bunExe(), "pm", "diff", ...args, "--name-only"], - cwd: String(dir), - env: { ...bunEnv, NO_COLOR: "1" }, - stdout: "pipe", - stderr: "pipe", + test.skipIf(process.platform === "win32")( + "folders: symlinked files are read through, symlinked folders skipped, unreadable files are an error", + async () => { + using dir = tempDir("pm-diff-links", { + "a/index.js": "module.exports = 1;\n", + "b/real/index.js": "module.exports = 2;\n", + "c/index.js": "module.exports = 3;\n", + "c/secret.js": "nope\n", }); - const [stdout, stderr, exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); - return { stdout, stderr, exitCode }; - }; - const linked = await run(["./a", "./b"]); - expect(linked.stdout.split("\n").filter(l => /^[AMD] /.test(l))).toEqual(["M index.js", "A real/index.js"]); - expect(linked.exitCode).toBe(0); - chmodSync(join(String(dir), "c/secret.js"), 0o000); - try { - const denied = await run(["./a", "./c"]); - // root reads anything; everyone else must get a loud failure, not a diff with a hole in it. - if (!isRoot) { - expect(denied.stderr).toContain("secret.js"); - expect(denied.exitCode).toBe(1); + symlinkSync("real/index.js", join(String(dir), "b/index.js")); + symlinkSync("..", join(String(dir), "b/loop")); + symlinkSync("does-not-exist.js", join(String(dir), "b/dangling.js")); + const run = async (args: string[]) => { + await using p = Bun.spawn({ + cmd: [bunExe(), "pm", "diff", ...args, "--name-only"], + cwd: String(dir), + env: { ...bunEnv, NO_COLOR: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([p.stdout.text(), p.stderr.text(), p.exited]); + return { stdout, stderr, exitCode }; + }; + const linked = await run(["./a", "./b"]); + expect(linked.stdout.split("\n").filter(l => /^[AMD] /.test(l))).toEqual(["M index.js", "A real/index.js"]); + expect(linked.exitCode).toBe(0); + chmodSync(join(String(dir), "c/secret.js"), 0o000); + try { + const denied = await run(["./a", "./c"]); + // root reads anything; everyone else must get a loud failure, not a diff with a hole in it. + if (!isRoot) { + expect(denied.stderr).toContain("secret.js"); + expect(denied.exitCode).toBe(1); + } + } finally { + chmodSync(join(String(dir), "c/secret.js"), 0o644); } - } finally { - chmodSync(join(String(dir), "c/secret.js"), 0o644); - } - }); + }, + ); test("tarball shapes: .tar, a root folder not called package/, ~/ paths, and `./x.tar 1.0.1`", async () => { using dir = tempDir("pm-diff-tars", {}); diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index d90feac5b113..3ae7ed6b8902 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -2399,7 +2399,7 @@ it("bun pm cache prune removes packages that bun install cached and doesn't touc // does not fit is unusable in the same way as a directory the OS refuses to create, and bun falls // back to node_modules/.cache of the project like it does for those. On Windows the buffer holds // more than an environment variable or argument can carry. -describe.concurrent("cache directory setting longer than the path buffer", () => { +describe.concurrent.skipIf(isWindows)("cache directory setting longer than the path buffer", () => { // Longer than the buffer on every POSIX platform (the Linux one is the largest). const tooLong = "/" + Buffer.alloc(2 * 4096, "a").toString(); @@ -2694,7 +2694,7 @@ test.concurrent.each([ // The file: target becomes the resolution, so a dependent can put control characters in it (an // alias containing them is rejected before anything is installed). Windows does not allow them in // file names, so the package cannot be installed there in the first place. -test("bun pm untrusted escapes control characters in the package path and resolution", async () => { +test.skipIf(isWindows)("bun pm untrusted escapes control characters in the package path and resolution", async () => { using dir = tempDir("pm-untrusted-control-chars-path", { "package.json": JSON.stringify({ name: "foo", @@ -2729,7 +2729,7 @@ test("bun pm untrusted escapes control characters in the package path and resolu // $XDG_CACHE_HOME or $HOME) and the global bin directory (`$BUN_INSTALL/bin`, otherwise `.bun/bin` // under the same two) are each joined in a path buffer of MAX_PATH_BYTES: 4096 bytes on Linux, 1024 // on macOS. On Windows the buffer holds more than an environment variable can, so this is POSIX only. -describe.concurrent("global directories longer than the path buffer", () => { +describe.concurrent.skipIf(isWindows)("global directories longer than the path buffer", () => { const PATH_BUFFER_BYTES = isLinux ? 4096 : 1024; const LONG_VALUE = "/" + Buffer.alloc(8192, "a").toString(); @@ -2890,7 +2890,7 @@ describe.concurrent("the global bin directory and $PATH", () => { // A package.json that is readable but not writable (a read-only checkout, a file owned by // another user) must only stop the commands that actually rewrite it. -describe("read-only package.json", () => { +describe.skipIf(isWindows)("read-only package.json", () => { // Nothing here may reach a registry. const offlineEnv = { ...env, npm_config_registry: "http://127.0.0.1:1/" }; diff --git a/test/cli/install/bun-prune.test.ts b/test/cli/install/bun-prune.test.ts index e2356d51ac13..53c21973d5a2 100644 --- a/test/cli/install/bun-prune.test.ts +++ b/test/cli/install/bun-prune.test.ts @@ -2274,48 +2274,51 @@ test.concurrent("isolated + publicHoistPattern: hoisted links follow their store expect(await file(join(nm, "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0" }); }); -test.concurrent.skipIf(isRoot)("a failed deletion is reported, the rest is removed, exit code 1", async () => { - const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } }); - const nm = join(dir, "node_modules"); - const junkA = plant(dir, "node_modules/junk-a"); - const inner = plant(dir, "node_modules/junk-b/inner"); - const junkB = join(nm, "junk-b"); - chmodSync(junkB, 0o555); - const failure = /^error: failed to remove node_modules\/junk-b: E[A-Z]+ \(.+\)$/; - try { - // The failure is explained before the verdict, and the verdict counts it. - const { lines: merged, exitCode } = await pruneMerged(dir); - expect(merged).toStrictEqual([ - BANNER, - "", - "- junk-a", - expect.stringMatching(failure), - "1 package removed, 1 failed (checked 3 installed packages)", - ]); - expect(exitCode).toBe(1); - expect(existsSync(junkA)).toBeFalse(); - expect(existsSync(inner)).toBeTrue(); - - // --silent still reports what could not be removed; the exit code alone would hide which entry it was. - const silent = await prune(dir, "--silent"); - expect(silent.stdout).toBe(""); - expect(normalizeBunSnapshot(silent.stderr)).toMatch(failure); - expect(silent.exitCode).toBe(1); - expect(existsSync(inner)).toBeTrue(); - } finally { - chmodSync(junkB, 0o755); - } +test.concurrent.skipIf(isWindows || isRoot)( + "a failed deletion is reported, the rest is removed, exit code 1", + async () => { + const dir = await setup({ name: "foo", dependencies: { "no-deps": "1.0.0" } }); + const nm = join(dir, "node_modules"); + const junkA = plant(dir, "node_modules/junk-a"); + const inner = plant(dir, "node_modules/junk-b/inner"); + const junkB = join(nm, "junk-b"); + chmodSync(junkB, 0o555); + const failure = /^error: failed to remove node_modules\/junk-b: E[A-Z]+ \(.+\)$/; + try { + // The failure is explained before the verdict, and the verdict counts it. + const { lines: merged, exitCode } = await pruneMerged(dir); + expect(merged).toStrictEqual([ + BANNER, + "", + "- junk-a", + expect.stringMatching(failure), + "1 package removed, 1 failed (checked 3 installed packages)", + ]); + expect(exitCode).toBe(1); + expect(existsSync(junkA)).toBeFalse(); + expect(existsSync(inner)).toBeTrue(); + + // --silent still reports what could not be removed; the exit code alone would hide which entry it was. + const silent = await prune(dir, "--silent"); + expect(silent.stdout).toBe(""); + expect(normalizeBunSnapshot(silent.stderr)).toMatch(failure); + expect(silent.exitCode).toBe(1); + expect(existsSync(inner)).toBeTrue(); + } finally { + chmodSync(junkB, 0o755); + } - const { stdout, exitCode } = await prune(dir); - expect(out(stdout)).toMatchInlineSnapshot(` + const { stdout, exitCode } = await prune(dir); + expect(out(stdout)).toMatchInlineSnapshot(` "bun prune () - junk-b 1 package removed (checked 2 installed packages)" `); - expect(exitCode).toBe(0); - expect(existsSync(junkB)).toBeFalse(); -}); + expect(exitCode).toBe(0); + expect(existsSync(junkB)).toBeFalse(); + }, +); // pnpm#4770 / #5092 / #10275: `prepare` must not run after --production removed the tools it needs. test.concurrent("never runs the project's lifecycle scripts", async () => { @@ -3680,12 +3683,12 @@ test.concurrent.each(linkers)("%s: the bin of a removed package is relinked to t }); const nm = join(dir, "node_modules"); const target = () => readlinkSync(join(nm, ".bin", "what-bin")).replaceAll("\\", "/"); - expect(target()).toBe("../a-what-bin/what-bin.js"); + if (!isWindows) expect(target()).toBe("../a-what-bin/what-bin.js"); const { exitCode } = await prune(dir, "--production", "--linker", linker); expect(existsSync(join(nm, "a-what-bin"))).toBeFalse(); expectBinInstalled(nm, "what-bin"); - expect(target()).toBe("../what-bin/what-bin.js"); + if (!isWindows) expect(target()).toBe("../what-bin/what-bin.js"); expect(exitCode).toBe(0); }); @@ -3819,7 +3822,7 @@ test.concurrent( expectBinInstalled(join(store, entry, "node_modules"), "skip-test-cmd"); } const installed = nodeModulesTree(dir, { storeBins: true }); - { + if (!isWindows) { const ownBin = `node_modules/.bun/test-postinstall-skip@1.0.0/node_modules/.bin/skip-test-cmd`; expect(installed[ownBin]).toBe( `-> node_modules/.bun/${native}/node_modules/test-postinstall-skip-native/bin/cmd.js`, diff --git a/test/cli/install/bun-publish.test.ts b/test/cli/install/bun-publish.test.ts index fd50f3569ec9..79ae7bb8d636 100644 --- a/test/cli/install/bun-publish.test.ts +++ b/test/cli/install/bun-publish.test.ts @@ -1830,7 +1830,7 @@ describe.concurrent("tarball path", () => { // command line, and a path that fills the buffer exactly is refused by the open rather than the // join, which on Windows currently reports it as ENOSYS (it takes ENAMETOOLONG's number from the // C runtime, where it is a different one than in bun's errno table). - test.each([ + test.skipIf(isWindows).each([ ["longer than the path buffer", () => nameOfBytes(5000)], // Leaves no room for the terminating NUL. ["as long as the path buffer once resolved", (dir: string) => nameResolvingTo(PATH_BUFFER_BYTES, dir)], @@ -4191,7 +4191,7 @@ describe.concurrent.each(unprintable)("%s is printed escaped", (_, raw, spelled) const dryRunEnv = { ...env, npm_config_registry: "http://someuser:hunter2@127.0.0.1:1/" }; const anyUnprintable = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/; - test("in a file name of the directory", async () => { + test.skipIf(isWindows)("in a file name of the directory", async () => { using dir = tempDir("publish-file-name", { "package.json": JSON.stringify({ name: "publish-file-name", version: "1.0.0" }), [`a${raw}b.js`]: "x", diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index a8beb5e7f1c2..47ee427898dc 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -513,9 +513,10 @@ for (const linker of ["hoisted", "isolated"] as const) { // The global bin directory can be a folder like ~/.local/bin, where a dead link may belong to another tool. const foreign = isWindows ? [] : ["foreign-absolute", "foreign-relative"]; - - symlinkSync(join(String(dir), "gone", "cli.js"), join(globalBin, "foreign-absolute")); - symlinkSync(join("..", "gone", "cli.js"), join(globalBin, "foreign-relative")); + if (!isWindows) { + symlinkSync(join(String(dir), "gone", "cli.js"), join(globalBin, "foreign-absolute")); + symlinkSync(join("..", "gone", "cli.js"), join(globalBin, "foreign-relative")); + } const { stderr, exitCode } = await runGlobal("remove", "what-bin"); expect(stderr).not.toContain("error:"); @@ -724,7 +725,7 @@ it.concurrent("bun r is an alias of bun remove", async () => { }); // Serial: `unprivilegedSpawnOptions` may change and restore the modes of directories above the test's own. -it.serial("bun remove that cannot save bun.lock leaves node_modules as it was", async () => { +it.serial.skipIf(isWindows)("bun remove that cannot save bun.lock leaves node_modules as it was", async () => { const pkg = JSON.stringify({ name: "root", dependencies: { foo: "file:./foo", bar: "file:./bar" } }); using dir = tempDir("bun-remove-lockfile-not-saved", { ...local("foo"), ...local("bar"), "package.json": pkg }); expect(await run(String(dir), "install")).toMatchObject({ exitCode: 0 }); diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index a68b7f365a53..262b9e636a69 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -1265,7 +1265,7 @@ describe.concurrent("bun run", () => { // `/bun` — exactly the shim it was about to rewrite. The symlink // becomes self-referencing and `/usr/bin/env node` fails with ELOOP // ("Too many levels of symbolic links"). - it("nested --bun does not create a self-referencing node/bun shim", async () => { + it.skipIf(isWindows)("nested --bun does not create a self-referencing node/bun shim", async () => { // Reproduce the reporter's exact invocation: `bun run --bun bun run --bun ` // where: // - the literal "bun" must be resolved via PATH so the inner process's @@ -1556,7 +1556,7 @@ describe.concurrent("bun run", () => { // https://github.com/oven-sh/bun/issues/7504: `/tmp/bun-node-` was one directory for // all users. The first user to run `--bun` owned it (mode 0700), and every other user's // `--bun` silently got the real `node`. - it("--bun creates a per-user node shim directory", async () => { + it.skipIf(isWindows)("--bun creates a per-user node shim directory", async () => { using dir = tempDir("bun-run-node-shim-uid", { "package.json": JSON.stringify({ name: "shim-uid", diff --git a/test/cli/install/bun-upgrade.test.ts b/test/cli/install/bun-upgrade.test.ts index b0e1096252f2..eacecd6981fa 100644 --- a/test/cli/install/bun-upgrade.test.ts +++ b/test/cli/install/bun-upgrade.test.ts @@ -427,10 +427,12 @@ it.concurrent("recreates the staging directory in the temp dir instead of reusin // download (bun.zip) again. expect(readdirSync(join(stagingRootPath, "9.9.9"))).toEqual([]); - // The staging directory must be freshly created with no group/other - // access. The planted one was created with the default mode, so a reused - // directory fails here. - expect(statSync(join(stagingRootPath, "9.9.9")).mode & 0o077).toBe(0); + if (!isWindows) { + // The staging directory must be freshly created with no group/other + // access. The planted one was created with the default mode, so a reused + // directory fails here. + expect(statSync(join(stagingRootPath, "9.9.9")).mode & 0o077).toBe(0); + } // The bogus archive must not be installed; the upgrade fails cleanly. expect(exitCode).toBe(1); diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 19cf9754b53c..dfa883b3c1de 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -4553,7 +4553,7 @@ describe("a workspace outside the project", () => { }); }); - test.concurrent("a folder with a colon in its name is inside", async () => { + test.concurrent.skipIf(isWindows)("a folder with a colon in its name is inside", async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; const project = await setup(packageDir, { workspaces: ["a:b/pkg"] }); diff --git a/test/cli/install/bunx-directories-bin.test.ts b/test/cli/install/bunx-directories-bin.test.ts index 968adc05fe03..1a991000eeec 100644 --- a/test/cli/install/bunx-directories-bin.test.ts +++ b/test/cli/install/bunx-directories-bin.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -17,7 +17,7 @@ async function run(cwd: string, args: string[], env: Record) { } // The bins are shell scripts, which the bin links cannot run on Windows. -test.concurrent("bunx runs the bin of a directories.bin package", async () => { +test.concurrent.skipIf(isWindows)("bunx runs the bin of a directories.bin package", async () => { using dir = tempDir("bunx-directories-bin", { "package.json": JSON.stringify({ name: "app", dependencies: { tool: "file:./tool" } }), "tool/package.json": JSON.stringify({ name: "tool", version: "1.0.0", directories: { bin: "bins" } }), diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index 1dd57f884d02..f10c6dd8544a 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -385,7 +385,7 @@ it.concurrent("should choose the tagged versions instead of the PATH versions wh "bunx-decoy-bin", isWindows ? { "semver.cmd": "@echo semver from PATH\r\n" } : { "semver": '#!/bin/sh\necho "semver from PATH"\n' }, ); - chmodSync(join(String(decoyBinDir), "semver"), 0o755); + if (!isWindows) chmodSync(join(String(decoyBinDir), "semver"), 0o755); const processes = semverVersions.map(version => { return spawn({ @@ -1657,7 +1657,7 @@ it.concurrent.skipIf(!isWindows)("completions: writes bunx.cmd (not bunx.exe) wh // directory where other users can create files, which the temp directory is. // `.bunx-` is the trust boundary: a directory of this user with mode 0700. // Windows keeps %TEMP%\bunx--@: %TEMP% is per user. -describe("cache directory", () => { +describe.skipIf(isWindows)("cache directory", () => { const uid = process.getuid?.() ?? 0; const ran = "tool 1.1.0 with dep 1.0.0\n"; const bunxEntries = (dir: string) => @@ -2448,7 +2448,7 @@ require("child_process").execFileSync(process.env.BUN_EXE, ["install"], { cwd: p }); // Like on a file system without locks. - it.concurrent("runs without the lock when it cannot be taken", async () => { + it.concurrent.skipIf(isWindows)("runs without the lock when it cannot be taken", async () => { using registry = await localRegistry(...toolWithDep()); using box = sandbox(registry); mkdirSync(box.tree("tool@latest") + ".lock", { recursive: true }); @@ -2489,7 +2489,7 @@ describe("a package in node_modules", () => { }); }); - it.concurrent("a command in node_modules/.bin that cannot run is not downloaded", async () => { + it.concurrent.skipIf(isWindows)("a command in node_modules/.bin that cannot run is not downloaded", async () => { using registry = await localRegistry(fixturePackage("gen", "1.0.0", "gen")); using box = sandbox(registry); addLocalBin(box.cwd, "gen", `console.log("mine");`); @@ -2767,7 +2767,7 @@ describe("a package in node_modules", () => { // Of two packages with one bin name, one gets the entry in .bin. The install makes the file of the other // executable too. On Windows the entry is a shim that does not say whose it is, and bunx starts it. - it.concurrent("after an install, the package that did not get the entry in .bin", async () => { + it.concurrent.todoIf(isWindows)("after an install, the package that did not get the entry in .bin", async () => { using registry = await localRegistry( fixturePackage("aaa-first", "1.0.0", "tool"), fixturePackage("zzz-last", "1.0.0", "tool", echoBin("zzz-last 1.0.0")), @@ -2796,40 +2796,37 @@ describe("a package in node_modules", () => { // The install links the bin of a package with native builds into an optional dependency that has an `os` and a // `cpu`. This one has neither, so the entry in .bin is its own bin. - it.concurrent.todoIf(isWindows)( - "after an install, a package whose optional dependency got the entry in .bin", - async () => { - using registry = await localRegistry(fixturePackage("aaa-first", "1.0.0", "tool"), { - ...fixturePackage("zzz-last", "1.0.0", "tool", echoBin("zzz-last 1.0.0")), - optionalDependencies: { "aaa-first": "1.0.0" }, - }); - using box = sandbox(registry, { - cwd: { - "package.json": JSON.stringify({ name: "app", dependencies: { "aaa-first": "1.0.0", "zzz-last": "1.0.0" } }), - }, - }); - expect(await box.run(["--linker=hoisted"], { bunx: [bunExe(), "install"] })).toMatchObject({ exitCode: 0 }); - expect(join(box.cwd, "node_modules", ".bin", "tool")).toBeValidBin(join("..", "aaa-first", "cli.js")); + it.concurrent("after an install, a package whose optional dependency got the entry in .bin", async () => { + using registry = await localRegistry(fixturePackage("aaa-first", "1.0.0", "tool"), { + ...fixturePackage("zzz-last", "1.0.0", "tool", echoBin("zzz-last 1.0.0")), + optionalDependencies: { "aaa-first": "1.0.0" }, + }); + using box = sandbox(registry, { + cwd: { + "package.json": JSON.stringify({ name: "app", dependencies: { "aaa-first": "1.0.0", "zzz-last": "1.0.0" } }), + }, + }); + expect(await box.run(["--linker=hoisted"], { bunx: [bunExe(), "install"] })).toMatchObject({ exitCode: 0 }); + expect(join(box.cwd, "node_modules", ".bin", "tool")).toBeValidBin(join("..", "aaa-first", "cli.js")); - expect(await box.run(["zzz-last", "arg"])).toEqual({ - stdout: "zzz-last 1.0.0 arg\n", - stderr: "", - exitCode: 0, - requests: [], - }); + expect(await box.run(["zzz-last", "arg"])).toEqual({ + stdout: "zzz-last 1.0.0 arg\n", + stderr: "", + exitCode: 0, + requests: [], + }); - chmodSync(join(box.cwd, "node_modules", "zzz-last", "cli.js"), 0o644); - expect(await box.run(["zzz-last", "arg"])).toEqual({ - stdout: "", - stderr: expect.stringMatching(/^error: .*\/node_modules\/zzz-last\/cli\.js is not executable\n$/), - exitCode: 126, - requests: [], - }); - }, - ); + chmodSync(join(box.cwd, "node_modules", "zzz-last", "cli.js"), 0o644); + expect(await box.run(["zzz-last", "arg"])).toEqual({ + stdout: "", + stderr: expect.stringMatching(/^error: .*\/node_modules\/zzz-last\/cli\.js is not executable\n$/), + exitCode: 126, + requests: [], + }); + }); // The file is not there when the bins are linked. The install makes it executable after the script. - it.concurrent( + it.concurrent.todoIf(isWindows)( "after an install, a package without the entry in .bin whose install script writes the bin", async () => { const gen = (version: string): FixturePackage => ({ @@ -2863,7 +2860,7 @@ describe("a package in node_modules", () => { }, ); - it.concurrent("after an install, a workspace package whose bin name a dependency has", async () => { + it.concurrent.todoIf(isWindows)("after an install, a workspace package whose bin name a dependency has", async () => { using registry = await localRegistry( fixturePackage("aaa-first", "1.0.0", "tool"), fixturePackage("mycli", "9.9.9", "tool"), @@ -3217,20 +3214,23 @@ describe("flags", () => { }); // BUN_OPTIONS is for every command. - it.concurrent("flags in BUN_OPTIONS that bunx doesn't know are skipped, including their values", async () => { - using registry = await localRegistry(); - using box = sandbox(registry); - addLocalBin(box.cwd, "local-tool", `console.log("local", ...process.argv.slice(2));`); - symlinkSync(bunExe(), join(box.dir, "bunx")); + it.concurrent.skipIf(isWindows)( + "flags in BUN_OPTIONS that bunx doesn't know are skipped, including their values", + async () => { + using registry = await localRegistry(); + using box = sandbox(registry); + addLocalBin(box.cwd, "local-tool", `console.log("local", ...process.argv.slice(2));`); + symlinkSync(bunExe(), join(box.dir, "bunx")); - const env = { ...box.env, BUN_OPTIONS: "--smol --preload ./preloaded.ts" }; - expect(await box.run(["local-tool", "--nope"], { env, bunx: [join(box.dir, "bunx")] })).toEqual({ - stdout: "local --nope\n", - stderr: "", - exitCode: 0, - requests: [], - }); - }); + const env = { ...box.env, BUN_OPTIONS: "--smol --preload ./preloaded.ts" }; + expect(await box.run(["local-tool", "--nope"], { env, bunx: [join(box.dir, "bunx")] })).toEqual({ + stdout: "local --nope\n", + stderr: "", + exitCode: 0, + requests: [], + }); + }, + ); it.concurrent.each(["--help", "-h"])("%s prints the flags", async flag => { using box = sandbox(); @@ -3541,7 +3541,7 @@ describe("paths that do not fit in the path buffer", () => { // The install cache is so deep that /node_modules//package.json, which bunx reads for the // name of the bin, ends right at the size of the buffer, while /node_modules/.bin/ always fits. // One byte under, it fits together with its NUL. On Windows no existing directory overflows the buffer. - it.concurrent.each([ + it.concurrent.skipIf(isWindows).each([ ["one byte under the size of the path buffer", -1], ["exactly the size of the path buffer", 0], ["64 bytes over the size of the path buffer", 64], diff --git a/test/cli/install/escape-control-characters.test.ts b/test/cli/install/escape-control-characters.test.ts index dea9fbd387f3..259df70ef514 100644 --- a/test/cli/install/escape-control-characters.test.ts +++ b/test/cli/install/escape-control-characters.test.ts @@ -434,7 +434,7 @@ describe.concurrent.each(Object.entries(messages))( ); // Windows has no such file names. -test.concurrent.each(characters)("--verbose, a file of a package: %s", async (_, raw, spelled) => { +test.concurrent.skipIf(isWindows).each(characters)("--verbose, a file of a package: %s", async (_, raw, spelled) => { using dir = tempDir("escape-controls-file-name", { "package.json": JSON.stringify({ ...app, dependencies: { dep: "./dep.tgz" } }), }); diff --git a/test/cli/install/install-fd-leak.test.ts b/test/cli/install/install-fd-leak.test.ts index e35a69065a55..2597456add68 100644 --- a/test/cli/install/install-fd-leak.test.ts +++ b/test/cli/install/install-fd-leak.test.ts @@ -1,7 +1,7 @@ import { Archive, spawn } from "bun"; import { beforeAll, describe, expect, test } from "bun:test"; import { rm } from "fs/promises"; -import { bunEnv, bunExe, isMacOS, readdirSorted, tempDir } from "harness"; +import { bunEnv, bunExe, isMacOS, isWindows, readdirSorted, tempDir } from "harness"; import { join } from "path"; // Installing a package with many files must not exhaust the file-descriptor @@ -16,7 +16,7 @@ const LINKERS = ["isolated", "hoisted"] as const; const BACKENDS = (["copyfile", "hardlink"] as string[]).concat(isMacOS ? (["clonefile"] as const) : []); // `ulimit` isn't available on Windows; skip the matrix there. -describe("install does not leak file descriptors", () => { +describe.skipIf(isWindows)("install does not leak file descriptors", () => { let tgz: string; beforeAll(async () => { diff --git a/test/cli/install/isolated-install.test.ts b/test/cli/install/isolated-install.test.ts index 67df048dce74..91fc99bb6fdb 100644 --- a/test/cli/install/isolated-install.test.ts +++ b/test/cli/install/isolated-install.test.ts @@ -620,7 +620,7 @@ test.concurrent("can install folder dependencies", async () => { ).toBe("module.exports = 'hello from pkg-1';"); }); -test.concurrent("a backslash in the name of a file or a directory of a package is kept", async () => { +test.concurrent.skipIf(isWindows)("a backslash in the name of a file or a directory of a package is kept", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker: "isolated" } }); await Promise.all([ @@ -1040,7 +1040,7 @@ test.concurrent( ); // symlinkSync needs Developer Mode or elevation on Windows. -describe.each(["isolated", "hoisted"] as const)("links in a file: folder (%s)", linker => { +describe.skipIf(isWindows).each(["isolated", "hoisted"] as const)("links in a file: folder (%s)", linker => { const files = { "package.json": JSON.stringify({ name: "folder-links", dependencies: { lib: "file:./vendor/lib" } }), "shared/tsconfig.base.json": JSON.stringify({ shared: true }), @@ -1260,90 +1260,96 @@ describe.each(["isolated", "hoisted"] as const)("bin target modes with the %s li ["077", 0o700], ["002", 0o775], ])("umask %s", (umask, expectedMode) => { - test.concurrent("are 0o777 & ~umask, and an install that changes nothing writes nothing", async () => { - const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker } }); - const workspaceBin = join(packageDir, "packages", "tool", "cli.js"); - const binDir = join(packageDir, "node_modules", ".bin"); - const links = ["tool", "dep", "tb", "what-bin"].map(name => join(binDir, name)); - // A cache that the tests share hardlinks one inode of the tarball's bin into each of them. - const env = installEnv(packageDir); + test.concurrent.skipIf(isWindows)( + "are 0o777 & ~umask, and an install that changes nothing writes nothing", + async () => { + const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: { linker } }); + const workspaceBin = join(packageDir, "packages", "tool", "cli.js"); + const binDir = join(packageDir, "node_modules", ".bin"); + const links = ["tool", "dep", "tb", "what-bin"].map(name => join(binDir, name)); + // A cache that the tests share hardlinks one inode of the tarball's bin into each of them. + const env = installEnv(packageDir); - await Promise.all([ - write( - packageJson, - JSON.stringify({ - name: "umask-root", - workspaces: ["packages/*"], - dependencies: { - tool: "workspace:*", - dep: "file:./dep", - tb: "file:./tb-1.0.0.tgz", - "what-bin": "1.0.0", + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "umask-root", + workspaces: ["packages/*"], + dependencies: { + tool: "workspace:*", + dep: "file:./dep", + tb: "file:./tb-1.0.0.tgz", + "what-bin": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "tool", "package.json"), + JSON.stringify({ name: "tool", version: "1.0.0", bin: { tool: "./cli.js" } }), + ), + write(workspaceBin, "#!/usr/bin/env node\nconsole.log(1)\n"), + write( + join(packageDir, "dep", "package.json"), + JSON.stringify({ name: "dep", version: "1.0.0", bin: "d.js" }), + ), + write(join(packageDir, "dep", "d.js"), "#!/bin/sh\necho d\n"), + Bun.Archive.write( + join(packageDir, "tb-1.0.0.tgz"), + { + "package/package.json": JSON.stringify({ name: "tb", version: "1.0.0", bin: "t.js" }), + "package/t.js": "#!/bin/sh\necho t\n", }, - }), - ), - write( - join(packageDir, "packages", "tool", "package.json"), - JSON.stringify({ name: "tool", version: "1.0.0", bin: { tool: "./cli.js" } }), - ), - write(workspaceBin, "#!/usr/bin/env node\nconsole.log(1)\n"), - write(join(packageDir, "dep", "package.json"), JSON.stringify({ name: "dep", version: "1.0.0", bin: "d.js" })), - write(join(packageDir, "dep", "d.js"), "#!/bin/sh\necho d\n"), - Bun.Archive.write( - join(packageDir, "tb-1.0.0.tgz"), - { - "package/package.json": JSON.stringify({ name: "tb", version: "1.0.0", bin: "t.js" }), - "package/t.js": "#!/bin/sh\necho t\n", - }, - { compress: "gzip" }, - ), - ]); - await Promise.all([chmod(workspaceBin, 0o644), chmod(join(packageDir, "dep", "d.js"), 0o644)]); - - const want = expectedMode.toString(8); - async function installAndExpectModes() { - await using proc = spawn({ - cmd: ["sh", "-c", `umask ${umask} && exec "$0" install`, bunExe()], - cwd: packageDir, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).not.toContain("error:"); - expect([workspaceBin, ...links].map(path => (statSync(path).mode & 0o777).toString(8))).toEqual([ - want, - want, - want, - want, - want, + { compress: "gzip" }, + ), ]); - expect(exitCode).toBe(0); - } + await Promise.all([chmod(workspaceBin, 0o644), chmod(join(packageDir, "dep", "d.js"), 0o644)]); + + const want = expectedMode.toString(8); + async function installAndExpectModes() { + await using proc = spawn({ + cmd: ["sh", "-c", `umask ${umask} && exec "$0" install`, bunExe()], + cwd: packageDir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("error:"); + expect([workspaceBin, ...links].map(path => (statSync(path).mode & 0o777).toString(8))).toEqual([ + want, + want, + want, + want, + want, + ]); + expect(exitCode).toBe(0); + } - await installAndExpectModes(); - - // A link that is removed and made again loses its mtime. A chmod, even to the mode the file - // has, moves its ctime, which has the resolution of the kernel's clock tick: wait one out. - // The folder and the local tarball will not do: a run can install those again. - const unchanged = [workspaceBin, realpathSync(join(binDir, "what-bin"))]; - const ctimes = () => unchanged.map(path => statSync(path, { bigint: true }).ctimeNs); - for (const link of links) lutimesSync(link, 1, 1); - const before = ctimes(); - const probe = join(packageDir, "ctime-probe"); - await write(probe, ""); - do chmodSync(probe, 0o600); - while (before.some(ctime => statSync(probe, { bigint: true }).ctimeNs <= ctime)); - await installAndExpectModes(); - expect(links.map(link => lstatSync(link).mtimeMs)).toEqual([1000, 1000, 1000, 1000]); - expect(ctimes()).toEqual(before); - - // It isn't executable. The isolated linker of older versions left files like this behind. - for (const mode of [0o644, 0o777]) { - await Promise.all(links.map(link => chmod(realpathSync(link), mode))); await installAndExpectModes(); - } - }); + + // A link that is removed and made again loses its mtime. A chmod, even to the mode the file + // has, moves its ctime, which has the resolution of the kernel's clock tick: wait one out. + // The folder and the local tarball will not do: a run can install those again. + const unchanged = [workspaceBin, realpathSync(join(binDir, "what-bin"))]; + const ctimes = () => unchanged.map(path => statSync(path, { bigint: true }).ctimeNs); + for (const link of links) lutimesSync(link, 1, 1); + const before = ctimes(); + const probe = join(packageDir, "ctime-probe"); + await write(probe, ""); + do chmodSync(probe, 0o600); + while (before.some(ctime => statSync(probe, { bigint: true }).ctimeNs <= ctime)); + await installAndExpectModes(); + expect(links.map(link => lstatSync(link).mtimeMs)).toEqual([1000, 1000, 1000, 1000]); + expect(ctimes()).toEqual(before); + + // It isn't executable. The isolated linker of older versions left files like this behind. + for (const mode of [0o644, 0o777]) { + await Promise.all(links.map(link => chmod(realpathSync(link), mode))); + await installAndExpectModes(); + } + }, + ); }); }); @@ -2252,9 +2258,10 @@ describe.concurrent("lifecycle scripts of workspace members", () => { await runBunInstall(installEnv(packageDir), packageDir); expect(await bins()).toEqual({ root: names, member: names, registryPackage: names }); - - // The script writes the file without the execute bit. The linker sets the bit when it links the bin. - expect(statSync(join(packageDir, "packages", "no-deps", "dist", "no-deps-cli")).mode & 0o100).toBe(0o100); + if (!isWindows) { + // The script writes the file without the execute bit. The linker sets the bit when it links the bin. + expect(statSync(join(packageDir, "packages", "no-deps", "dist", "no-deps-cli")).mode & 0o100).toBe(0o100); + } await runBunInstall(installEnv(packageDir), packageDir, { savesLockfile: false }); @@ -5976,7 +5983,7 @@ describe.concurrent("bin links of a failed optional dependency (hoisted)", () => }); // Here, the `.bin` of w2 is a link to the root's `.bin`, which has the link of the root's q. - test.concurrent("nothing is removed through a `.bin` that is a link", async () => { + test.concurrent.skipIf(isWindows)("nothing is removed through a `.bin` that is a link", async () => { const { packageDir } = await registry.createTestDir({ bunfigOpts: { linker: "hoisted" }, files: { @@ -7040,7 +7047,7 @@ describe.concurrent("package trees deeper than the path buffer", () => { // On Windows the buffer holds as much as the filesystem does, so no tree that exists overflows it // with a store-relative path; the last test overflows with the absolute path instead. - describe("a folder dependency", () => { + describe.skipIf(isWindows)("a folder dependency", () => { test("fails with ENAMETOOLONG when a directory does not fit", async () => { using dir = projectWithDeepFolderDependency("isolated-deep-dir", chainDirs(PATH_BUFFER_LEN + 256)); expectPackageFailure(await bunInstall(String(dir)), "pkg"); @@ -7067,7 +7074,7 @@ describe.concurrent("package trees deeper than the path buffer", () => { }); // Folder dependencies are always hardlinked; --backend selects how packages from the cache are linked. - describe("a cached package", () => { + describe.skipIf(isWindows)("a cached package", () => { test.each(["hardlink", "copyfile"])("fails with ENAMETOOLONG and leaves nothing (%s)", async backend => { const { dir, cachedPackage, entryNodeModules } = await projectWithCachedPackage(`isolated-cached-${backend}`); using _ = dir; @@ -7502,7 +7509,7 @@ describe.concurrent("global virtual store", () => { expect(await readdirSorted(dirname(gvsTarget))).toEqual([basename(gvsTarget)]); }); - test("--force replaces an entry atomically, so it's never missing", async () => { + test.skipIf(isWindows)("--force replaces an entry atomically, so it's never missing", async () => { const { packageJson, packageDir } = await registry.createTestDir({ bunfigOpts: gvsBunfigOpts }); await write( packageJson, diff --git a/test/cli/install/migration/pnpm-lock-migration.test.ts b/test/cli/install/migration/pnpm-lock-migration.test.ts index eb7c8cc06c24..2e6b137dc011 100644 --- a/test/cli/install/migration/pnpm-lock-migration.test.ts +++ b/test/cli/install/migration/pnpm-lock-migration.test.ts @@ -1,6 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import fs from "fs"; -import { bunExe, bunEnv as harnessEnv, isRoot, tempDir } from "harness"; +import { bunExe, bunEnv as harnessEnv, isRoot, isWindows, tempDir } from "harness"; import { join } from "path"; import { startNpmjs } from "./migration-harness"; @@ -811,7 +811,7 @@ importers: }); // A 0444 package.json does not stop root from writing it, and the mode bits mean something else on Windows. - test.skipIf(isRoot).each([ + test.skipIf(isWindows || isRoot).each([ ["without a lockfile", {}], [ "while migrating pnpm-lock.yaml", diff --git a/test/cli/install/migration/pnpm-migration.test.ts b/test/cli/install/migration/pnpm-migration.test.ts index 16d59aafd288..47e6a70871e4 100644 --- a/test/cli/install/migration/pnpm-migration.test.ts +++ b/test/cli/install/migration/pnpm-migration.test.ts @@ -1,6 +1,6 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunExe, bunEnv as env, isLinux, nodeModulesPackages, tempDir } from "harness"; +import { bunExe, bunEnv as env, isLinux, isWindows, nodeModulesPackages, tempDir } from "harness"; import { join } from "path"; import { TestRegistry } from "registry"; @@ -311,7 +311,7 @@ snapshots: // The link resolves to `${dir}/packages/a/${link}`. A path of the buffer size leaves no room // for the NUL that ends it, so that is the first length that does not fit. - test.each([ + test.skipIf(isWindows).each([ ["one byte below", -1, "migrated lockfile from pnpm-lock.yaml", 0], ["exactly", 0, linkTooLong, 1], ["one byte above", 1, linkTooLong, 1], diff --git a/test/cli/install/shebang-normalize.test.ts b/test/cli/install/shebang-normalize.test.ts index 13cfb0592aab..a3749401cdf7 100644 --- a/test/cli/install/shebang-normalize.test.ts +++ b/test/cli/install/shebang-normalize.test.ts @@ -4,7 +4,7 @@ import { mkdir, readFile, stat, writeFile } from "fs/promises"; import { bunExe, bunEnv as env, isWindows, runBunInstall, tempDir, tmpdirSync } from "harness"; import { join } from "path"; -test("bin linking normalizes CRLF in shebang", async () => { +test.skipIf(isWindows)("bin linking normalizes CRLF in shebang", async () => { const testDir = tmpdirSync(); const pkgDir = join(testDir, "pkg"); const consumerDir = join(testDir, "consumer"); diff --git a/test/cli/install/symlink-path-traversal.test.ts b/test/cli/install/symlink-path-traversal.test.ts index 71775f886423..7a77d61ac81e 100644 --- a/test/cli/install/symlink-path-traversal.test.ts +++ b/test/cli/install/symlink-path-traversal.test.ts @@ -146,7 +146,7 @@ function createTarball( // Skip on Windows - symlink extraction is POSIX-only const isWindows = process.platform === "win32"; -describe.concurrent("symlink path traversal protection", () => { +describe.concurrent.skipIf(isWindows)("symlink path traversal protection", () => { setDefaultTimeout(60000); it("rejects symlink targets that climb above the package root before re-entering a 'packages' directory (streaming extraction)", async () => { @@ -869,189 +869,205 @@ it("rejects symlink targets that climb through other symlinks from the same arch } }, 60000); -it("does not change permissions of a file reached through a symlinked bin target", async () => { - // After creating `node_modules/.bin/`, the installer chmods the bin - // target to make it executable. If the bin target inside the package is - // itself a symlink (git/file/workspace dependencies can ship one — the npm - // tarball extractor never materializes one), chmod follows it and changes - // the mode of whatever file it points at, including files outside - // node_modules. The chmod must be skipped when the bin target is a symlink. - using dir = tempDir("bin-target-symlink-test", { - // Pin the hoisted linker so the bin link lands at node_modules/.bin and - // the chmod runs against the package's own bin target. - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-symlink-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-symlinked-bin", - version: "1.0.0", - bin: { "dep-with-symlinked-bin": "./payload" }, - }), - "victim.txt": "do not make me executable", - }); - const installDir = String(dir); - - const victimPath = join(installDir, "victim.txt"); - await chmod(victimPath, 0o600); - // The bin target is a symlink whose destination is outside the package - // directory. - await symlink(join("..", "..", "victim.txt"), join(installDir, "packages", "dep", "payload")); - - const proc = spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); +it.skipIf(isWindows)( + "does not change permissions of a file reached through a symlinked bin target", + async () => { + // After creating `node_modules/.bin/`, the installer chmods the bin + // target to make it executable. If the bin target inside the package is + // itself a symlink (git/file/workspace dependencies can ship one — the npm + // tarball extractor never materializes one), chmod follows it and changes + // the mode of whatever file it points at, including files outside + // node_modules. The chmod must be skipped when the bin target is a symlink. + using dir = tempDir("bin-target-symlink-test", { + // Pin the hoisted linker so the bin link lands at node_modules/.bin and + // the chmod runs against the package's own bin target. + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-symlink-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-symlinked-bin", + version: "1.0.0", + bin: { "dep-with-symlinked-bin": "./payload" }, + }), + "victim.txt": "do not make me executable", + }); + const installDir = String(dir); - // The file the symlink points at keeps its original permissions; the - // installer must not chmod through a symlinked bin target. - expect((await stat(victimPath)).mode & 0o777).toBe(0o600); + const victimPath = join(installDir, "victim.txt"); + await chmod(victimPath, 0o600); + // The bin target is a symlink whose destination is outside the package + // directory. + await symlink(join("..", "..", "victim.txt"), join(installDir, "packages", "dep", "payload")); - // The bin link itself is still created. - const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-bin"); - expect((await lstat(binLink)).isSymbolicLink()).toBe(true); + const proc = spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); -}, 60000); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); -it("does not change permissions of a directory reached through a symlinked bin target with a trailing slash", async () => { - // Same as above, but the bin target is written as "payload/". The kernel - // follows a symlink named with a trailing slash even when chmod is told not - // to follow symlinks, so the installer has to drop the slash before it - // stats, links and chmods the target. - using dir = tempDir("bin-target-symlink-trailing-slash-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-symlink-slash-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-symlinked-dir-bin", - version: "1.0.0", - bin: { "dep-with-symlinked-dir-bin": "./payload/" }, - }), - "victim-dir/keep": "", - }); - const installDir = String(dir); + // The file the symlink points at keeps its original permissions; the + // installer must not chmod through a symlinked bin target. + expect((await stat(victimPath)).mode & 0o777).toBe(0o600); - const victimDir = join(installDir, "victim-dir"); - await chmod(victimDir, 0o700); - await symlink(join("..", "..", "victim-dir"), join(installDir, "packages", "dep", "payload")); + // The bin link itself is still created. + const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-bin"); + expect((await lstat(binLink)).isSymbolicLink()).toBe(true); - await using proc = spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); + }, + 60000, +); + +it.skipIf(isWindows)( + "does not change permissions of a directory reached through a symlinked bin target with a trailing slash", + async () => { + // Same as above, but the bin target is written as "payload/". The kernel + // follows a symlink named with a trailing slash even when chmod is told not + // to follow symlinks, so the installer has to drop the slash before it + // stats, links and chmods the target. + using dir = tempDir("bin-target-symlink-trailing-slash-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-symlink-slash-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-symlinked-dir-bin", + version: "1.0.0", + bin: { "dep-with-symlinked-dir-bin": "./payload/" }, + }), + "victim-dir/keep": "", + }); + const installDir = String(dir); - expect((await stat(victimDir)).mode & 0o777).toBe(0o700); + const victimDir = join(installDir, "victim-dir"); + await chmod(victimDir, 0o700); + await symlink(join("..", "..", "victim-dir"), join(installDir, "packages", "dep", "payload")); - // Without the slash this target links (see above), so with it the result is the same. - const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-dir-bin"); - expect(await readlink(binLink)).toBe(join("..", "dep-with-symlinked-dir-bin", "payload")); + await using proc = spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); -}, 60000); + expect((await stat(victimDir)).mode & 0o777).toBe(0o700); -it("skips a package bin entry whose name contains a NUL byte and links the remaining entries", async () => { - using dir = tempDir("bin-name-nul-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-name-nul-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-nul-bin", - version: "1.0.0", - bin: { ["extra" + String.fromCharCode(0) + "ignoredtail"]: "./cli.js", "good-bin": "./cli.js" }, - }), - "packages/dep/cli.js": `#!/usr/bin/env node\nconsole.log("ok");\n`, - }); - const installDir = String(dir); + // Without the slash this target links (see above), so with it the result is the same. + const binLink = join(installDir, "node_modules", ".bin", "dep-with-symlinked-dir-bin"); + expect(await readlink(binLink)).toBe(join("..", "dep-with-symlinked-dir-bin", "payload")); - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); + }, + 60000, +); + +it.skipIf(isWindows)( + "skips a package bin entry whose name contains a NUL byte and links the remaining entries", + async () => { + using dir = tempDir("bin-name-nul-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-name-nul-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-nul-bin", + version: "1.0.0", + bin: { ["extra" + String.fromCharCode(0) + "ignoredtail"]: "./cli.js", "good-bin": "./cli.js" }, + }), + "packages/dep/cli.js": `#!/usr/bin/env node\nconsole.log("ok");\n`, + }); + const installDir = String(dir); - expect((await readdir(join(installDir, "node_modules", ".bin"))).sort()).toEqual(["good-bin"]); + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); -}, 60000); + expect((await readdir(join(installDir, "node_modules", ".bin"))).sort()).toEqual(["good-bin"]); -it("does not link a bin target that resolves outside the package through a symlinked directory", async () => { - using dir = tempDir("bin-target-symlinked-dir-test", { - "bunfig.toml": `[install]\nlinker = "hoisted"\n`, - "package.json": JSON.stringify({ - name: "bin-target-dir-app", - version: "1.0.0", - workspaces: ["packages/*"], - }), - "packages/dep/package.json": JSON.stringify({ - name: "dep-with-linked-dir-bin", - version: "1.0.0", - bin: { "linked-dir-tool": "lnk/tool.js" }, - }), - }); - const installDir = await realpath(String(dir)); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); + }, + 60000, +); + +it.skipIf(isWindows)( + "does not link a bin target that resolves outside the package through a symlinked directory", + async () => { + using dir = tempDir("bin-target-symlinked-dir-test", { + "bunfig.toml": `[install]\nlinker = "hoisted"\n`, + "package.json": JSON.stringify({ + name: "bin-target-dir-app", + version: "1.0.0", + workspaces: ["packages/*"], + }), + "packages/dep/package.json": JSON.stringify({ + name: "dep-with-linked-dir-bin", + version: "1.0.0", + bin: { "linked-dir-tool": "lnk/tool.js" }, + }), + }); + const installDir = await realpath(String(dir)); - const outsideDir = `${installDir}/abcdefghijkl${installDir}/packages/dep/y`; - await mkdir(outsideDir, { recursive: true }); - const toolPath = join(outsideDir, "tool.js"); - await writeFile(toolPath, `#!/usr/bin/env node\nconsole.log("ok");\n`); - await chmod(toolPath, 0o600); - await symlink(outsideDir, join(installDir, "packages", "dep", "lnk")); + const outsideDir = `${installDir}/abcdefghijkl${installDir}/packages/dep/y`; + await mkdir(outsideDir, { recursive: true }); + const toolPath = join(outsideDir, "tool.js"); + await writeFile(toolPath, `#!/usr/bin/env node\nconsole.log("ok");\n`); + await chmod(toolPath, 0o600); + await symlink(outsideDir, join(installDir, "packages", "dep", "lnk")); - await using proc = Bun.spawn({ - cmd: [bunExe(), "install"], - cwd: installDir, - stdout: "pipe", - stderr: "pipe", - env, - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + await using proc = Bun.spawn({ + cmd: [bunExe(), "install"], + cwd: installDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect((await stat(toolPath)).mode & 0o777).toBe(0o600); - expect(await readdir(join(installDir, "node_modules", ".bin")).catch(() => [])).toEqual([]); + expect((await stat(toolPath)).mode & 0o777).toBe(0o600); + expect(await readdir(join(installDir, "node_modules", ".bin")).catch(() => [])).toEqual([]); - if (exitCode !== 0) { - console.error("Install failed with exit code:", exitCode); - console.error("stdout:", stdout); - console.error("stderr:", stderr); - } - expect(exitCode).toBe(0); -}, 60000); + if (exitCode !== 0) { + console.error("Install failed with exit code:", exitCode); + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(exitCode).toBe(0); + }, + 60000, +); // The cache folder and the bun.lock entry of a `github:` package are named after the root // directory of its tarball: the first component of the first entry's path, whatever that @@ -1454,7 +1470,7 @@ describe.concurrent("links below node_modules with the hoisted linker", () => { // A `bin` key is a free-form file name. Through a link at `.bin`, a regular file of that // name was unlinked and replaced by a link that dangles there. - it("does not replace files in the target of node_modules/.bin", async () => { + it.skipIf(isWindows)("does not replace files in the target of node_modules/.bin", async () => { using dir = tempDir("nm-bin-files", { "package.json": JSON.stringify({ name: "bin-dir-symlink-app", @@ -1639,7 +1655,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { }); // The victim has every file an installed package is known by. Linking a bin sets the mode of its file. - it.each([ + it.skipIf(isWindows).each([ ["node_modules", ["@x", "dep"]], ["node_modules/@scope", ["dep"]], ["node_modules/@scope/", []], @@ -1686,7 +1702,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { // People link every `node_modules` of a monorepo to a volume or a cache disk, not only the root's. // A first install resets the `node_modules` of the members, but not with `--filter`. - it.each([ + it.skipIf(isWindows).each([ ["", []], [" with --filter", ["--filter", "evil"]], ] as const)("still installs through a link at a workspace member's node_modules%s", async (_, args) => { @@ -1956,7 +1972,7 @@ describe.concurrent("links below node_modules with the isolated linker", () => { // A link at the `node_modules` of the project or of a workspace is followed: people keep theirs on a // volume or a cache disk. Not all that is behind such a link need be bun's. -describe.concurrent("the target of a link at node_modules", () => { +describe.concurrent.skipIf(isWindows)("the target of a link at node_modules", () => { const MINE: Record = { "notes.txt": KEEP, ".hidden": KEEP, diff --git a/test/cli/lint/config-files.test.ts b/test/cli/lint/config-files.test.ts index ca650745abf9..027407209285 100644 --- a/test/cli/lint/config-files.test.ts +++ b/test/cli/lint/config-files.test.ts @@ -1280,7 +1280,7 @@ test.concurrent.each([ }); // Same as ESLint 10.12.0 and oxlint 1.87.0, which load a config in Node. On Windows the preload still runs. -test.each([ +test.skipIf(isWindows).each([ ["eslint.config.mjs", (rules: string) => `export default [{ rules: ${rules} }];`], ["oxlint.config.ts", (rules: string) => `export default { categories: { correctness: "off" }, rules: ${rules} };`], ])("a `preload` in bunfig.toml doesn't run before %s", async (name, config) => { @@ -1360,7 +1360,7 @@ describe.concurrent("`process.argv` in a config file is bun lint's command line" }); // It isn't passed on the child process's command line or in its environment, because there's no room for it twice. - test("works with a one-megabyte command line", async () => { + test.skipIf(isWindows)("works with a one-megabyte command line", async () => { const names = Array.from({ length: 15_000 }, (_, index) => `a${index}`.padEnd(40, "b")); const args = [...names.flatMap(it => ["--global", it]), "a.js"]; const rules = `{ eqeqeq: process.argv.length === ${1 + before.length + args.length} ? "error" : "off" }`; @@ -5819,7 +5819,7 @@ describe.concurrent("a config that asks for something unsupported", () => { }); // Windows doesn't allow these file names. - test("escapes control characters in printed file names", async () => { + test.skipIf(isWindows)("escapes control characters in printed file names", async () => { const files = { "eslint.config.mjs": `export default [ { files: ["**/*.one"], plugins: { p: { languages: { l: {} } } }, language: "p/l" }, @@ -7092,7 +7092,7 @@ describe.concurrent("a repository's info/exclude, without an oxlint config", () }); // Uses a file with mode 000, which root could still open. -describe.skipIf(process.getuid?.() === 0).concurrent("a file that can't be opened", () => { +describe.skipIf(isWindows || process.getuid?.() === 0).concurrent("a file that can't be opened", () => { const files = { "a.js": code, "locked.js": code, "sub/b.js": code }; const before = (dir: string) => chmodSync(join(dir, "locked.js"), 0); diff --git a/test/cli/lint/explain.test.ts b/test/cli/lint/explain.test.ts index c80a3f6fb3a8..bd78c171ff9e 100644 --- a/test/cli/lint/explain.test.ts +++ b/test/cli/lint/explain.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "fs"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join } from "path"; const pagesDir = join(import.meta.dir, "../../../docs/lint/rules"); @@ -222,7 +222,7 @@ describe.concurrent("--rules", () => { expect(exitCode).toBe(0); }); - test("groups rules by plugin and adds descriptions in a terminal", async () => { + test.skipIf(isWindows)("groups rules by plugin and adds descriptions in a terminal", async () => { const [{ output, exitCode }, rules] = await Promise.all([lintInTerminal(["--rules"]), listRules()]); const lines = output.split("\n"); const count = (scope: string) => rules.filter(it => it.scope === scope).length; @@ -374,7 +374,7 @@ describe.concurrent("--explain", () => { expect(Bun.stripANSI(colored.stdout)).toContain("no-var"); }); - test("the URL is a hyperlink in a terminal", async () => { + test.skipIf(isWindows)("the URL is a hyperlink in a terminal", async () => { const { output, exitCode } = await lintInTerminal(["--explain", "no-var"], { NO_COLOR: undefined }); expect(output).toContain(`\x1b]8;;${base}eslint/no-var\x1b\\`); expect(exitCode).toBe(0); diff --git a/test/cli/lint/git.test.ts b/test/cli/lint/git.test.ts index 077f5af844f2..e3fdbebd08ea 100644 --- a/test/cli/lint/git.test.ts +++ b/test/cli/lint/git.test.ts @@ -301,42 +301,45 @@ describe.concurrent.each(commands)("bun %s", command => { }); // On macOS /tmp is such a link, and so is $PWD in a shell that got to the repository through one. - test("files that are named through a link to the repository are selected, written and staged", async () => { - using repo = await repository({ "src/a.js": tidy, "src/partly.js": tidy, "b.js": tidy }); - const { root } = repo; - using outside = tempDir("bun-lint-git-link", {}); - // The link is above the repository. `bun fmt` doesn't take an argument that is itself a link, like Prettier. - symlinkSync(join(root, ".."), join(String(outside), "link")); - const link = join(realpathSync(String(outside)), "link", basename(root)); - for (const name of ["src/a.js", "src/partly.js"]) write(root, name, before); - await git(root, "add", "-A"); - write(root, "src/partly.js", before + "\n"); - write(root, "b.js", before); - - // The names are printed relative to the real working directory, and go through the link. - const found = async (cwd: string, args: string[]) => { - const { stdout, stderr, exitCode } = await bun(cwd, [command, ...args, "--list-files"]); - expect({ exitCode, stderr: exitCode === 0 ? "" : stderr }).toEqual({ exitCode: 0, stderr: "" }); - const names = stdout.split("\n").filter(it => it !== ""); - return names.map(it => relative(root, realpathSync(resolve(root, it)))).sort(); - }; - for (const cwd of [root, link]) { - for (const flag of ["--staged", "--changed"]) { - const also = flag === "--changed" ? ["b.js"] : []; - expect(await found(cwd, [flag, link])).toEqual([...also, "src/a.js", "src/partly.js"]); - expect(await found(cwd, [flag, join(link, "src")])).toEqual(["src/a.js", "src/partly.js"]); - expect(await found(cwd, [flag, join(link, "src/a.js")])).toEqual(["src/a.js"]); + test.skipIf(isWindows)( + "files that are named through a link to the repository are selected, written and staged", + async () => { + using repo = await repository({ "src/a.js": tidy, "src/partly.js": tidy, "b.js": tidy }); + const { root } = repo; + using outside = tempDir("bun-lint-git-link", {}); + // The link is above the repository. `bun fmt` doesn't take an argument that is itself a link, like Prettier. + symlinkSync(join(root, ".."), join(String(outside), "link")); + const link = join(realpathSync(String(outside)), "link", basename(root)); + for (const name of ["src/a.js", "src/partly.js"]) write(root, name, before); + await git(root, "add", "-A"); + write(root, "src/partly.js", before + "\n"); + write(root, "b.js", before); + + // The names are printed relative to the real working directory, and go through the link. + const found = async (cwd: string, args: string[]) => { + const { stdout, stderr, exitCode } = await bun(cwd, [command, ...args, "--list-files"]); + expect({ exitCode, stderr: exitCode === 0 ? "" : stderr }).toEqual({ exitCode: 0, stderr: "" }); + const names = stdout.split("\n").filter(it => it !== ""); + return names.map(it => relative(root, realpathSync(resolve(root, it)))).sort(); + }; + for (const cwd of [root, link]) { + for (const flag of ["--staged", "--changed"]) { + const also = flag === "--changed" ? ["b.js"] : []; + expect(await found(cwd, [flag, link])).toEqual([...also, "src/a.js", "src/partly.js"]); + expect(await found(cwd, [flag, join(link, "src")])).toEqual(["src/a.js", "src/partly.js"]); + expect(await found(cwd, [flag, join(link, "src/a.js")])).toEqual(["src/a.js"]); + } } - } - const { stderr, exitCode } = await bun(root, [...writing[command], "--staged", join(link, "src")]); - expect(stderr).toContain("src/partly.js has both staged and unstaged changes"); - expect(exitCode).toBe(2); - expect(read(root, "src/a.js")).toBe(after[command]); - expect(read(root, "src/partly.js")).toBe(before + "\n"); - // a.js was staged again, so only what was unstaged before still is. - expect(await git(root, "diff", "--name-only")).toBe("b.js\nsrc/partly.js\n"); - }); + const { stderr, exitCode } = await bun(root, [...writing[command], "--staged", join(link, "src")]); + expect(stderr).toContain("src/partly.js has both staged and unstaged changes"); + expect(exitCode).toBe(2); + expect(read(root, "src/a.js")).toBe(after[command]); + expect(read(root, "src/partly.js")).toBe(before + "\n"); + // a.js was staged again, so only what was unstaged before still is. + expect(await git(root, "diff", "--name-only")).toBe("b.js\nsrc/partly.js\n"); + }, + ); test("a fully staged file is staged again after it's rewritten", async () => { using repo = await repository({ "a.js": tidy, "sub/b.js": tidy, "c.js": tidy }); diff --git a/test/cli/lint/help.test.ts b/test/cli/lint/help.test.ts index ace206018b96..7a1457dd5f2d 100644 --- a/test/cli/lint/help.test.ts +++ b/test/cli/lint/help.test.ts @@ -724,13 +724,13 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} expect(await whatRan(files, args, "apps/web", { from: "." })).toBe("the command"); }); - test.skipIf(command === "check")("in `a\\b`, --cwd=missing doesn't resolve to a/b/missing", async () => { + test.skipIf(isWindows || command === "check")("in `a\\b`, --cwd=missing doesn't resolve to a/b/missing", async () => { const result = await bun({ "a/b/missing/a.ts": source }, ["--cwd=missing", command, ...flags, "a.ts"], "a\\b"); expect(result.stderr).toContain('Could not change directory to "missing"'); expect(result.exitCode).toBe(1); }); - test("an absolute --cwd works when the working directory was deleted", async () => { + test.skipIf(isWindows)("an absolute --cwd works when the working directory was deleted", async () => { using dir = tempDir("bun-no-script", { "project/a.ts": source, "deleted/a.ts": "" }); const args = [`--cwd=${join(String(dir), "project")}`, command, ...flags, "a.ts"]; await using proc = Bun.spawn({ @@ -744,7 +744,7 @@ STARTED=1 exec "${bunExe()}" ${again.join(" ")} expect(exitCode).toBe(1); }); - test("an absolute --cwd can contain `//`, `.` and `..`", async () => { + test.skipIf(isWindows)("an absolute --cwd can contain `//`, `.` and `..`", async () => { using dir = tempDir("bun-no-script", { "project/a.ts": source }); const cwd = `/..//${String(dir).slice(1)}/./missing/../project//`; await using proc = Bun.spawn({ diff --git a/test/cli/lint/init.test.ts b/test/cli/lint/init.test.ts index c896ad7513b6..abec9786575f 100644 --- a/test/cli/lint/init.test.ts +++ b/test/cli/lint/init.test.ts @@ -1,6 +1,6 @@ // Tests for `bun lint --init` and `bun fmt --init`. import { afterAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { chmodSync, existsSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { endChildren, spawn } from "../children"; @@ -153,7 +153,7 @@ describe.concurrent.each(["lint", "fmt"] as const)("bun %s --init", command => { expect(readFileSync(join(String(dir), "bunfig.toml"), "utf8")).toBe("a = 1\nb = \n"); }); - const cannotBeKeptFromWriting = process.getuid?.() === 0; + const cannotBeKeptFromWriting = isWindows || process.getuid?.() === 0; test.skipIf(cannotBeKeptFromWriting)("a read-only bunfig.toml is an error", async () => { using dir = tempDir("bun-init-section", { "bunfig.toml": "a = 1\n" }); diff --git a/test/cli/lint/lint.test.ts b/test/cli/lint/lint.test.ts index 791423c0d527..89500fef5022 100644 --- a/test/cli/lint/lint.test.ts +++ b/test/cli/lint/lint.test.ts @@ -11857,20 +11857,23 @@ describe.concurrent("a script, file or executable named lint", () => { }); // `bun run` adds that directory to PATH. - test("a lint executable next to package.json wins over the linter, even from a subdirectory", async () => { - using dir = tempDir("bun-lint-name", { - ...files, - "package.json": "{}", - "lint": `#!/bin/sh\necho the executable\n`, - "sub/b.js": "debugger;\n", - }); - chmodSync(join(String(dir), "lint"), 0o755); - const cwd = join(String(dir), "sub"); - await using proc = spawn({ cmd: [bunExe(), "lint"], env, cwd, stdout: "pipe", stderr: "pipe" }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout.trim()).toBe("the executable"); - expect(exitCode).toBe(0); - }); + test.skipIf(isWindows)( + "a lint executable next to package.json wins over the linter, even from a subdirectory", + async () => { + using dir = tempDir("bun-lint-name", { + ...files, + "package.json": "{}", + "lint": `#!/bin/sh\necho the executable\n`, + "sub/b.js": "debugger;\n", + }); + chmodSync(join(String(dir), "lint"), 0o755); + const cwd = join(String(dir), "sub"); + await using proc = spawn({ cmd: [bunExe(), "lint"], env, cwd, stdout: "pipe", stderr: "pipe" }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe("the executable"); + expect(exitCode).toBe(0); + }, + ); // git stores a symlink to `/dev/zero` unchanged. `bun run` stops at such a file and finds no script in it. test("a package.json that's a symlink to a device has no script, and the parent directory's package.json isn't used", async () => { diff --git a/test/cli/run/cpu-prof.test.ts b/test/cli/run/cpu-prof.test.ts index 14d402a8fb2c..75df02a8a199 100644 --- a/test/cli/run/cpu-prof.test.ts +++ b/test/cli/run/cpu-prof.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readdirSync, readFileSync } from "fs"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import { join } from "path"; // Every workload below is time-bounded for 100ms. On Windows JSC's @@ -130,7 +130,7 @@ describe.concurrent("--cpu-prof", () => { // POSIX 5000 bytes exceeds the fixed path buffer (Linux 4096, macOS 1024); // 2500 + 2500 exercises the combined bound with components that fit // individually on Linux. - test.each([ + test.skipIf(isWindows).each([ ["--cpu-prof-dir", ["--cpu-prof-dir", Buffer.alloc(5000, "d").toString()]], ["--cpu-prof-name", ["--cpu-prof-name", Buffer.alloc(5000, "n").toString()]], [ @@ -446,7 +446,7 @@ describe.concurrent("--cpu-prof", () => { }); // "%80" decodes to a byte that isn't valid UTF-8. Windows decodes it to U+FFFD. - test("--cpu-prof-md prints a source's URL when the URL can't be converted to a path", async () => { + test.skipIf(isWindows)("--cpu-prof-md prints a source's URL when the URL can't be converted to a path", async () => { using dir = tempDir("cpu-prof-md-source-url", { "test.js": ` const loop = require("node:vm").runInThisContext( diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 9ef05a3f5857..9ffc7f8e2160 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -781,7 +781,7 @@ describe.concurrent(".env that is not a regular file", () => { // The resolver's directory listing drops a FIFO entry, so the loader only // sees one through a symlink. Without O_NONBLOCK the open blocks until a // writer appears. - test("FIFO behind a symlink", async () => { + test.skipIf(isWindows)("FIFO behind a symlink", async () => { using dir = tempDir("dotenv-fifo", files); mkfifo(path.join(String(dir), "fifo")); fs.symlinkSync("fifo", path.join(String(dir), ".env")); @@ -842,7 +842,7 @@ describe.concurrent("--env-file that is not a regular file", () => { // `sh` makes a real pipe. `Bun.spawn({ stdin: "pipe" })` is a socket pair, // which `/dev/stdin` cannot reopen on Linux. - test("a pipe through /dev/stdin", async () => { + test.skipIf(isWindows)("a pipe through /dev/stdin", async () => { using dir = tempDir("dotenv-arg-stdin", files); const script = await run(String(dir), [ @@ -856,7 +856,7 @@ describe.concurrent("--env-file that is not a regular file", () => { expect(script.exitCode).toBe(0); }); - test("a pipe in a comma list, and the process env still wins", async () => { + test.skipIf(isWindows)("a pipe in a comma list, and the process env still wins", async () => { using dir = tempDir("dotenv-arg-stdin-list", files); const script = await run( @@ -874,7 +874,7 @@ describe.concurrent("--env-file that is not a regular file", () => { expect(script.exitCode).toBe(0); }); - test("process substitution", async () => { + test.skipIf(isWindows)("process substitution", async () => { using dir = tempDir("dotenv-arg-procsub", files); const script = await run(String(dir), ["bash", "-c", '"$0" --env-file=<(echo BUNTEST_SUBST=1) index.ts', bunExe()]); @@ -885,7 +885,7 @@ describe.concurrent("--env-file that is not a regular file", () => { // The writer blocks in open(2) until bun opens the FIFO for reading, and bun // blocks in open(2) until a writer arrives. Either order works, as with `cat`. - test("a FIFO", async () => { + test.skipIf(isWindows)("a FIFO", async () => { using dir = tempDir("dotenv-arg-fifo", files); mkfifo(path.join(String(dir), "fifo")); await using writer = Bun.spawn({ @@ -1141,7 +1141,7 @@ test("NODE_ENV default is not propogated in bun run", () => { }); for (const shell of ["system", "bun"]) { - const isWindowsCMD = false; + const isWindowsCMD = isWindows && shell === "system"; const env = { ENV_FILE_NAME: "N/A", diff --git a/test/cli/run/filter-workspace.test.ts b/test/cli/run/filter-workspace.test.ts index 383f46fd23c1..3d177c7f66fc 100644 --- a/test/cli/run/filter-workspace.test.ts +++ b/test/cli/run/filter-workspace.test.ts @@ -609,7 +609,7 @@ describe("bun", () => { { signal: constants.signals.SIGUSR1, label: "SIGUSR1" }, ...(isLinux ? [{ signal: 40, label: "UNKNOWN" }] : []), ]; - test.each(signaled)("exit code of a script killed by signal $signal", ({ signal, label }) => { + test.skipIf(isWindows).each(signaled)("exit code of a script killed by signal $signal", ({ signal, label }) => { using dir = tempDir("testworkspace", { dep0: { "package.json": JSON.stringify({ name: "dep0", scripts: { script: `kill -${signal} $$` } }), @@ -755,7 +755,7 @@ describe("bun", () => { }); // The terminal renderer is TTY-only on Windows (see runElideLinesTest). - test("terminal output reports how long a successful script took", () => { + test.skipIf(isWindows)("terminal output reports how long a successful script took", () => { using dir = tempDir("filter-done-in", { packages: { dep0: { @@ -850,7 +850,7 @@ describe("bun", () => { describe("selectors", () => { // A Windows command line is too short for a path longer than the path buffer. - test("rejects a path longer than the path buffer", () => { + test.skipIf(isWindows)("rejects a path longer than the path buffer", () => { const long = "./" + Array(21).fill(Buffer.alloc(200, "a").toString()).join("/"); runInCwdFailure(graph_root, long, "present", /error: --filter "\.\/a.*" path is too long/); }); @@ -1448,7 +1448,7 @@ describe("output timing", () => { // On abort (here: SIGINT), exit alone finishes a script; waiting for pipe // EOF would hang on the detached child that still holds go's stdout. - test("SIGINT does not wait for a child holding the script's pipes", async () => { + test.skipIf(isWindows)("SIGINT does not wait for a child holding the script's pipes", async () => { using dir = tempDir("filter-abort-late", { "package.json": JSON.stringify({ name: "ws-abort", @@ -1565,7 +1565,7 @@ describe("auto-discovered bunfig.toml [run] section", () => { // Elision only happens on a terminal. On POSIX FORCE_COLOR=1 turns the // terminal renderer on for a pipe; on Windows it stays off (see runElideLinesTest). - test("[run] elide-lines enables elision for --filter", () => { + test.skipIf(isWindows)("[run] elide-lines enables elision for --filter", () => { using dir = workspace("filter-bunfig-elide", "[run]\nelide-lines = 15\n"); const r = run(String(dir), ["run", "--filter", "dep0", "lines"], { FORCE_COLOR: "1", NO_COLOR: "0" }); expect(r.stdout).toMatch(/\[5 lines elided\]/); @@ -1617,7 +1617,7 @@ test.concurrent.each([ }); // Windows paths can't be as long as MAX_PATH_BYTES. -describe.concurrent("workspace paths longer than the path buffer", () => { +describe.concurrent.skipIf(isWindows)("workspace paths longer than the path buffer", () => { const workspace = { "package.json": JSON.stringify({ name: "ws", workspaces: ["packages/*"] }), "packages/ok/package.json": JSON.stringify({ name: "ok", scripts: { present: "echo ok-ran" } }), diff --git a/test/cli/run/garbage-env.test.ts b/test/cli/run/garbage-env.test.ts index 96a39f0641bf..1ec09ceb0bc3 100644 --- a/test/cli/run/garbage-env.test.ts +++ b/test/cli/run/garbage-env.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { bunExe, tempDir } from "harness"; +import { bunExe, isPosix, tempDir } from "harness"; import path from "path"; -describe("garbage env", () => { +describe.if(isPosix)("garbage env", () => { test("garbage env", async () => { const cfile = path.join(import.meta.dirname, "garbage-env.c"); // Compile into a temp dir so the binary never lands in the repo root. diff --git a/test/cli/run/multi-run.test.ts b/test/cli/run/multi-run.test.ts index 2d1a349a9a6f..32cc82ed21ad 100644 --- a/test/cli/run/multi-run.test.ts +++ b/test/cli/run/multi-run.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { realpathSync } from "fs"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import path from "path"; // Helper: spawn bun with multi-run flags, returns { stdout, stderr, exitCode } @@ -1009,7 +1009,7 @@ describe.concurrent("timing edge cases", () => { // Same shape, sh syntax: "orphan": "sleep 30 & echo ..." must not block // --parallel completion until the backgrounded sleep exits. - test("a backgrounded helper does not block --parallel completion", async () => { + test.skipIf(isWindows)("a backgrounded helper does not block --parallel completion", async () => { using dir = tempDir("mr-bg-helper", { "package.json": JSON.stringify({ scripts: { @@ -1294,7 +1294,7 @@ describe("abort: failure kills long-running processes", () => { expect(elapsed).toBeLessThan(15000); }); - test("parallel: signaled process shows Signaled message", async () => { + test.skipIf(isWindows)("parallel: signaled process shows Signaled message", async () => { using dir = tempDir("mr-abort-signal", { "package.json": JSON.stringify({ scripts: { diff --git a/test/cli/run/no-orphans.test.ts b/test/cli/run/no-orphans.test.ts index f8de819e2e81..e6b8f6481f7c 100644 --- a/test/cli/run/no-orphans.test.ts +++ b/test/cli/run/no-orphans.test.ts @@ -201,7 +201,7 @@ test.concurrent.skipIf(!isSupported)( // Linux this is covered by Bun setting linux_pdeathsig on every spawn when // no-orphans mode is enabled (prctl in the vfork child before exec). On macOS // it's covered by the libproc descendant walk in the exit handler. -test.concurrent( +test.concurrent.skipIf(!isPosix)( "BUN_FEATURE_FLAG_NO_ORPHANS=1: non-Bun grandchildren are reaped when bun dies with its parent", async () => { const { sh, bunPid, grandchildPid } = await spawnTree("1", "child-nonbun.js"); @@ -331,7 +331,7 @@ describe.concurrent.each([ }, }, ])("bun run --no-orphans $label: supervisor SIGKILLed", ({ runArgs, files }) => { - test.concurrent("bun run and the script exit", async () => { + test.concurrent.skipIf(!isPosix)("bun run and the script exit", async () => { using dir = tempDir("no-orphans-run", files as DirectoryTree); const env: NodeJS.Dict = { ...bunEnv }; delete env.BUN_FEATURE_FLAG_NO_ORPHANS; @@ -582,7 +582,7 @@ test.concurrent.skipIf(!isLinux || !hasPerl)( // the grandchild on its own clean exit. Uses a non-Bun grandchild so the test // doesn't depend on env-var inheritance — proves the descendant walk runs from // the `bun run` process itself. -test.concurrent("bun run --no-orphans