[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat!: rewrite pools without `tinypool` (#8705)

Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>

authored by

Ari Perkkiö
Vladimir Sheremet
and committed by
GitHub
(Oct 21, 2025, 7:12 PM +0200) 4822d047 9dc18673

+2457 -3113
+1 -1
CONTRIBUTING.md
··· 149 149 150 150 If there are libraries that are needed and don't comply with our size 151 151 requirements, a fork can be tried to reduce its size while we work with them to 152 - upstream our changes (see [tinypool](https://github.com/tinylibs/tinypool) for example) 152 + upstream our changes. 153 153 154 154 ### Think before adding yet another option 155 155
-1
README.md
··· 41 41 - [JSDOM](https://github.com/jsdom/jsdom) and [happy-dom](https://github.com/capricorn86/happy-dom) for DOM and browser API mocking 42 42 - [Browser Mode](https://vitest.dev/guide/browser/) for running component tests in the browser 43 43 - Components testing ([Vue](https://github.com/vitest-tests/browser-examples/tree/main/examples/vue), [React](https://github.com/vitest-tests/browser-examples/tree/main/examples/react), [Svelte](https://github.com/vitest-tests/browser-examples/tree/main/examples/svelte), [Lit](./examples/lit), [Marko](https://github.com/marko-js/examples/tree/master/examples/library-ts)) 44 - - Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)) 45 44 - Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench) 46 45 - [Projects](https://vitest.dev/guide/projects) support 47 46 - [expect-type](https://github.com/mmkal/expect-type) for type-level testing
-9
pnpm-lock.yaml
··· 1011 1011 tinyglobby: 1012 1012 specifier: 'catalog:' 1013 1013 version: 0.2.15 1014 - tinypool: 1015 - specifier: ^2.0.0 1016 - version: 2.0.0 1017 1014 tinyrainbow: 1018 1015 specifier: 'catalog:' 1019 1016 version: 3.0.3 ··· 8816 8813 peerDependenciesMeta: 8817 8814 picocolors: 8818 8815 optional: true 8819 - 8820 - tinypool@2.0.0: 8821 - resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==} 8822 - engines: {node: ^20.0.0 || >=22.0.0} 8823 8816 8824 8817 tinyrainbow@3.0.3: 8825 8818 resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} ··· 17642 17635 js-tokens: 8.0.2 17643 17636 optionalDependencies: 17644 17637 picocolors: 1.1.1 17645 - 17646 - tinypool@2.0.0: {} 17647 17638 17648 17639 tinyrainbow@3.0.3: {} 17649 17640
+5 -1
docs/.vitepress/config.ts
··· 52 52 ['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }], 53 53 ['link', { rel: 'icon', href: '/logo.svg', sizes: 'any', type: 'image/svg+xml' }], 54 54 ['meta', { name: 'author', content: `${teamMembers.map(c => c.name).join(', ')} and ${vitestName} contributors` }], 55 - ['meta', { name: 'keywords', content: 'vitest, vite, test, coverage, snapshot, react, vue, preact, svelte, solid, lit, marko, ruby, cypress, puppeteer, jsdom, happy-dom, test-runner, jest, typescript, esm, tinypool, tinyspy, node' }], 55 + ['meta', { name: 'keywords', content: 'vitest, vite, test, coverage, snapshot, react, vue, preact, svelte, solid, lit, marko, ruby, cypress, puppeteer, jsdom, happy-dom, test-runner, jest, typescript, esm, tinyspy, node' }], 56 56 ['meta', { property: 'og:title', content: vitestName }], 57 57 ['meta', { property: 'og:description', content: vitestDescription }], 58 58 ['meta', { property: 'og:url', content: ogUrl }], ··· 627 627 link: '/guide/improving-performance', 628 628 }, 629 629 ], 630 + }, 631 + { 632 + text: 'Recipes', 633 + link: '/guide/recipes', 630 634 }, 631 635 ] 632 636 }
+90 -39
docs/advanced/pool.md
··· 1 1 # Custom Pool 2 2 3 3 ::: warning 4 - This is an advanced and very low-level API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. 4 + This is an advanced, experimental and very low-level API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors. 5 5 ::: 6 6 7 - Vitest runs tests in pools. By default, there are several pools: 7 + Vitest runs tests in a pool. By default, there are several pool runners: 8 8 9 9 - `threads` to run tests using `node:worker_threads` (isolation is provided with a new worker context) 10 10 - `forks` to run tests using `node:child_process` (isolation is provided with a new `child_process.fork` process) ··· 12 12 - `browser` to run tests using browser providers 13 13 - `typescript` to run typechecking on tests 14 14 15 - You can provide your own pool by specifying a file path: 15 + ::: tip 16 + See [`vitest-pool-example`](https://www.npmjs.com/package/vitest-pool-example) for example of a custom pool runner implementation. 17 + ::: 18 + 19 + ## Usage 20 + 21 + You can provide your own pool runner by a function that returns `PoolRunnerInitializer`. 16 22 17 23 ```ts [vitest.config.ts] 18 24 import { defineConfig } from 'vitest/config' 25 + import customPool from './my-custom-pool.ts' 19 26 20 27 export default defineConfig({ 21 28 test: { 22 29 // will run every file with a custom pool by default 23 - pool: './my-custom-pool.ts', 24 - // you can provide options using `poolOptions` object 25 - poolOptions: { 26 - myCustomPool: { 27 - customProperty: true, 28 - }, 29 - }, 30 + pool: customPool({ 31 + customProperty: true, 32 + }) 30 33 }, 31 34 }) 32 35 ``` ··· 34 37 If you need to run tests in different pools, use the [`projects`](/guide/projects) feature: 35 38 36 39 ```ts [vitest.config.ts] 40 + import customPool from './my-custom-pool.ts' 41 + 37 42 export default defineConfig({ 38 43 test: { 39 44 projects: [ ··· 43 48 pool: 'threads', 44 49 }, 45 50 }, 51 + { 52 + extends: true, 53 + test: { 54 + pool: customPool({ 55 + customProperty: true, 56 + }) 57 + } 58 + } 46 59 ], 47 60 }, 48 61 }) ··· 50 63 51 64 ## API 52 65 53 - The file specified in `pool` option should export a function (can be async) that accepts `Vitest` interface as its first option. This function needs to return an object matching `ProcessPool` interface: 66 + The `pool` option accepts a `PoolRunnerInitializer` that can be used for custom pool runners. The `name` property should indicate name of the custom pool runner. It should be identical with your worker's `name` property. 54 67 55 - ```ts 56 - import type { ProcessPool, TestSpecification } from 'vitest/node' 68 + ```ts [my-custom-pool.ts] 69 + import type { PoolRunnerInitializer } from 'vitest/node' 57 70 58 - export interface ProcessPool { 59 - name: string 60 - runTests: (files: TestSpecification[], invalidates?: string[]) => Promise<void> 61 - collectTests: (files: TestSpecification[], invalidates?: string[]) => Promise<void> 62 - close?: () => Promise<void> 71 + export function customPool(customOptions: CustomOptions): PoolRunnerInitializer { 72 + return { 73 + name: 'custom-pool', 74 + createPoolWorker: options => new CustomPoolWorker(options, customOptions), 75 + } 63 76 } 64 77 ``` 65 78 66 - The function is called only once (unless the server config was updated), and it's generally a good idea to initialize everything you need for tests inside that function and reuse it when `runTests` is called. 79 + In your `CustomPoolWorker` you need to define all required methods: 67 80 68 - Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`projects`](/guide/projects) configuration. 81 + ```ts [my-custom-pool.ts] 82 + import type { PoolOptions, PoolWorker, WorkerRequest } from 'vitest/node' 69 83 70 - Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onTestRunEnd`](/advanced/reporters) only after `runTests` is resolved). 84 + class CustomPoolWorker implements PoolWorker { 85 + name = 'custom-pool' 86 + private customOptions: CustomOptions 71 87 72 - If you are using a custom pool, you will have to provide test files and their results yourself - you can reference [`vitest.state`](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/state.ts) for that (most important are `collectFiles` and `updateTasks`). Vitest uses `startTests` function from `@vitest/runner` package to do that. 88 + constructor(options: PoolOptions, customOptions: CustomOptions) { 89 + this.customOptions = customOptions 90 + } 73 91 74 - Vitest will call `collectTests` if `vitest.collect` is called or `vitest list` is invoked via a CLI command. It works the same way as `runTests`, but you don't have to run test callbacks, only report their tasks by calling `vitest.state.collectFiles(files)`. 92 + send(message: WorkerRequest): void { 93 + // Provide way to send your worker a message 94 + } 75 95 76 - To communicate between different processes, you can create methods object using `createMethodsRPC` from `vitest/node`, and use any form of communication that you prefer. For example, to use WebSockets with `birpc` you can write something like this: 96 + on(event: string, callback: (arg: any) => void): void { 97 + // Provide way to listen to your workers events, e.g. message, error, exit 98 + } 77 99 78 - ```ts 79 - import { createBirpc } from 'birpc' 80 - import { parse, stringify } from 'flatted' 81 - import { createMethodsRPC, TestProject } from 'vitest/node' 100 + off(event: string, callback: (arg: any) => void): void { 101 + // Provide way to unsubscribe `on` listeners 102 + } 82 103 83 - function createRpc(project: TestProject, wss: WebSocketServer) { 84 - return createBirpc( 85 - createMethodsRPC(project), 86 - { 87 - post: msg => wss.send(msg), 88 - on: fn => wss.on('message', fn), 89 - serialize: stringify, 90 - deserialize: parse, 91 - }, 92 - ) 104 + async start() { 105 + // do something when the worker is started 106 + } 107 + 108 + async stop() { 109 + // cleanup the state 110 + } 111 + 112 + deserialize(data) { 113 + return data 114 + } 93 115 } 94 116 ``` 95 117 96 - You can see a simple example of a pool made from scratch that doesn't run tests but marks them as collected in [pool/custom-pool.ts](https://github.com/vitest-dev/vitest/blob/main/test/cli/fixtures/custom-pool/pool/custom-pool.ts). 118 + Your `CustomPoolRunner` will be controlling how your custom test runner worker life cycles and communication channel works. For example, your `CustomPoolRunner` could launch a `node:worker_threads` `Worker`, and provide communication via `Worker.postMessage` and `parentPort`. 119 + 120 + In your worker file, you can import helper utilities from `vitest/worker`: 121 + 122 + ```ts [my-worker.ts] 123 + import { init, runBaseTests } from 'vitest/worker' 124 + 125 + init({ 126 + post: (response) => { 127 + // Provide way to send this message to CustomPoolRunner's onWorker as message event 128 + }, 129 + on: (callback) => { 130 + // Provide a way to listen CustomPoolRunner's "postMessage" calls 131 + }, 132 + off: (callback) => { 133 + // Optional, provide a way to remove listeners added by "on" calls 134 + }, 135 + teardown: () => { 136 + // Optional, provide a way to teardown worker, e.g. unsubscribe all the `on` listeners 137 + }, 138 + serialize: (value) => { 139 + // Optional, provide custom serializer for `post` calls 140 + }, 141 + deserialize: (value) => { 142 + // Optional, provide custom deserializer for `on` callbacks 143 + }, 144 + runTests: state => runBaseTests('run', state), 145 + collectTests: state => runBaseTests('collect', state), 146 + }) 147 + ```
+18 -219
docs/config/index.md
··· 674 674 Write test results to a file when the `--reporter=json`, `--reporter=html` or `--reporter=junit` option is also specified. 675 675 By providing an object instead of a string you can define individual outputs when using multiple reporters. 676 676 677 - ### pool<NonProjectOption /> {#pool} 677 + ### pool 678 678 679 679 - **Type:** `'threads' | 'forks' | 'vmThreads' | 'vmForks'` 680 680 - **Default:** `'forks'` ··· 682 682 683 683 Pool used to run tests in. 684 684 685 - #### threads<NonProjectOption /> 685 + #### threads 686 686 687 - Enable multi-threading using [tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)). When using threads you are unable to use process related APIs such as `process.chdir()`. Some libraries written in native languages, such as Prisma, `bcrypt` and `canvas`, have problems when running in multiple threads and run into segfaults. In these cases it is advised to use `forks` pool instead. 687 + Enable multi-threading. When using threads you are unable to use process related APIs such as `process.chdir()`. Some libraries written in native languages, such as Prisma, `bcrypt` and `canvas`, have problems when running in multiple threads and run into segfaults. In these cases it is advised to use `forks` pool instead. 688 688 689 - #### forks<NonProjectOption /> 689 + #### forks 690 690 691 - Similar as `threads` pool but uses `child_process` instead of `worker_threads` via [tinypool](https://github.com/tinylibs/tinypool). Communication between tests and main process is not as fast as with `threads` pool. Process related APIs such as `process.chdir()` are available in `forks` pool. 691 + Similar as `threads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and main process is not as fast as with `threads` pool. Process related APIs such as `process.chdir()` are available in `forks` pool. 692 692 693 - #### vmThreads<NonProjectOption /> 693 + #### vmThreads 694 694 695 695 Run tests using [VM context](https://nodejs.org/api/vm.html) (inside a sandboxed environment) in a `threads` pool. 696 696 697 - This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`poolOptions.vmThreads.memoryLimit`](#pooloptions-vmthreads-memorylimit) value. 697 + This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`vmMemoryLimit`](#vmMemorylimit) value. 698 698 699 699 ::: warning 700 700 Running code in a sandbox has some advantages (faster tests), but also comes with a number of disadvantages. ··· 716 716 Please, be aware of these issues when using this option. Vitest team cannot fix any of the issues on our side. 717 717 ::: 718 718 719 - #### vmForks<NonProjectOption /> 719 + #### vmForks 720 720 721 - Similar as `vmThreads` pool but uses `child_process` instead of `worker_threads` via [tinypool](https://github.com/tinylibs/tinypool). Communication between tests and the main process is not as fast as with `vmThreads` pool. Process related APIs such as `process.chdir()` are available in `vmForks` pool. Please be aware that this pool has the same pitfalls listed in `vmThreads`. 721 + Similar as `vmThreads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and the main process is not as fast as with `vmThreads` pool. Process related APIs such as `process.chdir()` are available in `vmForks` pool. Please be aware that this pool has the same pitfalls listed in `vmThreads`. 722 722 723 - ### poolOptions<NonProjectOption /> {#pooloptions} 724 - 725 - - **Type:** `Record<'threads' | 'forks' | 'vmThreads' | 'vmForks', {}>` 726 - - **Default:** `{}` 727 - 728 - #### poolOptions.threads 729 - 730 - Options for `threads` pool. 731 - 732 - ```ts 733 - import { defineConfig } from 'vitest/config' 734 - 735 - export default defineConfig({ 736 - test: { 737 - poolOptions: { 738 - threads: { 739 - // Threads related options here 740 - } 741 - } 742 - } 743 - }) 744 - ``` 745 - 746 - ##### poolOptions.threads.maxThreads<NonProjectOption /> 747 - 748 - - **Type:** `number | string` 749 - - **Default:** _available CPUs_ 750 - 751 - Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. 752 - 753 - ##### poolOptions.threads.singleThread 754 - 755 - - **Type:** `boolean` 756 - - **Default:** `false` 757 - 758 - Run all tests with the same environment inside a single worker thread. This will disable built-in module isolation (your source code or [inlined](#server-deps-inline) code will still be reevaluated for each test), but can improve test performance. 759 - 760 - :::warning 761 - Even though this option will force tests to run one after another, this option is different from Jest's `--runInBand`. Vitest uses workers not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself. 762 - 763 - This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that. 764 - ::: 765 - 766 - ##### poolOptions.threads.useAtomics<NonProjectOption /> 767 - 768 - - **Type:** `boolean` 769 - - **Default:** `false` 770 - 771 - Use Atomics to synchronize threads. 772 - 773 - This can improve performance in some cases, but might cause segfault in older Node versions. 774 - 775 - ##### poolOptions.threads.isolate 776 - 777 - - **Type:** `boolean` 778 - - **Default:** `true` 779 - 780 - Isolate environment for each test file. 781 - 782 - ##### poolOptions.threads.execArgv<NonProjectOption /> 723 + ### execArgv 783 724 784 725 - **Type:** `string[]` 785 726 - **Default:** `[]` 786 727 787 - Pass additional arguments to `node` in the threads. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 728 + Pass additional arguments to `node` in the runner worker. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 788 729 789 730 :::warning 790 731 Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103. 791 732 ::: 792 733 793 - #### poolOptions.forks 794 - 795 - Options for `forks` pool. 796 - 797 - ```ts 798 - import { defineConfig } from 'vitest/config' 799 - 800 - export default defineConfig({ 801 - test: { 802 - poolOptions: { 803 - forks: { 804 - // Forks related options here 805 - } 806 - } 807 - } 808 - }) 809 - ``` 810 - 811 - ##### poolOptions.forks.maxForks<NonProjectOption /> 812 - 813 - - **Type:** `number | string` 814 - - **Default:** _available CPUs_ 815 - 816 - Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable. 817 - 818 - ##### poolOptions.forks.isolate 819 - 820 - - **Type:** `boolean` 821 - - **Default:** `true` 822 - 823 - Isolate environment for each test file. 824 - 825 - ##### poolOptions.forks.singleFork 826 - 827 - - **Type:** `boolean` 828 - - **Default:** `false` 829 - 830 - Run all tests with the same environment inside a single child process. This will disable built-in module isolation (your source code or [inlined](#server-deps-inline) code will still be reevaluated for each test), but can improve test performance. 831 - 832 - :::warning 833 - Even though this option will force tests to run one after another, this option is different from Jest's `--runInBand`. Vitest uses child processes not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself. 834 - 835 - This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that. 836 - ::: 837 - 838 - ##### poolOptions.forks.execArgv<NonProjectOption /> 839 - 840 - - **Type:** `string[]` 841 - - **Default:** `[]` 842 - 843 - Pass additional arguments to `node` process in the child processes. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 844 - 845 - :::warning 846 - Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103. 847 - ::: 848 - 849 - #### poolOptions.vmThreads 850 - 851 - Options for `vmThreads` pool. 852 - 853 - ```ts 854 - import { defineConfig } from 'vitest/config' 855 - 856 - export default defineConfig({ 857 - test: { 858 - poolOptions: { 859 - vmThreads: { 860 - // VM threads related options here 861 - } 862 - } 863 - } 864 - }) 865 - ``` 866 - 867 - ##### poolOptions.vmThreads.maxThreads<NonProjectOption /> 868 - 869 - - **Type:** `number | string` 870 - - **Default:** _available CPUs_ 871 - 872 - Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. 873 - 874 - ##### poolOptions.vmThreads.memoryLimit<NonProjectOption /> 734 + ### vmMemoryLimit 875 735 876 736 - **Type:** `string | number` 877 737 - **Default:** `1 / CPU Cores` 738 + 739 + This option affects only `vmForks` and `vmThreads` pools. 878 740 879 741 Specifies the memory limit for workers before they are recycled. This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default. 880 742 ··· 900 762 Percentage based memory limit [does not work on Linux CircleCI](https://github.com/jestjs/jest/issues/11956#issuecomment-1212925677) workers due to incorrect system memory being reported. 901 763 ::: 902 764 903 - ##### poolOptions.vmThreads.useAtomics<NonProjectOption /> 904 - 905 - - **Type:** `boolean` 906 - - **Default:** `false` 907 - 908 - Use Atomics to synchronize threads. 909 - 910 - This can improve performance in some cases, but might cause segfault in older Node versions. 911 - 912 - ##### poolOptions.vmThreads.execArgv<NonProjectOption /> 913 - 914 - - **Type:** `string[]` 915 - - **Default:** `[]` 916 - 917 - Pass additional arguments to `node` process in the VM context. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 918 - 919 - :::warning 920 - Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103. 921 - ::: 922 - 923 - #### poolOptions.vmForks<NonProjectOption /> 924 - 925 - Options for `vmForks` pool. 926 - 927 - ```ts 928 - import { defineConfig } from 'vitest/config' 929 - 930 - export default defineConfig({ 931 - test: { 932 - poolOptions: { 933 - vmForks: { 934 - // VM forks related options here 935 - } 936 - } 937 - } 938 - }) 939 - ``` 940 - 941 - ##### poolOptions.vmForks.maxForks<NonProjectOption /> 942 - 943 - - **Type:** `number | string` 944 - - **Default:** _available CPUs_ 945 - 946 - Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable. 947 - 948 - ##### poolOptions.vmForks.memoryLimit<NonProjectOption /> 949 - 950 - - **Type:** `string | number` 951 - - **Default:** `1 / CPU Cores` 952 - 953 - Specifies the memory limit for workers before they are recycled. This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default. How the value is calculated is described in [`poolOptions.vmThreads.memoryLimit`](#pooloptions-vmthreads-memorylimit) 954 - 955 - ##### poolOptions.vmForks.execArgv<NonProjectOption /> 956 - 957 - - **Type:** `string[]` 958 - - **Default:** `[]` 959 - 960 - Pass additional arguments to `node` process in the VM context. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 961 - 962 - :::warning 963 - Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103. 964 - ::: 965 - 966 - ### fileParallelism<NonProjectOption /> {#fileparallelism} 765 + ### fileParallelism 967 766 968 767 - **Type:** `boolean` 969 768 - **Default:** `true` ··· 975 774 This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/#describe-concurrent) or via [a config](#sequence-concurrent). 976 775 ::: 977 776 978 - ### maxWorkers<NonProjectOption /> {#maxworkers} 777 + ### maxWorkers 979 778 980 779 - **Type:** `number | string` 981 780 982 - Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. 781 + Maximum number or percentage of workers to run tests in. 983 782 984 783 ### testTimeout 985 784 ··· 2342 2141 Disabling this option might [improve performance](/guide/improving-performance) if your code doesn't rely on side effects (which is usually true for projects with `node` environment). 2343 2142 2344 2143 ::: tip 2345 - You can disable isolation for specific pools by using [`poolOptions`](#pooloptions) property. 2144 + You can disable isolation for specific test files by using Vitest workspaces and disabling isolation per project. 2346 2145 ::: 2347 2146 2348 2147 ### includeTaskLocation {#includeTaskLocation}
+8 -106
docs/guide/cli-generated.md
··· 390 390 391 391 Specify pool, if not running in the browser (default: `forks`) 392 392 393 - ### poolOptions.threads.isolate 393 + ### execArgv 394 394 395 - - **CLI:** `--poolOptions.threads.isolate` 396 - - **Config:** [poolOptions.threads.isolate](/config/#pooloptions-threads-isolate) 395 + - **CLI:** `--execArgv <option>` 396 + - **Config:** [execArgv](/config/#execargv) 397 397 398 - Isolate tests in threads pool (default: `true`) 398 + Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`. 399 399 400 - ### poolOptions.threads.singleThread 400 + ### vmMemoryLimit 401 401 402 - - **CLI:** `--poolOptions.threads.singleThread` 403 - - **Config:** [poolOptions.threads.singleThread](/config/#pooloptions-threads-singlethread) 402 + - **CLI:** `--vmMemoryLimit <limit>` 403 + - **Config:** [vmMemoryLimit](/config/#vmmemorylimit) 404 404 405 - Run tests inside a single thread (default: `false`) 406 - 407 - ### poolOptions.threads.maxThreads 408 - 409 - - **CLI:** `--poolOptions.threads.maxThreads <workers>` 410 - - **Config:** [poolOptions.threads.maxThreads](/config/#pooloptions-threads-maxthreads) 411 - 412 - Maximum number or percentage of threads to run tests in 413 - 414 - ### poolOptions.threads.useAtomics 415 - 416 - - **CLI:** `--poolOptions.threads.useAtomics` 417 - - **Config:** [poolOptions.threads.useAtomics](/config/#pooloptions-threads-useatomics) 418 - 419 - Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`) 420 - 421 - ### poolOptions.vmThreads.isolate 422 - 423 - - **CLI:** `--poolOptions.vmThreads.isolate` 424 - - **Config:** [poolOptions.vmThreads.isolate](/config/#pooloptions-vmthreads-isolate) 425 - 426 - Isolate tests in threads pool (default: `true`) 427 - 428 - ### poolOptions.vmThreads.singleThread 429 - 430 - - **CLI:** `--poolOptions.vmThreads.singleThread` 431 - - **Config:** [poolOptions.vmThreads.singleThread](/config/#pooloptions-vmthreads-singlethread) 432 - 433 - Run tests inside a single thread (default: `false`) 434 - 435 - ### poolOptions.vmThreads.maxThreads 436 - 437 - - **CLI:** `--poolOptions.vmThreads.maxThreads <workers>` 438 - - **Config:** [poolOptions.vmThreads.maxThreads](/config/#pooloptions-vmthreads-maxthreads) 439 - 440 - Maximum number or percentage of threads to run tests in 441 - 442 - ### poolOptions.vmThreads.useAtomics 443 - 444 - - **CLI:** `--poolOptions.vmThreads.useAtomics` 445 - - **Config:** [poolOptions.vmThreads.useAtomics](/config/#pooloptions-vmthreads-useatomics) 446 - 447 - Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`) 448 - 449 - ### poolOptions.vmThreads.memoryLimit 450 - 451 - - **CLI:** `--poolOptions.vmThreads.memoryLimit <limit>` 452 - - **Config:** [poolOptions.vmThreads.memoryLimit](/config/#pooloptions-vmthreads-memorylimit) 453 - 454 - Memory limit for VM threads pool. If you see memory leaks, try to tinker this value. 455 - 456 - ### poolOptions.forks.isolate 457 - 458 - - **CLI:** `--poolOptions.forks.isolate` 459 - - **Config:** [poolOptions.forks.isolate](/config/#pooloptions-forks-isolate) 460 - 461 - Isolate tests in forks pool (default: `true`) 462 - 463 - ### poolOptions.forks.singleFork 464 - 465 - - **CLI:** `--poolOptions.forks.singleFork` 466 - - **Config:** [poolOptions.forks.singleFork](/config/#pooloptions-forks-singlefork) 467 - 468 - Run tests inside a single child_process (default: `false`) 469 - 470 - ### poolOptions.forks.maxForks 471 - 472 - - **CLI:** `--poolOptions.forks.maxForks <workers>` 473 - - **Config:** [poolOptions.forks.maxForks](/config/#pooloptions-forks-maxforks) 474 - 475 - Maximum number or percentage of processes to run tests in 476 - 477 - ### poolOptions.vmForks.isolate 478 - 479 - - **CLI:** `--poolOptions.vmForks.isolate` 480 - - **Config:** [poolOptions.vmForks.isolate](/config/#pooloptions-vmforks-isolate) 481 - 482 - Isolate tests in forks pool (default: `true`) 483 - 484 - ### poolOptions.vmForks.singleFork 485 - 486 - - **CLI:** `--poolOptions.vmForks.singleFork` 487 - - **Config:** [poolOptions.vmForks.singleFork](/config/#pooloptions-vmforks-singlefork) 488 - 489 - Run tests inside a single child_process (default: `false`) 490 - 491 - ### poolOptions.vmForks.maxForks 492 - 493 - - **CLI:** `--poolOptions.vmForks.maxForks <workers>` 494 - - **Config:** [poolOptions.vmForks.maxForks](/config/#pooloptions-vmforks-maxforks) 495 - 496 - Maximum number or percentage of processes to run tests in 497 - 498 - ### poolOptions.vmForks.memoryLimit 499 - 500 - - **CLI:** `--poolOptions.vmForks.memoryLimit <limit>` 501 - - **Config:** [poolOptions.vmForks.memoryLimit](/config/#pooloptions-vmforks-memorylimit) 502 - 503 - Memory limit for VM forks pool. If you see memory leaks, try to tinker this value. 405 + Memory limit for VM pools. If you see memory leaks, try to tinker this value. 504 406 505 407 ### fileParallelism 506 408
+2 -12
docs/guide/debugging.md
··· 120 120 121 121 ```sh 122 122 # To run in a single worker 123 - vitest --inspect-brk --pool threads --poolOptions.threads.singleThread 124 - 125 - # To run in a single child process 126 - vitest --inspect-brk --pool forks --poolOptions.forks.singleFork 123 + vitest --inspect-brk --no-file-parallelism 127 124 128 125 # To run in browser mode 129 126 vitest --inspect-brk --browser --no-file-parallelism 130 127 ``` 131 128 132 - If you are using Vitest 1.1 or higher, you can also just provide `--no-file-parallelism` flag: 133 - 134 - ```sh 135 - # If pool is unknown 136 - vitest --inspect-brk --no-file-parallelism 137 - ``` 138 - 139 129 Once Vitest starts it will stop execution and wait for you to open developer tools that can connect to [Node.js inspector](https://nodejs.org/en/docs/guides/debugging-getting-started/). You can use Chrome DevTools for this by opening `chrome://inspect` on browser. 140 130 141 - In watch mode you can keep the debugger open during test re-runs by using the `--poolOptions.threads.isolate false` options. 131 + In watch mode you can keep the debugger open during test re-runs by using the `--isolate false` options.
+2 -3
docs/guide/features.md
··· 34 34 35 35 ## Threads 36 36 37 - By default Vitest runs test files in [multiple processes](/guide/parallelism) using [`node:child_process`](https://nodejs.org/api/child_process.html) via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)), allowing tests to run simultaneously. If you want to speed up your test suite even further, consider enabling `--pool=threads` to run tests using [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) (beware that some packages might not work with this setup). 38 - 39 - To run tests in a single thread or process, see [`poolOptions`](/config/#pooloptions). 37 + By default Vitest runs test files in [multiple processes](/guide/parallelism) using [`node:child_process`](https://nodejs.org/api/child_process.html), allowing tests to run simultaneously. If you want to speed up your test suite even further, consider enabling `--pool=threads` to run tests using [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) (beware that some packages might not work with this setup). 38 + To run tests in a single thread or process, see [`fileParallelism`](/config/#fileParallelism). 40 39 41 40 Vitest also isolates each file's environment so env mutations in one file don't affect others. Isolation can be disabled by passing `--no-isolate` to the CLI (trading correctness for run performance). 42 41
+28 -11
docs/guide/improving-performance.md
··· 20 20 export default defineConfig({ 21 21 test: { 22 22 isolate: false, 23 - // you can also disable isolation only for specific pools 24 - poolOptions: { 25 - forks: { 26 - isolate: false, 27 - }, 28 - }, 29 23 }, 30 24 }) 31 25 ``` 32 26 ::: 27 + 28 + You can also disable isolation for specific files only by using `projects`: 29 + 30 + ```ts [vitest.config.js] 31 + import { defineConfig } from 'vitest/config' 32 + 33 + export default defineConfig({ 34 + test: { 35 + projects: [ 36 + { 37 + name: 'Isolated', 38 + isolate: true, // (default value) 39 + exclude: ['**.non-isolated.test.ts'], 40 + }, 41 + { 42 + name: 'Non-isolated', 43 + isolate: false, 44 + include: ['**.non-isolated.test.ts'], 45 + } 46 + ] 47 + }, 48 + }) 49 + ``` 33 50 34 51 :::tip 35 52 If you are using `vmThreads` pool, you cannot disable isolation. Use `threads` pool instead to improve your tests performance. ··· 179 196 ```sh 180 197 # Example for splitting tests on 32 CPU to 4 shards. 181 198 # As each process needs 1 main thread, there's 7 threads for test runners (1+7)*4 = 32 182 - # Use VITEST_MAX_THREADS or VITEST_MAX_FORKS depending on the pool: 183 - VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=1/4 & \ 184 - VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=2/4 & \ 185 - VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=3/4 & \ 186 - VITEST_MAX_THREADS=7 vitest run --reporter=blob --shard=4/4 & \ 199 + # Use VITEST_MAX_WORKERS: 200 + VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=1/4 & \ 201 + VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=2/4 & \ 202 + VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=3/4 & \ 203 + VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=4/4 & \ 187 204 wait # https://man7.org/linux/man-pages/man2/waitpid.2.html 188 205 189 206 vitest run --merge-reports
+101 -1
docs/guide/migration.md
··· 271 271 Both `@vitest/browser/context` and `@vitest/browser/utils` work at runtime during the transition period, but they will be removed in a future release. 272 272 ::: 273 273 274 + ### Pool Rework 275 + 276 + Vitest has used [`tinypool`](https://github.com/tinylibs/tinypool) for orchestrating how test files are run in the test runner workers. Tinypool has controlled how complex tasks like parallelism, isolation and IPC communication works internally. However we've found that Tinypool has some flaws that are slowing down development of Vitest. In Vitest v4 we've completely removed Tinypool and rewritten how pools work without new dependencies. Read more about reasoning from [feat!: rewrite pools without tinypool #8705 277 + ](https://github.com/vitest-dev/vitest/pull/8705). 278 + 279 + New pool architecture allows Vitest to simplify many previously complex configuration options: 280 + 281 + - `maxThreads` and `maxForks` are now `maxWorkers`. 282 + - Environment variables `VITEST_MAX_THREADS` and `VITEST_MAX_FORKS` are now `VITEST_MAX_WORKERS`. 283 + - `singleThread` and `singleFork` are now `maxWorkers: 1`. 284 + - `poolOptions` is removed. All previous `poolOptions` are now top-level options. The `memoryLimit` of VM pools is renamed to `vmMemoryLimit`. 285 + - `threads.useAtomics` is removed. If you have a use case for this, feel free to open a new feature request. 286 + - Custom pool interface has been rewritten, see [Custom Pool](/advanced/pool.html#custom-pool) 287 + 288 + ```ts 289 + export default defineConfig({ 290 + test: { 291 + poolOptions: { // [!code --] 292 + forks: { // [!code --] 293 + execArgv: ['--expose-gc'], // [!code --] 294 + isolate: false, // [!code --] 295 + singleFork: true, // [!code --] 296 + }, // [!code --] 297 + vmThreads: { // [!code --] 298 + memoryLimit: '300Mb' // [!code --] 299 + }, // [!code --] 300 + }, // [!code --] 301 + execArgv: ['--expose-gc'], // [!code ++] 302 + isolate: false, // [!code ++] 303 + maxWorkers: 1, // [!code ++] 304 + memoryLimit: '300Mb', // [!code ++] 305 + } 306 + }) 307 + ``` 308 + 309 + Previously it was not possible to specify some pool related options per project when using [Vitest Projects](/guide/projects). With the new architecture this is no longer a blocker. 310 + 311 + ::: code-group 312 + ```ts [Isolation per project] 313 + import { defineConfig } from 'vitest/config' 314 + 315 + export default defineConfig({ 316 + test: { 317 + projects: [ 318 + { 319 + // Non-isolated unit tests 320 + name: 'Unit tests', 321 + isolate: false, 322 + exclude: ['**.integration.test.ts'], 323 + }, 324 + { 325 + // Isolated integration tests 326 + name: 'Integration tests', 327 + include: ['**.integration.test.ts'], 328 + }, 329 + ], 330 + }, 331 + }) 332 + ``` 333 + ```ts [Parallel & Sequential projects] 334 + import { defineConfig } from 'vitest/config' 335 + 336 + export default defineConfig({ 337 + test: { 338 + projects: [ 339 + { 340 + name: 'Parallel', 341 + exclude: ['**.sequantial.test.ts'], 342 + }, 343 + { 344 + name: 'Sequential', 345 + include: ['**.sequantial.test.ts'], 346 + fileParallelism: false, 347 + }, 348 + ], 349 + }, 350 + }) 351 + ``` 352 + ```ts [Node CLI options per project] 353 + import { defineConfig } from 'vitest/config' 354 + 355 + export default defineConfig({ 356 + test: { 357 + projects: [ 358 + { 359 + name: 'Production env', 360 + execArgv: ['--env-file=.env.prod'] 361 + }, 362 + { 363 + name: 'Staging env', 364 + execArgv: ['--env-file=.env.staging'] 365 + }, 366 + ], 367 + }, 368 + }) 369 + ``` 370 + ::: 371 + 372 + See [Recipes](/guide/recipes) for more examples. 373 + 274 374 ### Reporter Updates 275 375 276 376 Reporter APIs `onCollected`, `onSpecsCollected`, `onPathsCollected`, `onTaskUpdate` and `onFinished` were removed. See [`Reporters API`](/advanced/api/reporters) for new alternatives. The new APIs were introduced in Vitest `v3.0.0`. ··· 424 524 425 525 ### Envs 426 526 427 - Just like Jest, Vitest sets `NODE_ENV` to `test`, if it wasn't set before. Vitest also has a counterpart for `JEST_WORKER_ID` called `VITEST_POOL_ID` (always less than or equal to `maxThreads`), so if you rely on it, don't forget to rename it. Vitest also exposes `VITEST_WORKER_ID` which is a unique ID of a running worker - this number is not affected by `maxThreads`, and will increase with each created worker. 527 + Just like Jest, Vitest sets `NODE_ENV` to `test`, if it wasn't set before. Vitest also has a counterpart for `JEST_WORKER_ID` called `VITEST_POOL_ID` (always less than or equal to `maxWorkers`), so if you rely on it, don't forget to rename it. Vitest also exposes `VITEST_WORKER_ID` which is a unique ID of a running worker - this number is not affected by `maxWorkers`, and will increase with each created worker. 428 528 429 529 ### Replace property 430 530
+1 -1
docs/guide/parallelism.md
··· 12 12 - `forks` (the default) and `vmForks` run tests in different [child processes](https://nodejs.org/api/child_process.html) 13 13 - `threads` and `vmThreads` run tests in different [worker threads](https://nodejs.org/api/worker_threads.html) 14 14 15 - Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`maxWorkers`](/config/#maxworkers) option. Or more granually with [`poolOptions`](/config/#pooloptions) configuration. 15 + Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`maxWorkers`](/config/#maxworkers) option. 16 16 17 17 If you have a lot of tests, it is usually faster to run them in parallel, but it also depends on the project, the environment and [isolation](/config/#isolate) state. To disable file parallelisation, you can set [`fileParallelism`](/config/#fileparallelism) to `false`. To learn more about possible performance improvements, read the [Performance Guide](/guide/improving-performance). 18 18
+9 -40
docs/guide/profiling-test-performance.md
··· 34 34 The `--prof` option does not work with `pool: 'threads'` due to `node:worker_threads` limitations. 35 35 ::: 36 36 37 - To pass these options to Vitest's test runner, define `poolOptions.<pool>.execArgv` in your Vitest configuration: 37 + To pass these options to Vitest's test runner, define `execArgv` in your Vitest configuration: 38 38 39 - ::: code-group 40 - ```ts [Forks] 39 + ```ts 41 40 import { defineConfig } from 'vitest/config' 42 41 43 42 export default defineConfig({ 44 43 test: { 45 - pool: 'forks', 46 - poolOptions: { 47 - forks: { 48 - execArgv: [ 49 - '--cpu-prof', 50 - '--cpu-prof-dir=test-runner-profile', 51 - '--heap-prof', 52 - '--heap-prof-dir=test-runner-profile' 53 - ], 54 - 55 - // To generate a single profile 56 - singleFork: true, 57 - }, 58 - }, 44 + fileParallelism: false, 45 + execArgv: [ 46 + '--cpu-prof', 47 + '--cpu-prof-dir=test-runner-profile', 48 + '--heap-prof', 49 + '--heap-prof-dir=test-runner-profile' 50 + ], 59 51 }, 60 52 }) 61 53 ``` 62 - ```ts [Threads] 63 - import { defineConfig } from 'vitest/config' 64 - 65 - export default defineConfig({ 66 - test: { 67 - pool: 'threads', 68 - poolOptions: { 69 - threads: { 70 - execArgv: [ 71 - '--cpu-prof', 72 - '--cpu-prof-dir=test-runner-profile', 73 - '--heap-prof', 74 - '--heap-prof-dir=test-runner-profile' 75 - ], 76 - 77 - // To generate a single profile 78 - singleThread: true, 79 - }, 80 - }, 81 - }, 82 - }) 83 - ``` 84 - ::: 85 54 86 55 After the tests have run there should be a `test-runner-profile/*.cpuprofile` and `test-runner-profile/*.heapprofile` files generated. See [Inspecting profiling records](#inspecting-profiling-records) for instructions how to analyze these files. 87 56
+55
docs/guide/recipes.md
··· 1 + --- 2 + title: Recipes | Guide 3 + --- 4 + 5 + # Recipes 6 + 7 + ## Disabling isolation for specific test files only 8 + 9 + You can speed up your test run by disabling isolation for specific set of files by specifying `isolate` per `projects` entries: 10 + 11 + ```ts [vitest.config.ts] 12 + import { defineConfig } from 'vitest/config' 13 + 14 + export default defineConfig({ 15 + test: { 16 + projects: [ 17 + { 18 + // Non-isolated unit tests 19 + name: 'Unit tests', 20 + isolate: false, 21 + exclude: ['**.integration.test.ts'], 22 + }, 23 + { 24 + // Isolated integration tests 25 + name: 'Integration tests', 26 + include: ['**.integration.test.ts'], 27 + }, 28 + ], 29 + }, 30 + }) 31 + ``` 32 + 33 + ## Parallel and Sequential test files 34 + 35 + You can split test files into parallel and sequential groups by using `projects` option: 36 + 37 + ```ts [vitest.config.ts] 38 + import { defineConfig } from 'vitest/config' 39 + 40 + export default defineConfig({ 41 + test: { 42 + projects: [ 43 + { 44 + name: 'Parallel', 45 + exclude: ['**.sequantial.test.ts'], 46 + }, 47 + { 48 + name: 'Sequential', 49 + include: ['**.sequantial.test.ts'], 50 + fileParallelism: false, 51 + }, 52 + ], 53 + }, 54 + }) 55 + ```
+1 -1
docs/guide/test-context.md
··· 409 409 410 410 The `worker` scope will run the fixture once per worker. The number of running workers depends on various factors. By default, every file runs in a separate worker, so `file` and `worker` scopes work the same way. 411 411 412 - However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) or [`poolOptions`](/config/#pooloptions) configuration. 412 + However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) configuration. 413 413 414 414 Note that specifying `scope: 'worker'` when running tests in `vmThreads` or `vmForks` will work the same way as `scope: 'file'`. This limitation exists because every test file has its own VM context, so if Vitest were to initiate it once, one context could leak to another and create many reference inconsistencies (instances of the same class would reference different constructors, for example). 415 415
+10 -32
examples/profiling/vitest.config.ts
··· 5 5 watch: false, 6 6 globalSetup: './global-setup.ts', 7 7 8 - // Switch between forks|threads 9 - pool: 'forks', 8 + // Generate a single profile 9 + fileParallelism: false, 10 10 11 - poolOptions: { 12 - threads: { 13 - execArgv: [ 14 - // https://nodejs.org/api/cli.html#--cpu-prof 15 - '--cpu-prof', 16 - '--cpu-prof-dir=threads-profile', 11 + execArgv: [ 12 + // https://nodejs.org/api/cli.html#--cpu-prof 13 + '--cpu-prof', 14 + '--cpu-prof-dir=vitest-profile', 17 15 18 - // https://nodejs.org/api/cli.html#--heap-prof 19 - '--heap-prof', 20 - '--heap-prof-dir=threads-profile', 21 - ], 22 - 23 - // Generate a single profile 24 - singleThread: true, 25 - }, 26 - 27 - forks: { 28 - execArgv: [ 29 - // https://nodejs.org/api/cli.html#--cpu-prof 30 - '--cpu-prof', 31 - '--cpu-prof-dir=forks-profile', 32 - 33 - // https://nodejs.org/api/cli.html#--heap-prof 34 - '--heap-prof', 35 - '--heap-prof-dir=forks-profile', 36 - ], 37 - 38 - // Generate a single profile 39 - singleFork: true, 40 - }, 41 - }, 16 + // https://nodejs.org/api/cli.html#--heap-prof 17 + '--heap-prof', 18 + '--heap-prof-dir=vitest-profile', 19 + ], 42 20 }, 43 21 })
+4 -1
packages/vitest/package.json
··· 104 104 "./mocker": { 105 105 "types": "./dist/mocker.d.ts", 106 106 "default": "./dist/mocker.js" 107 + }, 108 + "./worker": { 109 + "types": "./worker.d.ts", 110 + "default": "./dist/worker.js" 107 111 } 108 112 }, 109 113 "main": "./dist/index.js", ··· 186 190 "tinybench": "^2.9.0", 187 191 "tinyexec": "^0.3.2", 188 192 "tinyglobby": "catalog:", 189 - "tinypool": "^2.0.0", 190 193 "tinyrainbow": "catalog:", 191 194 "vite": "^6.0.0 || ^7.0.0", 192 195 "why-is-node-running": "^2.3.0"
+8 -2
packages/vitest/rollup.config.js
··· 29 29 'spy': 'src/integrations/spy.ts', 30 30 'coverage': 'src/public/coverage.ts', 31 31 'reporters': 'src/public/reporters.ts', 32 + 'worker': 'src/public/worker.ts', 32 33 'module-runner': 'src/public/module-runner.ts', 33 34 'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts', 34 35 35 36 // for performance reasons we bundle them separately so we don't import everything at once 36 - 'worker-vm': 'src/runtime/worker-vm.ts', 37 - 'worker-base': 'src/runtime/worker-base.ts', 37 + // 'worker': 'src/runtime/worker.ts', 38 + 'workers/forks': 'src/runtime/workers/forks.ts', 39 + 'workers/threads': 'src/runtime/workers/threads.ts', 40 + 'workers/vmThreads': 'src/runtime/workers/vmThreads.ts', 41 + 'workers/vmForks': 'src/runtime/workers/vmForks.ts', 38 42 39 43 'workers/runVmTests': 'src/runtime/runVmTests.ts', 40 44 ··· 53 57 'reporters': 'src/public/reporters.ts', 54 58 'mocker': 'src/public/mocker.ts', 55 59 'snapshot': 'src/public/snapshot.ts', 60 + 'worker': 'src/public/worker.ts', 56 61 'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts', 57 62 } 58 63 ··· 68 73 'node:vm', 69 74 'node:http', 70 75 'node:console', 76 + 'node:events', 71 77 'inspector', 72 78 'vitest/optional-types.js', 73 79 'vitest/browser',
+1
packages/vitest/worker.d.ts
··· 1 + export * from './dist/worker.js'
+1 -5
test/browser/vitest.config.unit.mts
··· 5 5 test: { 6 6 include: ['specs/**/*.{spec,test}.ts'], 7 7 pool: 'threads', 8 - poolOptions: { 9 - threads: { 10 - singleThread: true, 11 - }, 12 - }, 8 + fileParallelism: false, 13 9 reporters: 'verbose', 14 10 setupFiles: ['./setup.unit.ts'], 15 11 // 3 is the maximum of browser instances - in a perfect world they will run in parallel
+1 -5
test/cli/vitest.config.ts
··· 7 7 reporters: ['verbose'], 8 8 testTimeout: 60_000, 9 9 globals: true, 10 - poolOptions: { 11 - forks: { 12 - singleFork: true, 13 - }, 14 - }, 10 + fileParallelism: false, 15 11 chaiConfig: { 16 12 truncateThreshold: 999, 17 13 },
+1 -8
test/config/vitest.config.ts
··· 7 7 reporters: ['verbose'], 8 8 testTimeout: 60_000, 9 9 pool: 'forks', 10 - poolOptions: { 11 - forks: { 12 - singleFork: true, 13 - }, 14 - threads: { 15 - singleThread: true, 16 - }, 17 - }, 10 + fileParallelism: false, 18 11 chaiConfig: { 19 12 truncateThreshold: 999, 20 13 },
+7 -8
test/core/vite.config.ts
··· 111 111 option: 'config-option', 112 112 }, 113 113 }, 114 - poolOptions: { 115 - threads: { 116 - execArgv: ['--experimental-wasm-modules'], 117 - }, 118 - forks: { 119 - execArgv: ['--experimental-wasm-modules'], 120 - }, 121 - }, 114 + execArgv: ['--experimental-wasm-modules'], 122 115 env: { 123 116 CUSTOM_ENV: 'foo', 124 117 }, ··· 151 144 return false 152 145 } 153 146 if (log.includes('run [...filters]')) { 147 + return false 148 + } 149 + if (log.includes('Cannot find module') && log.includes('/web-worker/some-invalid-path')) { 150 + return false 151 + } 152 + if (log.includes('Cannot find module') && log.includes('/web-worker/workerInvalid-path.ts')) { 154 153 return false 155 154 } 156 155 if (log.startsWith(`[vitest]`) && log.includes(`did not use 'function' or 'class' in its implementation`)) {
+9 -1
test/coverage-test/utils.ts
··· 10 10 import { playwright } from '@vitest/browser-playwright' 11 11 import libCoverage from 'istanbul-lib-coverage' 12 12 import { normalize } from 'pathe' 13 - import { vi, describe as vitestDescribe, test as vitestTest } from 'vitest' 13 + import { onTestFailed, vi, describe as vitestDescribe, test as vitestTest } from 'vitest' 14 + import { getCurrentTest } from 'vitest/suite' 14 15 import * as testUtils from '../test-utils' 15 16 16 17 export function test(name: string, fn: TestFunction, skip = false) { ··· 61 62 }, 62 63 }, 63 64 }) 65 + 66 + if (getCurrentTest()) { 67 + onTestFailed(() => { 68 + console.error('stderr:', result.stderr) 69 + console.error('stdout:', result.stdout) 70 + }) 71 + } 64 72 65 73 if (options.throwOnError) { 66 74 if (result.stderr !== '') {
+3 -6
test/coverage-test/vitest.config.ts
··· 71 71 ...config.test, 72 72 name: { label: 'istanbul-browser', color: 'blue' }, 73 73 env: { COVERAGE_PROVIDER: 'istanbul', COVERAGE_BROWSER: 'true' }, 74 + testTimeout: 15_000, 74 75 include: [ 75 76 BROWSER_TESTS, 76 77 ··· 101 102 ...config.test, 102 103 name: { label: 'v8-browser', color: 'red' }, 103 104 env: { COVERAGE_PROVIDER: 'v8', COVERAGE_BROWSER: 'true' }, 105 + testTimeout: 15_000, 104 106 include: [ 105 107 BROWSER_TESTS, 106 108 ··· 142 144 }, 143 145 }, 144 146 ], 145 - poolOptions: { 146 - threads: { 147 - // Tests may have side effects, e.g. writing files to disk, 148 - singleThread: true, 149 - }, 150 - }, 147 + fileParallelism: false, 151 148 onConsoleLog(log) { 152 149 if (log.includes('ERROR: Coverage for')) { 153 150 // Ignore threshold error messages
+2 -2
test/test-utils/index.ts
··· 2 2 import type { UserConfig as ViteUserConfig } from 'vite' 3 3 import type { WorkerGlobalState } from 'vitest' 4 4 import type { TestProjectConfiguration } from 'vitest/config' 5 - import type { TestModule, TestSpecification, TestUserConfig, Vitest, VitestRunMode } from 'vitest/node' 5 + import type { TestSpecification, TestUserConfig, Vitest, VitestRunMode } from 'vitest/node' 6 6 import { webcrypto as crypto } from 'node:crypto' 7 7 import fs from 'node:fs' 8 8 import { Readable, Writable } from 'node:stream' ··· 376 376 root, 377 377 ...vitest, 378 378 get results() { 379 - return (vitest.ctx?.state.getFiles() || []).map(file => vitest.ctx?.state.getReportedEntity(file) as TestModule) 379 + return vitest.ctx?.state.getTestModules() || [] 380 380 }, 381 381 } 382 382 }
+3 -3
test/workspaces/globalTest.ts
··· 36 36 37 37 try { 38 38 assert.ok(results.success) 39 - assert.equal(results.numTotalTestSuites, 28) 40 - assert.equal(results.numTotalTests, 33) 41 - assert.equal(results.numPassedTests, 33) 39 + assert.equal(results.numTotalTestSuites, 24) 40 + assert.equal(results.numTotalTests, 29) 41 + assert.equal(results.numPassedTests, 29) 42 42 assert.ok(results.coverageMap) 43 43 44 44 const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts'))
+6 -28
test/workspaces/vitest.config.ts
··· 60 60 }, 61 61 { 62 62 test: { 63 - name: 'Single thread pool', 63 + name: 'Non-parallel thread pool', 64 64 include: [ 65 65 './space-pools/threads.test.ts', 66 66 './space-pools/single-worker.test.ts', 67 67 ], 68 68 pool: 'threads', 69 - poolOptions: { threads: { singleThread: true } }, 69 + fileParallelism: false, 70 70 }, 71 71 }, 72 72 { 73 73 test: { 74 - name: 'Non-isolated thread pool #1', 75 - include: [ 76 - './space-pools/threads.test.ts', 77 - './space-pools/no-isolate.test.ts', 78 - ], 79 - pool: 'threads', 80 - poolOptions: { threads: { isolate: false } }, 81 - }, 82 - }, 83 - { 84 - test: { 85 - name: 'Non-isolated thread pool #2', 74 + name: 'Non-isolated thread pool', 86 75 include: [ 87 76 './space-pools/threads.test.ts', 88 77 './space-pools/no-isolate.test.ts', ··· 104 93 }, 105 94 { 106 95 test: { 107 - name: 'Single fork pool', 96 + name: 'Non-parallel fork pool', 108 97 include: [ 109 98 './space-pools/forks.test.ts', 110 99 './space-pools/single-worker.test.ts', 111 100 ], 112 101 pool: 'forks', 113 - poolOptions: { forks: { singleFork: true } }, 102 + fileParallelism: false, 114 103 }, 115 104 }, 116 105 { 117 106 test: { 118 - name: 'Non-isolated fork pool #1', 119 - include: [ 120 - './space-pools/forks.test.ts', 121 - './space-pools/no-isolate.test.ts', 122 - ], 123 - pool: 'forks', 124 - poolOptions: { forks: { isolate: false } }, 125 - }, 126 - }, 127 - { 128 - test: { 129 - name: 'Non-isolated fork pool #2', 107 + name: 'Non-isolated fork pool', 130 108 include: [ 131 109 './space-pools/forks.test.ts', 132 110 './space-pools/no-isolate.test.ts',
-1
docs/.vitepress/components/FeaturesList.vue
··· 10 10 <ListItem>Component testing for Vue, React, Svelte, Lit, Marko and more</ListItem> 11 11 <ListItem>Out-of-the-box TypeScript / JSX support</ListItem> 12 12 <ListItem>ESM first, top level await</ListItem> 13 - <ListItem>Workers multi-threading via <a target="_blank" href="https://github.com/tinylibs/tinypool" rel="noopener noreferrer">Tinypool</a></ListItem> 14 13 <ListItem>Benchmarking support with <a target="_blank" href="https://github.com/tinylibs/tinybench" rel="noopener noreferrer">Tinybench</a></ListItem> 15 14 <ListItem>Filtering, timeouts, concurrent for suite and tests</ListItem> 16 15 <ListItem><a href="/guide/projects">Projects</a> support</ListItem>
-1
packages/utils/src/source-map.ts
··· 23 23 '/vite-node/dist/', 24 24 '/vite-node/src/', 25 25 '/node_modules/chai/', 26 - '/node_modules/tinypool/', 27 26 '/node_modules/tinyspy/', 28 27 '/vite/dist/node/module-runner', 29 28 '/rolldown-vite/dist/node/module-runner',
-2
packages/vitest/src/defaults.ts
··· 58 58 watch: boolean 59 59 globals: boolean 60 60 environment: 'node' 61 - pool: 'forks' 62 61 clearMocks: boolean 63 62 restoreMocks: boolean 64 63 mockReset: boolean ··· 96 95 watch: !isCI && process.stdin.isTTY, 97 96 globals: false, 98 97 environment: 'node' as const, 99 - pool: 'forks' as const, 100 98 clearMocks: false, 101 99 restoreMocks: false, 102 100 mockReset: false,
+1 -1
test/cli/test/forks-channel.test.ts
··· 7 7 pool, 8 8 }) 9 9 10 - expect(stderr).toContain('⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯') 10 + expect(stderr).toContain('⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯') 11 11 expect(stderr).toContain('Error: [vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process\'s channel.') 12 12 })
+5 -13
test/cli/test/network-imports.test.ts
··· 2 2 import { runVitest } from '../../test-utils' 3 3 4 4 const config = { 5 - poolOptions: { 6 - threads: { 7 - execArgv: ['--experimental-network-imports'], 8 - }, 9 - forks: { 10 - execArgv: ['--experimental-network-imports'], 11 - }, 12 - vmThreads: { 13 - execArgv: ['--experimental-network-imports'], 14 - }, 15 - }, 5 + execArgv: ['--experimental-network-imports'], 16 6 // let vite serve public/slash@3.0.0.js 17 7 api: 9602, 18 8 } 19 9 20 10 const [major] = process.version.slice(1).split('.') 21 11 12 + // TODO: remove when we drop support for Node 20 22 13 // --experimental-network-imports was removed in Node 22 in favor of module loaders 23 14 it.runIf(Number(major) <= 20).each([ 24 15 'threads', 25 16 'forks', 26 17 'vmThreads', 27 18 ])('importing from network in %s', async (pool) => { 28 - const { stderr, exitCode } = await runVitest({ 19 + const { ctx, exitCode } = await runVitest({ 29 20 ...config, 30 21 root: './fixtures/network-imports', 31 22 pool, 32 23 }) 33 - expect(stderr).toBe('') 24 + expect(ctx!.state.getTestModules()).toHaveLength(1) 25 + expect(ctx!.state.getTestModules()[0].state()).toBe('passed') 34 26 expect(exitCode).toBe(0) 35 27 })
+4 -4
test/cli/test/signal.test.ts
··· 96 96 import { test } from 'vitest' 97 97 test('aborted', async ({ signal, task }) => { 98 98 return new Promise(resolve => { 99 - console.log('ready') 100 99 signal.addEventListener('abort', () => { 101 100 task.meta.aborted = true 102 101 resolve() 103 102 }) 103 + console.log('ready') 104 104 }) 105 105 }, Infinity) 106 106 `, ··· 126 126 { timeout: Infinity }, 127 127 async (number, { signal, task }) => { 128 128 return new Promise(resolve => { 129 - if (number === 3) { 130 - console.log('ready') 131 - } 132 129 signal.addEventListener('abort', () => { 133 130 task.meta.aborted = true 134 131 resolve() 135 132 }) 133 + if (number === 3) { 134 + console.log('ready') 135 + } 136 136 }) 137 137 }) 138 138 `,
+4 -10
test/config/test/bail.test.ts
··· 8 8 const pools: TestUserConfig[] = [ 9 9 { pool: 'threads' }, 10 10 { pool: 'forks' }, 11 - { pool: 'threads', poolOptions: { threads: { singleThread: true } } }, 11 + { pool: 'threads', fileParallelism: false }, 12 12 ] 13 13 14 14 if (process.platform !== 'win32') { ··· 42 42 for (const pool of pools) { 43 43 configs.push({ 44 44 ...pool, 45 - poolOptions: { 46 - threads: { 47 - ...pool.poolOptions?.threads, 48 - isolate, 49 - }, 50 - forks: { isolate }, 51 - }, 45 + isolate, 52 46 browser: { 53 47 ...pool.browser!, 54 48 isolate, ··· 65 59 }, 66 60 async () => { 67 61 const isParallel 68 - = (config.pool === 'threads' && config.poolOptions?.threads?.singleThread !== true) 69 - || (config.pool === 'forks' && config.poolOptions?.forks?.singleFork !== true) 62 + = (config.pool === 'threads' && config.fileParallelism !== false) 63 + || (config.pool === 'forks' && config.fileParallelism !== false) 70 64 || (config.browser?.enabled && config.browser.fileParallelism) 71 65 72 66 // THREADS here means that multiple tests are run parallel
-1
test/config/test/config-types.test-d.ts
··· 19 19 20 20 test('allows expected project fields on a project config', () => { 21 21 expectProjectTestConfig.toHaveProperty('pool') 22 - expectProjectTestConfig.toHaveProperty('poolOptions') 23 22 }) 24 23 }) 25 24
+1 -5
test/config/test/exec-args.test.ts
··· 23 23 root, 24 24 include: [fileToTest], 25 25 pool, 26 - poolOptions: { 27 - [pool]: { 28 - execArgv, 29 - }, 30 - }, 26 + execArgv: [...execArgv], 31 27 }) 32 28 33 29 expect(vitest.stdout).toContain(`✓ ${fileToTest}`)
+7 -7
test/config/test/failures.test.ts
··· 72 72 test('inspect requires changing pool and singleThread/singleFork', async () => { 73 73 const { stderr } = await runVitest({ inspect: true }) 74 74 75 - expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 75 + expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"') 76 76 }) 77 77 78 78 test('inspect cannot be used with multi-threading', async () => { 79 - const { stderr } = await runVitest({ inspect: true, pool: 'threads', poolOptions: { threads: { singleThread: false } } }) 79 + const { stderr } = await runVitest({ inspect: true, pool: 'threads', fileParallelism: true }) 80 80 81 - expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 81 + expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"') 82 82 }) 83 83 84 84 test('inspect in browser mode requires no-file-parallelism', async () => { 85 85 const { stderr } = await runVitest({ inspect: true, browser: { enabled: true, instances: [{ browser: 'chromium' }], provider: playwright() } }) 86 86 87 - expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 87 + expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"') 88 88 }) 89 89 90 90 test('inspect-brk cannot be used with multi processing', async () => { 91 - const { stderr } = await runVitest({ inspect: true, pool: 'forks', poolOptions: { forks: { singleFork: false } } }) 91 + const { stderr } = await runVitest({ inspect: true, pool: 'forks', fileParallelism: true }) 92 92 93 - expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 93 + expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism"') 94 94 }) 95 95 96 96 test('inspect-brk in browser mode requires no-file-parallelism', async () => { 97 97 const { stderr } = await runVitest({ inspectBrk: true, browser: { enabled: true, instances: [{ browser: 'chromium' }], provider: playwright() } }) 98 98 99 - expect(stderr).toMatch('Error: You cannot use --inspect-brk without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 99 + expect(stderr).toMatch('Error: You cannot use --inspect-brk without "--no-file-parallelism"') 100 100 }) 101 101 102 102 test('inspect and --inspect-brk cannot be used when not playwright + chromium', async () => {
-1
test/config/test/get-state.test.ts
··· 6 6 { isolate: true }, 7 7 { isolate: false, maxWorkers: 1 }, 8 8 { isolate: false, fileParallelism: false }, 9 - { isolate: false, poolOptions: { forks: { singleFork: true } } }, 10 9 ] satisfies TestUserConfig[])(`getState().testPath during collection %s`, async (config) => { 11 10 const result = await runVitest({ 12 11 root: './fixtures/get-state',
+5 -5
test/config/test/mixed-environments.test.ts
··· 4 4 import { runVitest } from '../../test-utils' 5 5 6 6 const configs: TestUserConfig[] = [ 7 - { pool: 'threads', poolOptions: { threads: { isolate: false, singleThread: true } } }, 8 - { pool: 'threads', poolOptions: { threads: { isolate: false, singleThread: false } } }, 9 - { pool: 'threads', poolOptions: { threads: { isolate: false, maxThreads: 1 } } }, 10 - { pool: 'forks', poolOptions: { forks: { isolate: true } } }, 11 - { pool: 'forks', poolOptions: { forks: { isolate: false } } }, 7 + { pool: 'threads', isolate: false, fileParallelism: false }, 8 + { pool: 'threads', isolate: false, fileParallelism: true }, 9 + { pool: 'threads', isolate: false, maxWorkers: 1 }, 10 + { pool: 'forks', isolate: true }, 11 + { pool: 'forks', isolate: false }, 12 12 ] 13 13 14 14 test.each(configs)('should isolate environments when %s', async (config) => {
+1 -5
test/config/test/node-sqlite.test.ts
··· 8 8 'vitest.config.ts': { 9 9 test: { 10 10 pool: 'forks', 11 - poolOptions: { 12 - forks: { 13 - execArgv: ['--experimental-sqlite'], 14 - }, 15 - }, 11 + execArgv: ['--experimental-sqlite'], 16 12 }, 17 13 }, 18 14 'basic.test.ts': ts`
-112
test/config/test/override.test.ts
··· 16 16 return v.config 17 17 } 18 18 19 - describe('correctly defines isolated flags', async () => { 20 - it('does not merge user-defined poolOptions with itself', async () => { 21 - const c = await config({}, { 22 - poolOptions: { 23 - array: [1, 2, 3], 24 - }, 25 - }) 26 - // Ensure poolOptions.array has not been merged with itself 27 - // Previously, this would have been [1,2,3,1,2,3] 28 - expect(c.poolOptions?.array).toMatchInlineSnapshot(` 29 - [ 30 - 1, 31 - 2, 32 - 3, 33 - ] 34 - `) 35 - }) 36 - it('prefers CLI poolOptions flags over config', async () => { 37 - const c = await config({ 38 - isolate: true, 39 - poolOptions: { 40 - threads: { 41 - isolate: false, 42 - }, 43 - forks: { 44 - isolate: false, 45 - }, 46 - }, 47 - }) 48 - expect(c.poolOptions?.threads?.isolate).toBe(false) 49 - expect(c.poolOptions?.forks?.isolate).toBe(false) 50 - expect(c.isolate).toBe(true) 51 - }) 52 - 53 - it('override CLI poolOptions flags over isolate', async () => { 54 - const c = await config({ 55 - isolate: false, 56 - poolOptions: { 57 - threads: { 58 - isolate: true, 59 - }, 60 - forks: { 61 - isolate: true, 62 - }, 63 - }, 64 - }, { 65 - poolOptions: { 66 - threads: { 67 - isolate: false, 68 - }, 69 - forks: { 70 - isolate: false, 71 - }, 72 - }, 73 - }) 74 - expect(c.poolOptions?.threads?.isolate).toBe(true) 75 - expect(c.poolOptions?.forks?.isolate).toBe(true) 76 - expect(c.isolate).toBe(false) 77 - }) 78 - 79 - it('override CLI isolate flag if poolOptions is not set via CLI', async () => { 80 - const c = await config({ 81 - isolate: true, 82 - }, { 83 - poolOptions: { 84 - threads: { 85 - isolate: false, 86 - }, 87 - forks: { 88 - isolate: false, 89 - }, 90 - }, 91 - }) 92 - expect(c.poolOptions?.threads?.isolate).toBe(true) 93 - expect(c.poolOptions?.forks?.isolate).toBe(true) 94 - expect(c.isolate).toBe(true) 95 - }) 96 - 97 - it('keeps user configured poolOptions if no CLI flag is provided', async () => { 98 - const c = await config({}, { 99 - poolOptions: { 100 - threads: { 101 - isolate: false, 102 - }, 103 - forks: { 104 - isolate: false, 105 - }, 106 - }, 107 - }) 108 - expect(c.poolOptions?.threads?.isolate).toBe(false) 109 - expect(c.poolOptions?.forks?.isolate).toBe(false) 110 - expect(c.isolate).toBe(true) 111 - }) 112 - 113 - it('isolate config value overrides poolOptions defaults', async () => { 114 - const c = await config({}, { 115 - isolate: false, 116 - }) 117 - expect(c.poolOptions?.threads?.isolate).toBe(false) 118 - expect(c.poolOptions?.forks?.isolate).toBe(false) 119 - expect(c.isolate).toBe(false) 120 - }) 121 - 122 - it('if no isolation is defined in the config, fallback ot undefined', async () => { 123 - const c = await config({}, {}) 124 - expect(c.poolOptions?.threads?.isolate).toBe(undefined) 125 - expect(c.poolOptions?.forks?.isolate).toBe(undefined) 126 - // set in configDefaults, so it's always defined 127 - expect(c.isolate).toBe(true) 128 - }) 129 - }) 130 - 131 19 describe('correctly defines api flag', () => { 132 20 it('CLI overrides disabling api', async () => { 133 21 const c = await vitest({ api: false }, {
-39
test/config/test/pool-isolation.test.ts
··· 1 - import { describe, expect, test } from 'vitest' 2 - import { runVitest } from '../../test-utils' 3 - 4 - describe.each(['forks', 'threads'] as const)('%s', async (pool) => { 5 - test('is isolated', async () => { 6 - const { stderr, exitCode } = await runVitest({ 7 - root: './fixtures/pool-isolation', 8 - include: ['isolated.test.ts'], 9 - pool, 10 - poolOptions: { 11 - // Use default value on the tested pool and disable on the other one 12 - [invertPool(pool)]: { isolate: false }, 13 - }, 14 - env: { TESTED_POOL: pool }, 15 - }) 16 - 17 - expect(stderr).toBe('') 18 - expect(exitCode).toBe(0) 19 - }) 20 - 21 - test('is not isolated', async () => { 22 - const { stderr, exitCode } = await runVitest({ 23 - root: './fixtures/pool-isolation', 24 - include: ['non-isolated.test.ts'], 25 - pool, 26 - poolOptions: { 27 - [pool]: { isolate: false }, 28 - }, 29 - env: { TESTED_POOL: pool }, 30 - }) 31 - 32 - expect(stderr).toBe('') 33 - expect(exitCode).toBe(0) 34 - }) 35 - }) 36 - 37 - function invertPool(pool: 'threads' | 'forks') { 38 - return pool === 'threads' ? 'forks' : 'threads' 39 - }
+56
test/config/test/pool.test.ts
··· 1 + import type { SerializedConfig } from 'vitest' 2 + import type { TestUserConfig } from 'vitest/node' 3 + import { assert, describe, expect, test } from 'vitest' 4 + import { runVitest } from '../../test-utils' 5 + 6 + describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => { 7 + test('resolves top-level pool', async () => { 8 + const config = await getConfig({ pool }) 9 + 10 + expect(config.pool).toBe(pool) 11 + }) 12 + }) 13 + 14 + test('extended project inherits top-level pool related options', async () => { 15 + const config = await getConfig({ 16 + projects: [{ 17 + extends: true, 18 + test: { name: 'example' }, 19 + }], 20 + }, 21 + // project.extends works weirdly with runVitest(). Need to pass it here in cli options instead. 22 + { pool: 'threads', isolate: false }) 23 + 24 + expect(config.pool).toBe('threads') 25 + expect(config.isolate).toBe(false) 26 + }) 27 + 28 + test('project level pool options overwrites top-level', async () => { 29 + const config = await getConfig({ 30 + pool: 'vmForks', 31 + fileParallelism: true, 32 + projects: [{ 33 + extends: true, 34 + test: { pool: 'vmThreads', fileParallelism: false }, 35 + }], 36 + }) 37 + 38 + expect(config.pool).toBe('vmThreads') 39 + expect(config.fileParallelism).toBe(false) 40 + }) 41 + 42 + async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}) { 43 + let config: SerializedConfig | undefined 44 + 45 + await runVitest({ 46 + root: './fixtures/pool', 47 + include: ['print-config.test.ts'], 48 + ...cliOptions, 49 + onConsoleLog(log) { 50 + config = JSON.parse(log) 51 + }, 52 + }, undefined, undefined, { test: options }, { }) 53 + 54 + assert(config) 55 + return config 56 + }
+3 -13
test/config/test/workers-option.test.ts
··· 37 37 }) 38 38 39 39 test.each([ 40 - { poolOption: 'threads' }, 40 + { pool: 'threads' }, 41 41 { poolOption: 'vmThreads' }, 42 42 { poolOption: 'forks' }, 43 43 { poolOption: 'vmForks' }, 44 - ] as const)('workers percent argument in $poolOption should not throw error', async ({ poolOption }) => { 45 - let workerOptions = {} 46 - 47 - if (poolOption.toLowerCase().includes('threads')) { 48 - workerOptions = { maxThreads: '100%' } 49 - } 50 - 51 - if (poolOption.toLowerCase().includes('forks')) { 52 - workerOptions = { maxForks: '100%' } 53 - } 54 - 55 - const { stderr } = await runVitest({ poolOptions: { [poolOption]: workerOptions } }) 44 + ] as const)('workers percent argument in $poolOption should not throw error', async ({ pool }) => { 45 + const { stderr } = await runVitest({ maxWorkers: '100%', pool }) 56 46 57 47 expect(stderr).toBe('') 58 48 })
+22
test/core/test/exports.test.ts
··· 96 96 "mockObject": "function", 97 97 }, 98 98 "./node": { 99 + "BaseRuntime": "function", 99 100 "BaseSequencer": "function", 101 + "ForksRuntime": "function", 100 102 "GitNotFoundError": "function", 101 103 "TestsNotFoundError": "function", 104 + "ThreadsRuntime": "function", 105 + "TypecheckRuntime": "function", 102 106 "VitestPackageInstaller": "function", 103 107 "VitestPlugin": "function", 108 + "VmForksRuntime": "function", 109 + "VmThreadsRuntime": "function", 104 110 "createDebugger": "function", 105 111 "createMethodsRPC": "function", 106 112 "createViteLogger": "function", ··· 162 168 "getHooks": "function", 163 169 "setFn": "function", 164 170 "setHooks": "function", 171 + }, 172 + "./workers": { 173 + "collectVitestWorkerTests": "function", 174 + "provideWorkerState": "function", 175 + "runBaseTests": "function", 176 + "runVitestWorker": "function", 177 + "runVmTests": "function", 165 178 }, 166 179 } 167 180 `) ··· 249 262 }, 250 263 "./node": { 251 264 "BaseSequencer": "function", 265 + "ForksPoolWorker": "function", 252 266 "GitNotFoundError": "function", 253 267 "TestsNotFoundError": "function", 268 + "ThreadsPoolWorker": "function", 269 + "TypecheckPoolWorker": "function", 254 270 "VitestPackageInstaller": "function", 255 271 "VitestPlugin": "function", 272 + "VmForksPoolWorker": "function", 273 + "VmThreadsPoolWorker": "function", 256 274 "createDebugger": "function", 257 275 "createMethodsRPC": "function", 258 276 "createViteLogger": "function", ··· 314 332 "getHooks": "function", 315 333 "setFn": "function", 316 334 "setHooks": "function", 335 + }, 336 + "./worker": { 337 + "init": "function", 338 + "runBaseTests": "function", 317 339 }, 318 340 } 319 341 `)
+9
test/core/test/math.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('sum', () => { 4 + expect(1 + 1).toBe(2) 5 + }) 6 + 7 + test('multiply', () => { 8 + expect(1 * 2).toBe(2) 9 + })
+6 -36
test/core/test/memory-limit.test.ts
··· 1 - import type { PoolOptions, ResolvedConfig } from 'vitest/node' 2 1 import { describe, expect, it } from 'vitest' 3 2 import { getWorkerMemoryLimit } from 'vitest/src/utils/memory-limit.js' 4 3 5 - function makeConfig(poolOptions: PoolOptions): ResolvedConfig { 6 - return { 7 - poolOptions: { 8 - vmForks: { 9 - maxForks: poolOptions.maxForks, 10 - memoryLimit: poolOptions.memoryLimit, 11 - }, 12 - vmThreads: { 13 - maxThreads: poolOptions.maxThreads, 14 - memoryLimit: poolOptions.memoryLimit, 15 - }, 16 - }, 17 - } as ResolvedConfig 18 - } 19 - 20 4 describe('getWorkerMemoryLimit', () => { 21 - it('should prioritize vmThreads.memoryLimit when pool is vmThreads', () => { 22 - const config = { 23 - poolOptions: { 24 - vmForks: { memoryLimit: undefined }, 25 - vmThreads: { memoryLimit: '256MB' }, 26 - }, 27 - } as ResolvedConfig 28 - 29 - expect(getWorkerMemoryLimit(config, 'vmThreads')).toBe('256MB') 5 + it('should prioritize vmMemoryLimit', () => { 6 + expect(getWorkerMemoryLimit({ vmMemoryLimit: '512MB', maxWorkers: 1, watch: false })).toBe('512MB') 30 7 }) 31 8 32 - it('should prioritize vmForks.memoryLimit when pool is vmForks', () => { 33 - const config = makeConfig({ memoryLimit: '512MB' }) 34 - expect(getWorkerMemoryLimit(config, 'vmForks')).toBe('512MB') 9 + it('should calculate 1/maxWorkers', () => { 10 + expect(getWorkerMemoryLimit({ maxWorkers: 4, watch: false })).toBe(1 / 4) 35 11 }) 36 12 37 - it('should calculate 1/maxThreads when vmThreads.memoryLimit is unset', () => { 38 - const config = makeConfig({ maxThreads: 4 }) 39 - expect(getWorkerMemoryLimit(config, 'vmThreads')).toBe(1 / 4) 40 - }) 41 - 42 - it('should calculate 1/maxForks when vmForks.memoryLimit is unset', () => { 43 - const config = makeConfig({ maxForks: 4 }) 44 - expect(getWorkerMemoryLimit(config, 'vmForks')).toBe(1 / 4) 13 + it('should calculate maxWorkers/2 when in watch-mode', () => { 14 + expect(getWorkerMemoryLimit({ maxWorkers: 14, watch: true })).toBe(1 / 14) 45 15 }) 46 16 })
+10 -5
test/core/test/write-file-dynamic-import.test.ts
··· 1 + import type { ViteUserConfig } from 'vitest/config' 1 2 import { unlinkSync, writeFileSync } from 'node:fs' 2 3 import { pathToFileURL } from 'node:url' 3 - import { afterEach, expect, it } from 'vitest' 4 - 5 - const filename = 'bar.js' 6 - 7 - afterEach(() => unlinkSync(filename)) 4 + import { expect, it, onTestFinished } from 'vitest' 8 5 9 6 it('write file and import created file it should return created content.', async () => { 7 + // @ts-expect-error -- internal 8 + const config: NonNullable<ViteUserConfig['test']> = globalThis.__vitest_worker__.config 9 + 10 + // This test can run parallel on multiple projects - namespace the file to avoid conflicts 11 + const filename = `${config.name}-bar.js` 12 + 13 + onTestFinished(() => unlinkSync(filename)) 14 + 10 15 writeFileSync(filename, 'export default 123') 11 16 12 17 const mod = await import(pathToFileURL(filename).href)
+2 -2
test/reporters/tests/test-run.test.ts
··· 1041 1041 onInit(ctx) { 1042 1042 vitest = ctx 1043 1043 }, 1044 - async onTestModuleCollected() { 1045 - await vitest.cancelCurrentRun('keyboard-input') 1044 + onTestModuleCollected() { 1045 + vitest.cancelCurrentRun('keyboard-input') 1046 1046 }, 1047 1047 onTestRunEnd(_, __, reason_) { 1048 1048 reason = reason_
+8 -3
test/typescript/test/runner.test.ts
··· 81 81 dir: resolve(import.meta.dirname, '..', './failing'), 82 82 config: resolve(import.meta.dirname, './vitest.empty.config.ts'), 83 83 typecheck: { enabled: true }, 84 - }, 85 - ) 84 + }) 86 85 87 - expect(stderr.replace(resolve(import.meta.dirname, '..'), '<root>')).toMatchSnapshot() 86 + const message = removeLines(stderr.replace(resolve(import.meta.dirname, '..'), '<root>')) 87 + 88 + expect(message).toMatchSnapshot() 88 89 }) 89 90 }) 90 91 ··· 151 152 expect(stderr).toContain('Error: spawn non-existing-command ENOENT') 152 153 } 153 154 }) 155 + 156 + function removeLines(log: string) { 157 + return log.replace(/⎯{2,}/g, '⎯⎯') 158 + }
+10 -10
test/watch/test/file-watching.test.ts
··· 1 1 import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' 2 2 import { webdriverio } from '@vitest/browser-webdriverio' 3 3 4 - import { afterEach, describe, expect, test } from 'vitest' 4 + import { afterEach, describe, expect, onTestFinished, test } from 'vitest' 5 5 import * as testUtils from '../../test-utils' 6 6 7 7 const sourceFile = 'fixtures/math.ts' ··· 17 17 const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8') 18 18 19 19 const options = { root: 'fixtures', watch: true } 20 - const cleanups: (() => void)[] = [] 21 20 22 21 function editFile(fileContent: string) { 23 22 return `// Modified by file-watching.test.ts ··· 31 30 writeFileSync(testFile, testFileContent, 'utf8') 32 31 writeFileSync(configFile, configFileContent, 'utf8') 33 32 writeFileSync(forceTriggerFile, forceTriggerFileContent, 'utf8') 34 - cleanups.splice(0).forEach(cleanup => cleanup()) 35 33 }) 36 34 37 35 // TODO: Fix flakiness and enable on CI ··· 113 111 expect(true).toBeTruthy() 114 112 }) 115 113 ` 116 - cleanups.push(() => rmSync(testFile)) 114 + onTestFinished(() => rmSync(testFile)) 117 115 writeFileSync(testFile, testFileContent, 'utf-8') 118 116 119 117 await vitest.waitForStdout('Running added dynamic test') ··· 121 119 await vitest.waitForStdout('1 passed') 122 120 }) 123 121 124 - test('renaming an existing test file', async () => { 125 - cleanups.push(() => rmSync('fixtures/after.test.ts')) 122 + test('renaming an existing test file', { retry: 3 }, async () => { 123 + onTestFinished(() => rmSync('fixtures/after.test.ts')) 126 124 const beforeFile = 'fixtures/before.test.ts' 127 125 const afterFile = 'fixtures/after.test.ts' 128 126 const textContent = ` ··· 191 189 describe('browser', () => { 192 190 test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', { retry: 3 }, async () => { 193 191 const { vitest } = await testUtils.runVitest({ 194 - ...options, 195 - browser: { 196 - instances: [{ browser: 'chromium' }], 192 + root: 'fixtures', 193 + watch: true, 194 + }, undefined, undefined, { 195 + test: { browser: { 196 + instances: [{ browser: 'chrome' }], 197 197 provider: webdriverio(), 198 198 enabled: true, 199 199 headless: true, 200 - }, 200 + } }, 201 201 }) 202 202 203 203 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8')
+2 -2
test/workspaces/space-pools/isolate.test.ts
··· 6 6 const config: NonNullable<ViteUserConfig['test']> = globalThis.__vitest_worker__.config 7 7 8 8 if (config.pool === 'forks') { 9 - expect(config.poolOptions?.forks?.isolate).toBe(true) 9 + expect(config.isolate).toBe(true) 10 10 } 11 11 else { 12 12 expect(config.pool).toBe('threads') 13 - expect(config.poolOptions?.threads?.isolate).toBe(true) 13 + expect(config.isolate).toBe(true) 14 14 } 15 15 })
+2 -2
test/workspaces/space-pools/multi-worker.test.ts
··· 6 6 const config: NonNullable<ViteUserConfig['test']> = globalThis.__vitest_worker__.config 7 7 8 8 if (config.pool === 'forks') { 9 - expect(config.poolOptions?.forks?.singleFork).toBe(false) 9 + expect(config.fileParallelism).toBe(true) 10 10 } 11 11 else { 12 12 expect(config.pool).toBe('threads') 13 - expect(config.poolOptions?.threads?.singleThread).toBe(false) 13 + expect(config.fileParallelism).toBe(true) 14 14 } 15 15 })
+2 -2
test/workspaces/space-pools/no-isolate.test.ts
··· 6 6 const config: NonNullable<ViteUserConfig['test']> = globalThis.__vitest_worker__.config 7 7 8 8 if (config.pool === 'forks') { 9 - expect(config.poolOptions?.forks?.isolate).toBe(false) 9 + expect(config.isolate).toBe(false) 10 10 } 11 11 else { 12 12 expect(config.pool).toBe('threads') 13 - expect(config.poolOptions?.threads?.isolate).toBe(false) 13 + expect(config.isolate).toBe(false) 14 14 } 15 15 })
+2 -2
test/workspaces/space-pools/single-worker.test.ts
··· 6 6 const config: NonNullable<ViteUserConfig['test']> = globalThis.__vitest_worker__.config 7 7 8 8 if (config.pool === 'forks') { 9 - expect(config.poolOptions?.forks?.singleFork).toBe(true) 9 + expect(config.fileParallelism).toBe(false) 10 10 } 11 11 else { 12 12 expect(config.pool).toBe('threads') 13 - expect(config.poolOptions?.threads?.singleThread).toBe(true) 13 + expect(config.fileParallelism).toBe(false) 14 14 } 15 15 })
+2 -2
packages/browser/src/client/channel.ts
··· 1 - import type { CancelReason } from '@vitest/runner' 1 + import type { CancelReason, FileSpecification } from '@vitest/runner' 2 2 import { getBrowserState } from './utils' 3 3 4 4 export interface IframeViewportEvent { ··· 27 27 export interface IframeExecuteEvent { 28 28 event: 'execute' 29 29 method: 'run' | 'collect' 30 - files: string[] 30 + files: FileSpecification[] 31 31 iframeId: string 32 32 context: string 33 33 }
+5 -2
packages/browser/src/client/orchestrator.ts
··· 1 1 import type { GlobalChannelIncomingEvent, IframeChannelIncomingEvent, IframeChannelOutgoingEvent, IframeViewportDoneEvent, IframeViewportFailEvent } from '@vitest/browser/client' 2 + import type { FileSpecification } from '@vitest/runner' 2 3 import type { BrowserTesterOptions, SerializedConfig } from 'vitest' 3 4 import { channel, client, globalChannel } from '@vitest/browser/client' 4 5 import { generateFileHash } from '@vitest/runner/utils' ··· 134 135 135 136 private async runIsolatedTestInIframe( 136 137 container: HTMLDivElement, 137 - file: string, 138 + spec: FileSpecification, 138 139 options: BrowserTesterOptions, 139 140 startTime: number, 140 141 ) { 141 142 const config = getConfig() 142 143 const { width, height } = config.browser.viewport 144 + 145 + const file = spec.filepath 143 146 144 147 if (this.iframes.has(file)) { 145 148 this.iframes.get(file)!.remove() ··· 151 154 // running tests after the "prepare" event 152 155 await sendEventToIframe({ 153 156 event: 'execute', 154 - files: [file], 157 + files: [spec], 155 158 method: options.method, 156 159 iframeId: file, 157 160 context: options.providedContext,
-1
packages/vitest/src/node/core.ts
··· 1185 1185 setTimeout(() => { 1186 1186 this.report('onProcessTimeout').then(() => { 1187 1187 console.warn(`close timed out after ${this.config.teardownTimeout}ms`) 1188 - this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause)) 1189 1188 1190 1189 if (!this.pool) { 1191 1190 const runningServers = [this._vite, ...this.projects.map(p => p._vite)].filter(Boolean).length
+2 -1
packages/vitest/src/node/logger.ts
··· 318 318 process.exitCode = exitCode !== undefined ? (128 + exitCode) : Number(signal) 319 319 } 320 320 321 - process.exit() 321 + // Timeout to flush stderr 322 + setTimeout(() => process.exit(), 1) 322 323 } 323 324 324 325 process.once('SIGINT', onExit)
+278 -183
packages/vitest/src/node/pool.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 2 import type { Vitest } from './core' 3 + import type { PoolTask } from './pools/types' 3 4 import type { TestProject } from './project' 4 5 import type { TestSpecification } from './spec' 5 - import type { BuiltinPool, Pool } from './types/pool-options' 6 + import type { BuiltinPool, ResolvedConfig } from './types/config' 7 + import * as nodeos from 'node:os' 6 8 import { isatty } from 'node:tty' 7 9 import { resolve } from 'pathe' 8 10 import { version as viteVersion } from 'vite' 9 11 import { rootDir } from '../paths' 10 12 import { isWindows } from '../utils/env' 13 + import { getWorkerMemoryLimit, stringToBytes } from '../utils/memory-limit' 14 + import { groupFilesByEnv } from '../utils/test-helpers' 11 15 import { createBrowserPool } from './pools/browser' 12 - import { createForksPool } from './pools/forks' 13 - import { createThreadsPool } from './pools/threads' 14 - import { createTypecheckPool } from './pools/typecheck' 15 - import { createVmForksPool } from './pools/vmForks' 16 - import { createVmThreadsPool } from './pools/vmThreads' 16 + import { Pool } from './pools/pool' 17 17 18 18 const suppressWarningsPath = resolve(rootDir, './suppress-warnings.cjs') 19 19 20 - export type RunWithFiles = ( 20 + type RunWithFiles = ( 21 21 files: TestSpecification[], 22 22 invalidates?: string[] 23 - ) => Awaitable<void> 24 - 25 - type LocalPool = Exclude<Pool, 'browser'> 23 + ) => Promise<void> 26 24 27 25 export interface ProcessPool { 28 26 name: string ··· 45 43 'typescript', 46 44 ] 47 45 48 - function getDefaultPoolName(project: TestProject): Pool { 46 + export function getFilePoolName(project: TestProject): ResolvedConfig['pool'] { 49 47 if (project.config.browser.enabled) { 50 48 return 'browser' 51 49 } 52 50 return project.config.pool 53 51 } 54 52 55 - export function getFilePoolName(project: TestProject): Pool { 56 - return getDefaultPoolName(project) 57 - } 58 - 59 53 export function createPool(ctx: Vitest): ProcessPool { 60 - const pools: Record<Pool, ProcessPool | null> = { 61 - forks: null, 62 - threads: null, 63 - browser: null, 64 - vmThreads: null, 65 - vmForks: null, 66 - typescript: null, 54 + const pool = new Pool({ 55 + distPath: ctx.distPath, 56 + teardownTimeout: ctx.config.teardownTimeout, 57 + state: ctx.state, 58 + }, ctx.logger) 59 + 60 + const options = resolveOptions(ctx) 61 + 62 + const Sequencer = ctx.config.sequence.sequencer 63 + const sequencer = new Sequencer(ctx) 64 + 65 + let browserPool: ProcessPool | undefined 66 + 67 + async function executeTests(method: 'run' | 'collect', specs: TestSpecification[], invalidates?: string[]): Promise<void> { 68 + ctx.onCancel(() => pool.cancel()) 69 + 70 + if (ctx.config.shard) { 71 + if (!ctx.config.passWithNoTests && ctx.config.shard.count > specs.length) { 72 + throw new Error( 73 + '--shard <count> must be a smaller than count of test files. ' 74 + + `Resolved ${specs.length} test files for --shard=${ctx.config.shard.index}/${ctx.config.shard.count}.`, 75 + ) 76 + } 77 + specs = await sequencer.shard(Array.from(specs)) 78 + } 79 + 80 + const taskGroups: { 81 + tasks: PoolTask[] 82 + maxWorkers: number 83 + // browser pool has a more complex logic, so we keep it separately for now 84 + browserSpecs: TestSpecification[] 85 + }[] = [] 86 + let workerId = 0 87 + 88 + const sorted = await sequencer.sort(specs) 89 + const groups = groupSpecs(sorted) 90 + 91 + for (const group of groups) { 92 + if (!group) { 93 + continue 94 + } 95 + 96 + const taskGroup: PoolTask[] = [] 97 + const browserSpecs: TestSpecification[] = [] 98 + taskGroups.push({ 99 + tasks: taskGroup, 100 + maxWorkers: group.maxWorkers, 101 + browserSpecs, 102 + }) 103 + 104 + for (const specs of group.specs) { 105 + const { project, pool } = specs[0] 106 + if (pool === 'browser') { 107 + browserSpecs.push(...specs) 108 + continue 109 + } 110 + 111 + const byEnv = await groupFilesByEnv(specs) 112 + const env = Object.values(byEnv)[0][0] 113 + 114 + taskGroup.push({ 115 + context: { 116 + pool, 117 + config: project.serializedConfig, 118 + files: specs.map(spec => ({ filepath: spec.moduleId, testLocations: spec.testLines })), 119 + invalidates, 120 + environment: env.environment, 121 + projectName: project.name, 122 + providedContext: project.getProvidedContext(), 123 + workerId: workerId++, 124 + }, 125 + project, 126 + env: options.env, 127 + execArgv: [...options.execArgv, ...project.config.execArgv], 128 + worker: pool, 129 + isolate: project.config.isolate, 130 + memoryLimit: getMemoryLimit(ctx.config, pool) ?? null, 131 + }) 132 + } 133 + } 134 + 135 + const results: PromiseSettledResult<void>[] = [] 136 + 137 + for (const { tasks, browserSpecs, maxWorkers } of taskGroups) { 138 + pool.setMaxWorkers(maxWorkers) 139 + 140 + const promises = tasks.map(async (task) => { 141 + if (ctx.isCancelling) { 142 + return ctx.state.cancelFiles(task.context.files, task.project) 143 + } 144 + 145 + try { 146 + await pool.run(task, method) 147 + } 148 + catch (error) { 149 + // Intentionally cancelled 150 + if (ctx.isCancelling && error instanceof Error && error.message === 'Cancelled') { 151 + ctx.state.cancelFiles(task.context.files, task.project) 152 + } 153 + else { 154 + throw error 155 + } 156 + } 157 + }) 158 + 159 + if (browserSpecs.length) { 160 + browserPool ??= createBrowserPool(ctx) 161 + if (method === 'collect') { 162 + promises.push(browserPool.collectTests(browserSpecs)) 163 + } 164 + else { 165 + promises.push(browserPool.runTests(browserSpecs)) 166 + } 167 + } 168 + 169 + const groupResults = await Promise.allSettled(promises) 170 + 171 + results.push(...groupResults) 172 + } 173 + 174 + const errors = results 175 + .filter(result => result.status === 'rejected') 176 + .map(result => result.reason) 177 + 178 + if (errors.length > 0) { 179 + throw new AggregateError( 180 + errors, 181 + 'Errors occurred while running tests. For more information, see serialized error.', 182 + ) 183 + } 67 184 } 68 185 186 + return { 187 + name: 'default', 188 + runTests: (files, invalidates) => executeTests('run', files, invalidates), 189 + collectTests: (files, invalidates) => executeTests('collect', files, invalidates), 190 + async close() { 191 + await Promise.all([pool.close(), browserPool?.close?.()]) 192 + }, 193 + } 194 + } 195 + 196 + function resolveOptions(ctx: Vitest) { 69 197 // in addition to resolve.conditions Vite also adds production/development, 70 198 // see: https://github.com/vitejs/vite/blob/af2aa09575229462635b7cbb6d248ca853057ba2/packages/vite/src/node/plugins/resolve.ts#L1056-L1080 71 199 const viteMajor = Number(viteVersion.split('.')[0]) 200 + 72 201 const potentialConditions = new Set(viteMajor >= 6 73 202 ? (ctx.vite.config.ssr.resolve?.conditions ?? []) 74 203 : [ ··· 76 205 'development', 77 206 ...ctx.vite.config.resolve.conditions, 78 207 ]) 208 + 79 209 const conditions = [...potentialConditions] 80 210 .filter((condition) => { 81 211 if (condition === 'production') { ··· 103 233 || execArg.startsWith('--diagnostic-dir'), 104 234 ) 105 235 106 - async function executeTests(method: 'runTests' | 'collectTests', files: TestSpecification[], invalidate?: string[]) { 107 - const options: PoolProcessOptions = { 108 - execArgv: [ 109 - ...execArgv, 110 - ...conditions, 111 - '--experimental-import-meta-resolve', 112 - '--require', 113 - suppressWarningsPath, 114 - ], 115 - env: { 116 - TEST: 'true', 117 - VITEST: 'true', 118 - NODE_ENV: process.env.NODE_ENV || 'test', 119 - VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN', 120 - FORCE_TTY: isatty(1) ? 'true' : '', 121 - ...process.env, 122 - ...ctx.config.env, 123 - }, 124 - } 125 - 126 - // env are case-insensitive on Windows, but spawned processes don't support it 127 - if (isWindows) { 128 - for (const name in options.env) { 129 - options.env[name.toUpperCase()] = options.env[name] 130 - } 131 - } 132 - 133 - const poolConcurrentPromises = new Map<string, Promise<ProcessPool>>() 134 - const customPools = new Map<string, ProcessPool>() 135 - async function resolveCustomPool(filepath: string) { 136 - if (customPools.has(filepath)) { 137 - return customPools.get(filepath)! 138 - } 139 - 140 - const pool = await ctx.runner.import(filepath) 141 - if (typeof pool.default !== 'function') { 142 - throw new TypeError( 143 - `Custom pool "${filepath}" must export a function as default export`, 144 - ) 145 - } 146 - 147 - const poolInstance = await pool.default(ctx, options) 148 - 149 - if (typeof poolInstance?.name !== 'string') { 150 - throw new TypeError( 151 - `Custom pool "${filepath}" should return an object with "name" property`, 152 - ) 153 - } 154 - if (typeof poolInstance?.[method] !== 'function') { 155 - throw new TypeError( 156 - `Custom pool "${filepath}" should return an object with "${method}" method`, 157 - ) 158 - } 159 - 160 - customPools.set(filepath, poolInstance) 161 - return poolInstance as ProcessPool 162 - } 163 - 164 - function getConcurrentPool(pool: string, fn: () => Promise<ProcessPool>) { 165 - if (poolConcurrentPromises.has(pool)) { 166 - return poolConcurrentPromises.get(pool)! 167 - } 168 - const promise = fn().finally(() => { 169 - poolConcurrentPromises.delete(pool) 170 - }) 171 - poolConcurrentPromises.set(pool, promise) 172 - return promise 173 - } 174 - 175 - function getCustomPool(pool: string) { 176 - return getConcurrentPool(pool, () => resolveCustomPool(pool)) 177 - } 178 - 179 - const groupedSpecifications: Record<string, TestSpecification[]> = {} 180 - const groups = new Set<number>() 181 - 182 - const factories: Record<LocalPool, (specs: TestSpecification[]) => ProcessPool> = { 183 - vmThreads: specs => createVmThreadsPool(ctx, options, specs), 184 - vmForks: specs => createVmForksPool(ctx, options, specs), 185 - threads: specs => createThreadsPool(ctx, options, specs), 186 - forks: specs => createForksPool(ctx, options, specs), 187 - typescript: () => createTypecheckPool(ctx), 188 - browser: () => createBrowserPool(ctx), 189 - } 190 - 191 - for (const spec of files) { 192 - const group = spec.project.config.sequence.groupOrder ?? 0 193 - groups.add(group) 194 - groupedSpecifications[group] ??= [] 195 - groupedSpecifications[group].push(spec) 196 - } 197 - 198 - const Sequencer = ctx.config.sequence.sequencer 199 - const sequencer = new Sequencer(ctx) 200 - 201 - async function sortSpecs(specs: TestSpecification[]) { 202 - if (ctx.config.shard) { 203 - if (!ctx.config.passWithNoTests && ctx.config.shard.count > specs.length) { 204 - throw new Error( 205 - '--shard <count> must be a smaller than count of test files. ' 206 - + `Resolved ${specs.length} test files for --shard=${ctx.config.shard.index}/${ctx.config.shard.count}.`, 207 - ) 208 - } 209 - 210 - specs = await sequencer.shard(specs) 211 - } 212 - return sequencer.sort(specs) 213 - } 214 - 215 - const sortedGroups = Array.from(groups).sort() 216 - for (const group of sortedGroups) { 217 - const specifications = groupedSpecifications[group] 218 - 219 - if (!specifications?.length) { 220 - continue 221 - } 222 - 223 - const filesByPool: Record<LocalPool, TestSpecification[]> = { 224 - forks: [], 225 - threads: [], 226 - vmThreads: [], 227 - vmForks: [], 228 - typescript: [], 229 - } 230 - 231 - specifications.forEach((specification) => { 232 - const pool = specification.pool 233 - filesByPool[pool] ??= [] 234 - filesByPool[pool].push(specification) 235 - }) 236 - 237 - await Promise.all( 238 - Object.entries(filesByPool).map(async (entry) => { 239 - const [pool, files] = entry as [Pool, TestSpecification[]] 240 - 241 - if (!files.length) { 242 - return null 243 - } 244 - 245 - const specs = await sortSpecs(files) 246 - 247 - if (pool in factories) { 248 - const factory = factories[pool] 249 - pools[pool] ??= factory(specs) 250 - return pools[pool]![method](specs, invalidate) 251 - } 252 - 253 - const poolHandler = await getCustomPool(pool) 254 - pools[poolHandler.name] ??= poolHandler 255 - return poolHandler[method](specs, invalidate) 256 - }), 257 - ) 258 - } 259 - } 260 - 261 - return { 262 - name: 'default', 263 - runTests: (files, invalidates) => executeTests('runTests', files, invalidates), 264 - collectTests: (files, invalidates) => executeTests('collectTests', files, invalidates), 265 - async close() { 266 - await Promise.all(Object.values(pools).map(p => p?.close?.())) 236 + const options: PoolProcessOptions = { 237 + execArgv: [ 238 + ...execArgv, 239 + ...conditions, 240 + '--experimental-import-meta-resolve', 241 + '--require', 242 + suppressWarningsPath, 243 + ], 244 + env: { 245 + TEST: 'true', 246 + VITEST: 'true', 247 + NODE_ENV: process.env.NODE_ENV || 'test', 248 + VITEST_MODE: ctx.config.watch ? 'WATCH' : 'RUN', 249 + FORCE_TTY: isatty(1) ? 'true' : '', 250 + ...process.env, 251 + ...ctx.config.env, 267 252 }, 268 253 } 254 + 255 + // env are case-insensitive on Windows, but spawned processes don't support it 256 + if (isWindows) { 257 + for (const name in options.env) { 258 + options.env[name.toUpperCase()] = options.env[name] 259 + } 260 + } 261 + 262 + return options 263 + } 264 + 265 + function resolveMaxWorkers(project: TestProject) { 266 + if (project.config.maxWorkers) { 267 + return project.config.maxWorkers 268 + } 269 + 270 + if (project.vitest.config.maxWorkers) { 271 + return project.vitest.config.maxWorkers 272 + } 273 + 274 + const numCpus = typeof nodeos.availableParallelism === 'function' 275 + ? nodeos.availableParallelism() 276 + : nodeos.cpus().length 277 + 278 + if (project.vitest.config.watch) { 279 + return Math.max(Math.floor(numCpus / 2), 1) 280 + } 281 + 282 + return Math.max(numCpus - 1, 1) 283 + } 284 + 285 + function getMemoryLimit(config: ResolvedConfig, pool: string) { 286 + if (pool !== 'vmForks' && pool !== 'vmThreads') { 287 + return null 288 + } 289 + 290 + const memory = nodeos.totalmem() 291 + const limit = getWorkerMemoryLimit(config) 292 + 293 + if (typeof memory === 'number') { 294 + return stringToBytes(limit, config.watch ? memory / 2 : memory) 295 + } 296 + 297 + // If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%" 298 + if ( 299 + (typeof limit === 'number' && limit > 1) 300 + || (typeof limit === 'string' && limit.at(-1) !== '%') 301 + ) { 302 + return stringToBytes(limit) 303 + } 304 + 305 + // just ignore "memoryLimit" value because we cannot detect memory limit 306 + return null 307 + } 308 + 309 + function groupSpecs(specs: TestSpecification[]) { 310 + // Test files are passed to test runner one at a time, except Typechecker. 311 + // TODO: Should non-isolated test files be passed to test runner all at once? 312 + type SpecsForRunner = TestSpecification[] 313 + 314 + // Tests in a single group are executed with `maxWorkers` parallelism. 315 + // Next group starts running after previous finishes - allows real sequential tests. 316 + interface Groups { specs: SpecsForRunner[]; maxWorkers: number } 317 + const groups: Groups[] = [] 318 + 319 + // Files without file parallelism but without explicit sequence.groupOrder 320 + const sequential: Groups = { specs: [], maxWorkers: 1 } 321 + 322 + // Type tests are run in a single group, per project 323 + const typechecks: Record<string, TestSpecification[]> = {} 324 + 325 + specs.forEach((spec) => { 326 + if (spec.pool === 'typescript') { 327 + typechecks[spec.project.name] ||= [] 328 + typechecks[spec.project.name].push(spec) 329 + return 330 + } 331 + 332 + const order = spec.project.config.sequence.groupOrder 333 + 334 + // Files that have disabled parallelism and default groupId are set into their own group 335 + if (order === 0 && spec.project.config.fileParallelism === false) { 336 + return sequential.specs.push([spec]) 337 + } 338 + 339 + const maxWorkers = resolveMaxWorkers(spec.project) 340 + groups[order] ||= { specs: [], maxWorkers } 341 + 342 + // Multiple projects with different maxWorkers but same groupId 343 + if (groups[order].maxWorkers !== maxWorkers) { 344 + const last = groups[order].specs.at(-1)?.at(-1)?.project.name 345 + 346 + throw new Error(`Projects "${last}" and "${spec.project.name}" have different 'maxWorkers' but same 'sequence.groupId'.\nProvide unique 'sequence.groupId' for them.`) 347 + } 348 + 349 + groups[order].specs.push([spec]) 350 + }) 351 + 352 + for (const project in typechecks) { 353 + const order = Math.max(0, ...groups.keys()) + 1 354 + 355 + groups[order] ||= { specs: [], maxWorkers: resolveMaxWorkers(typechecks[project][0].project) } 356 + groups[order].specs.push(typechecks[project]) 357 + } 358 + 359 + if (sequential.specs.length) { 360 + groups.push(sequential) 361 + } 362 + 363 + return groups 269 364 }
+1 -1
packages/vitest/src/node/spec.ts
··· 1 1 import type { SerializedTestSpecification } from '../runtime/types/utils' 2 2 import type { TestProject } from './project' 3 3 import type { TestModule } from './reporters/reported-tasks' 4 - import type { Pool } from './types/pool-options' 4 + import type { Pool } from './types/config' 5 5 import { generateFileHash } from '@vitest/runner/utils' 6 6 import { relative } from 'pathe' 7 7
+13 -14
packages/vitest/src/node/state.ts
··· 1 - import type { File, Task, TaskResultPack } from '@vitest/runner' 1 + import type { File, FileSpecification, Task, TaskResultPack } from '@vitest/runner' 2 2 import type { UserConsoleLog } from '../types/general' 3 3 import type { TestProject } from './project' 4 4 import type { MergedBlobs } from './reporters/blob' 5 5 import type { OnUnhandledErrorCallback } from './types/config' 6 - import { createFileTask } from '@vitest/runner/utils' 6 + import { createFileTask, generateFileHash } from '@vitest/runner/utils' 7 + import { relative } from 'pathe' 7 8 import { defaultBrowserPort } from '../constants' 8 9 import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks' 9 10 ··· 21 22 idMap: Map<string, Task> = new Map() 22 23 taskFileMap: WeakMap<Task, File> = new WeakMap() 23 24 errorsSet: Set<unknown> = new Set() 24 - processTimeoutCauses: Set<string> = new Set() 25 25 reportedTasksMap: WeakMap<Task, TestModule | TestCase | TestSuite> = new WeakMap() 26 26 blobs?: MergedBlobs 27 27 transformTime = 0 ··· 88 88 89 89 getUnhandledErrors(): unknown[] { 90 90 return Array.from(this.errorsSet.values()) 91 - } 92 - 93 - addProcessTimeoutCause(cause: string): void { 94 - this.processTimeoutCauses.add(cause) 95 - } 96 - 97 - getProcessTimeoutCauses(): string[] { 98 - return Array.from(this.processTimeoutCauses.values()) 99 91 } 100 92 101 93 getPaths(): string[] { ··· 257 249 ).length 258 250 } 259 251 260 - cancelFiles(files: string[], project: TestProject): void { 252 + cancelFiles(files: FileSpecification[], project: TestProject): void { 253 + // if we don't filter existing modules, they will be overriden by `collectFiles` 254 + const nonRegisteredFiles = files.filter(({ filepath }) => { 255 + const relativePath = relative(project.config.root, filepath) 256 + const id = generateFileHash(relativePath, project.name) 257 + return !this.idMap.has(id) 258 + }) 259 + 261 260 this.collectFiles( 262 261 project, 263 - files.map(filepath => 264 - createFileTask(filepath, project.config.root, project.config.name), 262 + nonRegisteredFiles.map(file => 263 + createFileTask(file.filepath, project.config.root, project.config.name), 265 264 ), 266 265 ) 267 266 }
+1
packages/vitest/src/node/test-run.ts
··· 135 135 return modules.some(m => !m.ok()) 136 136 } 137 137 138 + // make sure the error always has a "stacks" property 138 139 private syncUpdateStacks(update: TaskResultPack[]): void { 139 140 update.forEach(([taskId, result]) => { 140 141 const task = this.vitest.state.idMap.get(taskId)
+13 -1
packages/vitest/src/public/node.ts
··· 24 24 export type { ProcessPool } from '../node/pool' 25 25 export { getFilePoolName } from '../node/pool' 26 26 export { createMethodsRPC } from '../node/pools/rpc' 27 + export type { 28 + PoolOptions, 29 + PoolRunnerInitializer, 30 + PoolTask, 31 + PoolWorker, 32 + WorkerRequest, 33 + WorkerResponse, 34 + } from '../node/pools/types' 35 + export { ForksPoolWorker } from '../node/pools/workers/forksWorker' 36 + export { ThreadsPoolWorker } from '../node/pools/workers/threadsWorker' 37 + export { TypecheckPoolWorker } from '../node/pools/workers/typecheckWorker' 38 + export { VmForksPoolWorker } from '../node/pools/workers/vmForksWorker' 39 + export { VmThreadsPoolWorker } from '../node/pools/workers/vmThreadsWorker' 27 40 export type { SerializedTestProject, TestProject } from '../node/project' 28 41 export type { HTMLOptions } from '../node/reporters/html' 29 42 export type { JsonOptions } from '../node/reporters/json' ··· 90 103 EnvironmentOptions, 91 104 InlineConfig, 92 105 Pool, 93 - PoolOptions, 94 106 ProjectConfig, 95 107 ResolvedConfig, 96 108 ResolvedProjectConfig,
+2
packages/vitest/src/public/worker.ts
··· 1 + export { runBaseTests } from '../runtime/workers/base' 2 + export { init } from '../runtime/workers/init'
+2 -16
packages/vitest/src/runtime/config.ts
··· 15 15 disableConsoleIntercept: boolean | undefined 16 16 runner: string | undefined 17 17 isolate: boolean 18 + fileParallelism: boolean 19 + maxWorkers: number 18 20 mode: 'test' | 'benchmark' 19 21 bail: number | undefined 20 22 environmentOptions?: Record<string, any> ··· 48 50 seed: number 49 51 hooks: SequenceHooks 50 52 setupFiles: SequenceSetupFiles 51 - } 52 - poolOptions: { 53 - forks: { 54 - singleFork: boolean 55 - isolate: boolean 56 - } 57 - threads: { 58 - singleThread: boolean 59 - isolate: boolean 60 - } 61 - vmThreads: { 62 - singleThread: boolean 63 - } 64 - vmForks: { 65 - singleFork: boolean 66 - } 67 53 } 68 54 deps: { 69 55 web: {
+1 -10
packages/vitest/src/runtime/inspector.ts
··· 68 68 69 69 function shouldKeepOpen(config: SerializedConfig) { 70 70 // In watch mode the inspector can persist re-runs if isolation is disabled and a single worker is used 71 - const isIsolatedSingleThread 72 - = config.pool === 'threads' 73 - && config.poolOptions?.threads?.isolate === false 74 - && config.poolOptions?.threads?.singleThread 75 - const isIsolatedSingleFork 76 - = config.pool === 'forks' 77 - && config.poolOptions?.forks?.isolate === false 78 - && config.poolOptions?.forks?.singleFork 79 - 80 - return config.watch && (isIsolatedSingleFork || isIsolatedSingleThread) 71 + return config.watch && config.isolate === false && config.maxWorkers === 1 81 72 }
+3 -7
packages/vitest/src/runtime/runBaseTests.ts
··· 25 25 ): Promise<void> { 26 26 const workerState = getWorkerState() 27 27 28 - const isIsolatedThreads = config.pool === 'threads' && (config.poolOptions?.threads?.isolate ?? true) 29 - const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true) 30 - const isolate = isIsolatedThreads || isIsolatedForks 31 - 32 28 await setupGlobalEnv(config, environment, moduleRunner) 33 - await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate }) 29 + await startCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }) 34 30 35 31 if (config.chaiConfig) { 36 32 setupChaiConfig(config.chaiConfig) ··· 54 50 = performance.now() - workerState.durations.environment 55 51 56 52 for (const file of files) { 57 - if (isolate) { 53 + if (config.isolate) { 58 54 moduleRunner.mocker.reset() 59 55 resetModules(workerState.evaluatedModules, true) 60 56 } ··· 74 70 vi.restoreAllMocks() 75 71 } 76 72 77 - await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate }) 73 + await stopCoverageInsideWorker(config.coverage, moduleRunner, { isolate: config.isolate }) 78 74 }, 79 75 ) 80 76
-16
packages/vitest/src/runtime/worker-base.ts
··· 1 - import type { ContextRPC } from '../types/worker' 2 - import * as worker from './worker' 3 - import forks from './workers/forks' 4 - import threads from './workers/threads' 5 - 6 - export async function run(ctx: ContextRPC): Promise<void> { 7 - await worker.execute('run', ctx, ctx.pool === 'forks' ? forks : threads) 8 - } 9 - 10 - export async function collect(ctx: ContextRPC): Promise<void> { 11 - await worker.execute('collect', ctx, ctx.pool === 'forks' ? forks : threads) 12 - } 13 - 14 - export async function teardown(): Promise<void> { 15 - await worker.teardown() 16 - }
-16
packages/vitest/src/runtime/worker-vm.ts
··· 1 - import type { ContextRPC } from '../types/worker' 2 - import * as worker from './worker' 3 - import vmForks from './workers/vmForks' 4 - import vmThreads from './workers/vmThreads' 5 - 6 - export async function run(ctx: ContextRPC): Promise<void> { 7 - await worker.execute('run', ctx, ctx.pool === 'vmForks' ? vmForks : vmThreads) 8 - } 9 - 10 - export async function collect(ctx: ContextRPC): Promise<void> { 11 - await worker.execute('collect', ctx, ctx.pool === 'vmForks' ? vmForks : vmThreads) 12 - } 13 - 14 - export async function teardown(): Promise<void> { 15 - await worker.teardown() 16 - }
+15 -39
packages/vitest/src/runtime/worker.ts
··· 6 6 import { setupInspect } from './inspector' 7 7 import { VitestEvaluatedModules } from './moduleRunner/evaluatedModules' 8 8 import { createRuntimeRpc, rpcDone } from './rpc' 9 - import { isChildProcess } from './utils' 10 - import { disposeInternalListeners } from './workers/utils' 11 - 12 - if (isChildProcess()) { 13 - const isProfiling = process.execArgv.some( 14 - execArg => 15 - execArg.startsWith('--prof') 16 - || execArg.startsWith('--cpu-prof') 17 - || execArg.startsWith('--heap-prof') 18 - || execArg.startsWith('--diagnostic-dir'), 19 - ) 20 - 21 - if (isProfiling) { 22 - // Work-around for nodejs/node#55094 23 - process.on('SIGTERM', () => { 24 - process.exit() 25 - }) 26 - } 27 - } 28 9 29 10 const resolvingModules = new Set<string>() 30 11 const globalListeners = new Set<() => unknown>() 31 12 32 - // this is what every pool executes when running tests 33 - export async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: VitestWorker): Promise<void> { 34 - disposeInternalListeners() 35 - 13 + async function execute(method: 'run' | 'collect', ctx: ContextRPC, worker: VitestWorker) { 36 14 const prepareStart = performance.now() 37 15 38 16 const cleanups: (() => void | Promise<void>)[] = [setupInspect(ctx)] 39 17 40 - process.env.VITEST_WORKER_ID = String(ctx.workerId) 41 - const poolId = process.__tinypool_state__?.workerId 42 - process.env.VITEST_POOL_ID = String(poolId) 43 - 44 18 let environmentLoader: ModuleRunner | undefined 45 19 20 + // RPC is used to communicate between worker (be it a thread worker or child process or a custom implementation) and the main thread 21 + const { rpc, onCancel } = createRuntimeRpc(worker) 22 + 46 23 try { 47 - if (!worker.getRpcOptions || typeof worker.getRpcOptions !== 'function') { 48 - throw new TypeError( 49 - `Test worker should expose "getRpcOptions" method. Received "${typeof worker.getRpcOptions}".`, 50 - ) 51 - } 52 - 53 - // RPC is used to communicate between worker (be it a thread worker or child process or a custom implementation) and the main thread 54 - const { rpc, onCancel } = createRuntimeRpc(worker.getRpcOptions(ctx)) 55 - 56 24 // do not close the RPC channel so that we can get the error messages sent to the main thread 57 25 cleanups.push(async () => { 58 26 await Promise.all(rpc.$rejectPendingCalls(({ method, reject }) => { ··· 100 68 await Promise.all(cleanups.map(fn => fn())) 101 69 102 70 await rpcDone().catch(() => {}) 103 - environmentLoader?.close() 71 + await environmentLoader?.close() 72 + rpc.$close() 104 73 } 105 74 } 106 75 76 + export function run(ctx: ContextRPC, worker: VitestWorker): Promise<void> { 77 + return execute('run', ctx, worker) 78 + } 79 + 80 + export function collect(ctx: ContextRPC, worker: VitestWorker): Promise<void> { 81 + return execute('collect', ctx, worker) 82 + } 83 + 107 84 export async function teardown(): Promise<void> { 108 - const promises = [...globalListeners].map(l => l()) 109 - await Promise.all(promises) 85 + await Promise.all([...globalListeners].map(l => l())) 110 86 } 111 87 112 88 function createImportMetaEnvProxy(): WorkerGlobalState['metaEnv'] {
+2 -1
packages/vitest/src/types/browser.ts
··· 1 + import type { FileSpecification } from '@vitest/runner' 1 2 import type { TestExecutionMethod } from './worker' 2 3 3 4 export interface BrowserTesterOptions { 4 5 method: TestExecutionMethod 5 - files: string[] 6 + files: FileSpecification[] 6 7 providedContext: string 7 8 }
+4 -2
packages/vitest/src/types/worker.ts
··· 16 16 17 17 export interface ContextRPC { 18 18 pool: string 19 - workerId: number 20 19 config: SerializedConfig 21 20 projectName: string 22 - files: string[] | FileSpecification[] 21 + files: FileSpecification[] 23 22 environment: ContextTestEnvironment 24 23 providedContext: Record<string, any> 25 24 invalidates?: string[] 25 + 26 + /** Exposed to test runner as `VITEST_WORKER_ID`. Value is unique per each isolated worker. */ 27 + workerId: number 26 28 } 27 29 28 30 export interface WorkerGlobalState {
-28
packages/vitest/src/utils/config-helpers.ts
··· 2 2 BenchmarkBuiltinReporters, 3 3 BuiltinReporters, 4 4 } from '../node/reporters' 5 - import type { SerializedConfig } from '../node/types/config' 6 - 7 - const REGEXP_WRAP_PREFIX = '$$vitest:' 8 5 9 6 interface PotentialConfig { 10 7 outputFile?: string | Partial<Record<string, string>> ··· 23 20 } 24 21 25 22 return config.outputFile[reporter] 26 - } 27 - 28 - /** 29 - * Prepares `SerializedConfig` for serialization, e.g. `node:v8.serialize` 30 - */ 31 - export function wrapSerializableConfig(config: SerializedConfig) { 32 - let testNamePattern = config.testNamePattern 33 - let defines = config.defines 34 - 35 - // v8 serialize does not support regex 36 - if (testNamePattern && typeof testNamePattern !== 'string') { 37 - testNamePattern 38 - = `${REGEXP_WRAP_PREFIX}${testNamePattern.toString()}` as unknown as RegExp 39 - } 40 - 41 - // v8 serialize drops properties with undefined value 42 - if (defines) { 43 - defines = { keys: Object.keys(defines), original: defines } 44 - } 45 - 46 - return { 47 - ...config, 48 - testNamePattern, 49 - defines, 50 - } as SerializedConfig 51 23 } 52 24 53 25 export function createDefinesScript(define: Record<string, any> | undefined): string {
+7 -18
packages/vitest/src/utils/memory-limit.ts
··· 8 8 import type { ResolvedConfig } from '../node/types/config' 9 9 import * as nodeos from 'node:os' 10 10 11 - function getDefaultThreadsCount(config: ResolvedConfig) { 11 + function getDefaultThreadsCount(config: Pick<ResolvedConfig, 'watch'>) { 12 12 const numCpus 13 13 = typeof nodeos.availableParallelism === 'function' 14 14 ? nodeos.availableParallelism() ··· 19 19 : Math.max(numCpus - 1, 1) 20 20 } 21 21 22 - export function getWorkerMemoryLimit(config: ResolvedConfig, pool: 'vmThreads' | 'vmForks'): string | number { 23 - if (pool === 'vmForks') { 24 - const opts = config.poolOptions?.vmForks ?? {} 25 - if (opts.memoryLimit) { 26 - return opts.memoryLimit 27 - } 28 - const workers = opts.maxForks ?? getDefaultThreadsCount(config) 29 - 30 - return 1 / workers 22 + export function getWorkerMemoryLimit(config: Pick<ResolvedConfig, 'vmMemoryLimit' | 'maxWorkers' | 'watch'>): string | number { 23 + if (config.vmMemoryLimit) { 24 + return config.vmMemoryLimit 31 25 } 32 - else { 33 - const opts = config.poolOptions?.vmThreads ?? {} 34 - if (opts.memoryLimit) { 35 - return opts.memoryLimit 36 - } 37 - const workers = opts.maxThreads ?? getDefaultThreadsCount(config) 38 26 39 - return 1 / workers 40 - } 27 + const workers = config.maxWorkers ?? getDefaultThreadsCount(config) 28 + 29 + return 1 / workers 41 30 } 42 31 43 32 /**
+5 -7
test/cli/fixtures/custom-pool/vitest.config.ts
··· 1 1 import { defineConfig } from 'vitest/config' 2 + import { createCustomPool } from './pool/custom-pool' 2 3 3 4 export default defineConfig({ 4 5 test: { 5 - poolOptions: { 6 - custom: { 7 - print: 'options are respected', 8 - array: [1, 2, 3], 9 - }, 10 - }, 11 6 projects: [ 12 7 { 13 8 extends: true, 14 9 test: { 15 10 name: 'custom-pool-test', 16 - pool: './pool/custom-pool.ts', 11 + pool: createCustomPool({ 12 + print: 'options are respected', 13 + array: [1, 2, 3], 14 + }), 17 15 exclude: ['**/*.threads.spec.ts'], 18 16 }, 19 17 },
+1 -5
test/cli/fixtures/expect-soft/vite.config.ts
··· 3 3 export default defineConfig({ 4 4 test: { 5 5 pool: 'forks', 6 - poolOptions: { 7 - forks: { 8 - isolate: false, 9 - }, 10 - }, 6 + isolate: false, 11 7 }, 12 8 })
+1 -5
test/cli/fixtures/fails/vite.config.ts
··· 3 3 export default defineConfig({ 4 4 test: { 5 5 pool: 'forks', 6 - poolOptions: { 7 - forks: { 8 - isolate: false, 9 - }, 10 - }, 6 + isolate: false, 11 7 expect: { 12 8 requireAssertions: true, 13 9 }
-17
test/config/fixtures/pool-isolation/isolated.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - import type { TestUserConfig } from 'vitest/config' 3 - 4 - const pool = process.env.TESTED_POOL as "forks" | "threads"; 5 - 6 - test('is isolated', () => { 7 - // @ts-expect-error -- internal 8 - const config: TestUserConfig = globalThis.__vitest_worker__.config 9 - 10 - if (pool === 'forks') { 11 - expect(config.poolOptions?.forks?.isolate).toBe(true) 12 - } 13 - else { 14 - expect(pool).toBe('threads') 15 - expect(config.poolOptions?.threads?.isolate).toBe(true) 16 - } 17 - })
-17
test/config/fixtures/pool-isolation/non-isolated.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - import type { TestUserConfig } from 'vitest/config' 3 - 4 - const pool = process.env.TESTED_POOL as "forks" | "threads"; 5 - 6 - test('is isolated', () => { 7 - // @ts-expect-error -- internal 8 - const config: TestUserConfig = globalThis.__vitest_worker__.config 9 - 10 - if (pool === 'forks') { 11 - expect(config.poolOptions?.forks?.isolate).toBe(false) 12 - } 13 - else { 14 - expect(pool).toBe('threads') 15 - expect(config.poolOptions?.threads?.isolate).toBe(false) 16 - } 17 - })
+6
test/config/fixtures/pool/print-config.test.ts
··· 1 + import { test } from 'vitest' 2 + 3 + test('print config', () => { 4 + // @ts-expect-error -- internal 5 + console.log(JSON.stringify(globalThis.__vitest_worker__.config, null, 2)) 6 + })
+3 -3
test/typescript/test/__snapshots__/runner.test.ts.snap
··· 102 102 exports[`should fail > typechecks empty "include" but with tests 1`] = ` 103 103 "Testing types with tsc and vue-tsc is an experimental feature. 104 104 Breaking changes might not follow SemVer, please pin Vitest's version when using it. 105 - ⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯ 105 + ⎯⎯ Unhandled Errors ⎯⎯ 106 106 107 107 Vitest caught 1 unhandled error during the test run. 108 108 This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected. 109 109 110 - ⎯⎯⎯⎯⎯⎯ Typecheck Error ⎯⎯⎯⎯⎯⎯⎯ 110 + ⎯⎯ Typecheck Error ⎯⎯ 111 111 Error: error TS18003: No inputs were found in config file '<root>/tsconfig.empty.json'. Specified 'include' paths were '["src"]' and 'exclude' paths were '["**/dist/**"]'. 112 112 113 - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ 113 + ⎯⎯ 114 114 115 115 " 116 116 `;
+6 -5
packages/browser/src/client/tester/tester.ts
··· 1 1 import type { BrowserRPC, IframeChannelEvent } from '@vitest/browser/client' 2 + import type { FileSpecification } from '@vitest/runner' 2 3 import { channel, client, onCancel } from '@vitest/browser/client' 3 4 import { parse } from 'flatted' 4 5 import { page, server, userEvent } from 'vitest/browser' ··· 155 156 | Awaited<ReturnType<typeof prepareTestEnvironment>> 156 157 | undefined 157 158 158 - async function executeTests(method: 'run' | 'collect', files: string[]) { 159 + async function executeTests(method: 'run' | 'collect', files: FileSpecification[]) { 159 160 if (!preparedData) { 160 161 throw new Error(`Data was not properly initialized. This is a bug in Vitest. Please, open a new issue with reproduction.`) 161 162 } ··· 168 169 runner.setMethod(method) 169 170 170 171 const version = url.searchParams.get('browserv') || '' 171 - files.forEach((filename) => { 172 - const currentVersion = browserHashMap.get(filename) 172 + files.forEach(({ filepath }) => { 173 + const currentVersion = browserHashMap.get(filepath) 173 174 if (!currentVersion || currentVersion[1] !== version) { 174 - browserHashMap.set(filename, version) 175 + browserHashMap.set(filepath, version) 175 176 } 176 177 }) 177 178 178 179 debug?.('prepare time', state.durations.prepare, 'ms') 179 180 180 181 for (const file of files) { 181 - state.filepath = file 182 + state.filepath = file.filepath 182 183 183 184 if (method === 'run') { 184 185 await startTests([file], runner)
+8 -79
packages/vitest/src/node/cli/cli-config.ts
··· 1 1 import type { ApiConfig } from '../types/config' 2 - import type { 3 - ForksOptions, 4 - ThreadsOptions, 5 - VmOptions, 6 - WorkerContextOptions, 7 - } from '../types/pool-options' 8 2 import type { CliOptions } from './cli-api' 9 3 import { defaultBrowserPort, defaultPort } from '../../constants' 10 4 import { ReportersMap } from '../reporters' ··· 54 48 }, 55 49 middlewareMode: null, 56 50 }) 57 - 58 - const poolThreadsCommands: CLIOptions<ThreadsOptions & WorkerContextOptions> = { 59 - isolate: { 60 - description: 'Isolate tests in threads pool (default: `true`)', 61 - }, 62 - singleThread: { 63 - description: 'Run tests inside a single thread (default: `false`)', 64 - }, 65 - maxThreads: { 66 - description: 'Maximum number or percentage of threads to run tests in', 67 - argument: '<workers>', 68 - }, 69 - useAtomics: { 70 - description: 71 - 'Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`)', 72 - }, 73 - execArgv: null, 74 - } 75 - 76 - const poolForksCommands: CLIOptions<ForksOptions & WorkerContextOptions> = { 77 - isolate: { 78 - description: 'Isolate tests in forks pool (default: `true`)', 79 - }, 80 - singleFork: { 81 - description: 'Run tests inside a single child_process (default: `false`)', 82 - }, 83 - maxForks: { 84 - description: 'Maximum number or percentage of processes to run tests in', 85 - argument: '<workers>', 86 - }, 87 - execArgv: null, 88 - } 89 51 90 52 function watermarkTransform(value: unknown) { 91 53 if (typeof value === 'string') { ··· 443 405 argument: '<pool>', 444 406 subcommands: null, // don't support custom objects 445 407 }, 446 - poolOptions: { 447 - description: 'Specify pool options', 448 - argument: '<options>', 449 - // we use casting here because TypeScript (for some reason) makes this into CLIOption<unknown> 450 - // even when using casting, these types fail if the new option is added which is good 451 - subcommands: { 452 - threads: { 453 - description: 'Specify threads pool options', 454 - argument: '<options>', 455 - subcommands: poolThreadsCommands, 456 - } as CLIOption<ThreadsOptions & WorkerContextOptions>, 457 - vmThreads: { 458 - description: 'Specify VM threads pool options', 459 - argument: '<options>', 460 - subcommands: { 461 - ...poolThreadsCommands, 462 - memoryLimit: { 463 - description: 464 - 'Memory limit for VM threads pool. If you see memory leaks, try to tinker this value.', 465 - argument: '<limit>', 466 - }, 467 - }, 468 - } as CLIOption<ThreadsOptions & VmOptions>, 469 - forks: { 470 - description: 'Specify forks pool options', 471 - argument: '<options>', 472 - subcommands: poolForksCommands, 473 - } as CLIOption<ForksOptions & WorkerContextOptions>, 474 - vmForks: { 475 - description: 'Specify VM forks pool options', 476 - argument: '<options>', 477 - subcommands: { 478 - ...poolForksCommands, 479 - memoryLimit: { 480 - description: 481 - 'Memory limit for VM forks pool. If you see memory leaks, try to tinker this value.', 482 - argument: '<limit>', 483 - }, 484 - }, 485 - } as CLIOption<ForksOptions & VmOptions>, 486 - }, 408 + execArgv: { 409 + description: 'Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`.', 410 + argument: '<option>', 411 + }, 412 + vmMemoryLimit: { 413 + description: 414 + 'Memory limit for VM pools. If you see memory leaks, try to tinker this value.', 415 + argument: '<limit>', 487 416 }, 488 417 fileParallelism: { 489 418 description:
+20 -62
packages/vitest/src/node/config/resolveConfig.ts
··· 8 8 UserConfig, 9 9 } from '../types/config' 10 10 import type { BaseCoverageOptions, CoverageReporterWithOptions } from '../types/coverage' 11 - import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options' 12 11 import crypto from 'node:crypto' 13 12 import { slash, toArray } from '@vitest/utils/helpers' 14 13 import { resolveModule } from 'local-pkg' ··· 24 23 import { benchmarkConfigDefaults, configDefaults } from '../../defaults' 25 24 import { isCI, stdProvider } from '../../utils/env' 26 25 import { getWorkersCountByPercentage } from '../../utils/workers' 27 - import { builtinPools } from '../pool' 28 26 import { BaseSequencer } from '../sequencers/BaseSequencer' 29 27 import { RandomSequencer } from '../sequencers/RandomSequencer' 30 28 ··· 143 141 mode, 144 142 } as any as ResolvedConfig 145 143 144 + if (options.pool && typeof options.pool !== 'string') { 145 + resolved.pool = options.pool.name 146 + resolved.poolRunner = options.pool 147 + } 148 + 149 + resolved.pool ??= 'forks' 150 + 146 151 resolved.project = toArray(resolved.project) 147 152 resolved.provide ??= {} 148 153 ··· 222 227 } 223 228 224 229 if (resolved.inspect || resolved.inspectBrk) { 225 - const isSingleThread 226 - = resolved.pool === 'threads' 227 - && resolved.poolOptions?.threads?.singleThread 228 - const isSingleFork 229 - = resolved.pool === 'forks' && resolved.poolOptions?.forks?.singleFork 230 - 231 - if (resolved.fileParallelism && !isSingleThread && !isSingleFork) { 230 + if (resolved.fileParallelism) { 232 231 const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}` 233 232 throw new Error( 234 - `You cannot use ${inspectOption} without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"`, 233 + `You cannot use ${inspectOption} without "--no-file-parallelism"`, 235 234 ) 236 235 } 237 236 } ··· 499 498 delete (resolved as any).resolveSnapshotPath 500 499 } 501 500 501 + resolved.execArgv ??= [] 502 502 resolved.pool ??= 'threads' 503 503 504 - if (process.env.VITEST_MAX_THREADS) { 505 - resolved.poolOptions = { 506 - ...resolved.poolOptions, 507 - threads: { 508 - ...resolved.poolOptions?.threads, 509 - maxThreads: Number.parseInt(process.env.VITEST_MAX_THREADS), 510 - }, 511 - vmThreads: { 512 - ...resolved.poolOptions?.vmThreads, 513 - maxThreads: Number.parseInt(process.env.VITEST_MAX_THREADS), 514 - }, 515 - } 504 + if ( 505 + resolved.pool === 'vmForks' 506 + || resolved.pool === 'vmThreads' 507 + || resolved.pool === 'typescript' 508 + ) { 509 + resolved.isolate = false 516 510 } 517 511 518 - if (process.env.VITEST_MAX_FORKS) { 519 - resolved.poolOptions = { 520 - ...resolved.poolOptions, 521 - forks: { 522 - ...resolved.poolOptions?.forks, 523 - maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS), 524 - }, 525 - vmForks: { 526 - ...resolved.poolOptions?.vmForks, 527 - maxForks: Number.parseInt(process.env.VITEST_MAX_FORKS), 528 - }, 529 - } 530 - } 531 - 532 - const poolThreadsOptions = [ 533 - ['threads', 'maxThreads'], 534 - ['vmThreads', 'maxThreads'], 535 - ] as const satisfies [keyof PoolOptions, keyof ThreadsOptions][] 536 - 537 - for (const [poolOptionKey, workerOptionKey] of poolThreadsOptions) { 538 - if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) { 539 - resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!) 540 - } 541 - } 542 - 543 - const poolForksOptions = [ 544 - ['forks', 'maxForks'], 545 - ['vmForks', 'maxForks'], 546 - ] as const satisfies [keyof PoolOptions, keyof ForksOptions][] 547 - 548 - for (const [poolOptionKey, workerOptionKey] of poolForksOptions) { 549 - if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) { 550 - resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!) 551 - } 552 - } 553 - 554 - if (!builtinPools.includes(resolved.pool as BuiltinPool)) { 555 - resolved.pool = resolvePath(resolved.pool, resolved.root) 512 + if (process.env.VITEST_MAX_WORKERS) { 513 + resolved.maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS) 556 514 } 557 515 558 516 if (mode === 'benchmark') { ··· 846 804 ) 847 805 } 848 806 849 - resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000 850 - resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000 807 + resolved.testTimeout ??= resolved.browser.enabled ? 30_000 : 5_000 808 + resolved.hookTimeout ??= resolved.browser.enabled ? 30_000 : 10_000 851 809 852 810 return resolved 853 811 }
+2 -40
packages/vitest/src/node/config/serializeConfig.ts
··· 5 5 const { config, globalConfig } = project 6 6 const viteConfig = project._vite?.config 7 7 const optimizer = config.deps?.optimizer || {} 8 - const poolOptions = config.poolOptions 9 - 10 - // Resolve from server.config to avoid comparing against default value 11 - const isolate = viteConfig?.test?.isolate 12 8 13 9 return { 14 10 // TODO: remove functions from environmentOptions 15 11 environmentOptions: config.environmentOptions, 16 12 mode: config.mode, 17 13 isolate: config.isolate, 14 + fileParallelism: config.fileParallelism, 15 + maxWorkers: config.maxWorkers, 18 16 base: config.base, 19 17 logHeapUsage: config.logHeapUsage, 20 18 runner: config.runner, ··· 63 61 } 64 62 })(config.coverage), 65 63 fakeTimers: config.fakeTimers, 66 - poolOptions: { 67 - forks: { 68 - singleFork: 69 - poolOptions?.forks?.singleFork 70 - ?? globalConfig.poolOptions?.forks?.singleFork 71 - ?? false, 72 - isolate: 73 - poolOptions?.forks?.isolate 74 - ?? isolate 75 - ?? globalConfig.poolOptions?.forks?.isolate 76 - ?? true, 77 - }, 78 - threads: { 79 - singleThread: 80 - poolOptions?.threads?.singleThread 81 - ?? globalConfig.poolOptions?.threads?.singleThread 82 - ?? false, 83 - isolate: 84 - poolOptions?.threads?.isolate 85 - ?? isolate 86 - ?? globalConfig.poolOptions?.threads?.isolate 87 - ?? true, 88 - }, 89 - vmThreads: { 90 - singleThread: 91 - poolOptions?.vmThreads?.singleThread 92 - ?? globalConfig.poolOptions?.vmThreads?.singleThread 93 - ?? false, 94 - }, 95 - vmForks: { 96 - singleFork: 97 - poolOptions?.vmForks?.singleFork 98 - ?? globalConfig.poolOptions?.vmForks?.singleFork 99 - ?? false, 100 - }, 101 - }, 102 64 deps: { 103 65 web: config.deps.web || {}, 104 66 optimizer: Object.entries(optimizer).reduce((acc, [name, option]) => {
-16
packages/vitest/src/node/plugins/index.ts
··· 113 113 }, 114 114 }, 115 115 test: { 116 - poolOptions: { 117 - threads: { 118 - isolate: 119 - options.poolOptions?.threads?.isolate 120 - ?? options.isolate 121 - ?? testConfig.poolOptions?.threads?.isolate 122 - ?? viteConfig.test?.isolate, 123 - }, 124 - forks: { 125 - isolate: 126 - options.poolOptions?.forks?.isolate 127 - ?? options.isolate 128 - ?? testConfig.poolOptions?.forks?.isolate 129 - ?? viteConfig.test?.isolate, 130 - }, 131 - }, 132 116 root: testConfig.root ?? viteConfig.test?.root, 133 117 deps: testConfig.deps ?? viteConfig.test?.deps, 134 118 },
+16 -9
packages/vitest/src/node/pools/browser.ts
··· 1 + import type { FileSpecification } from '@vitest/runner' 1 2 import type { DeferPromise } from '@vitest/utils/helpers' 2 3 import type { Vitest } from '../core' 3 4 import type { ProcessPool } from '../pool' ··· 20 21 ? nodeos.availableParallelism() 21 22 : nodeos.cpus().length 22 23 24 + // if there are more than ~12 threads (optimistically), the main thread chokes 25 + // https://github.com/vitest-dev/vitest/issues/7871 26 + const maxThreadsCount = Math.min(12, numCpus - 1) 23 27 const threadsCount = vitest.config.watch 24 - ? Math.max(Math.floor(numCpus / 2), 1) 25 - : Math.max(numCpus - 1, 1) 28 + ? Math.max(Math.floor(maxThreadsCount / 2), 1) 29 + : Math.max(maxThreadsCount, 1) 26 30 27 31 const projectPools = new WeakMap<TestProject, BrowserPool>() 28 32 ··· 55 59 } 56 60 57 61 const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => { 58 - const groupedFiles = new Map<TestProject, string[]>() 59 - for (const { project, moduleId } of specs) { 62 + const groupedFiles = new Map<TestProject, FileSpecification[]>() 63 + for (const { project, moduleId, testLines } of specs) { 60 64 const files = groupedFiles.get(project) || [] 61 - files.push(moduleId) 65 + files.push({ 66 + filepath: moduleId, 67 + testLocations: testLines, 68 + }) 62 69 groupedFiles.set(project, files) 63 70 } 64 71 ··· 81 88 debug?.('provider is ready for %s project', project.name) 82 89 83 90 const pool = ensurePool(project) 84 - vitest.state.clearFiles(project, files) 91 + vitest.state.clearFiles(project, files.map(f => f.filepath)) 85 92 providers.add(project.browser!.provider) 86 93 87 94 return { ··· 163 170 } 164 171 165 172 class BrowserPool { 166 - private _queue: string[] = [] 173 + private _queue: FileSpecification[] = [] 167 174 private _promise: DeferPromise<void> | undefined 168 175 private _providedContext: string | undefined 169 176 ··· 191 198 return this.project.browser!.state.orchestrators 192 199 } 193 200 194 - async runTests(method: 'run' | 'collect', files: string[]): Promise<void> { 201 + async runTests(method: 'run' | 'collect', files: FileSpecification[]): Promise<void> { 195 202 this._promise ??= createDefer<void>() 196 203 197 204 if (!files.length) { ··· 311 318 const orchestrator = this.getOrchestrator(sessionId) 312 319 debug?.('[%s] run test %s', sessionId, file) 313 320 314 - this.setBreakpoint(sessionId, file).then(() => { 321 + this.setBreakpoint(sessionId, file.filepath).then(() => { 315 322 // this starts running tests inside the orchestrator 316 323 orchestrator.createTesters( 317 324 {
-307
packages/vitest/src/node/pools/forks.ts
··· 1 - import type { FileSpecification } from '@vitest/runner' 2 - import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' 3 - import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 - import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' 5 - import type { Vitest } from '../core' 6 - import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' 7 - import type { TestProject } from '../project' 8 - import type { TestSpecification } from '../spec' 9 - import type { SerializedConfig } from '../types/config' 10 - import EventEmitter from 'node:events' 11 - import * as nodeos from 'node:os' 12 - import { resolve } from 'node:path' 13 - import v8 from 'node:v8' 14 - import { createBirpc } from 'birpc' 15 - import { Tinypool } from 'tinypool' 16 - import { groupBy } from '../../utils/base' 17 - import { wrapSerializableConfig } from '../../utils/config-helpers' 18 - import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' 19 - import { createMethodsRPC } from './rpc' 20 - 21 - function createChildProcessChannel(project: TestProject, collect = false) { 22 - const emitter = new EventEmitter() 23 - const events = { message: 'message', response: 'response' } 24 - 25 - const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { cacheFs: true, collect }), { 26 - eventNames: ['onCancel'], 27 - serialize: v8.serialize, 28 - deserialize: (v) => { 29 - try { 30 - return v8.deserialize(Buffer.from(v)) 31 - } 32 - catch (error) { 33 - let stringified = '' 34 - 35 - try { 36 - stringified = `\nReceived value: ${JSON.stringify(v)}` 37 - } 38 - catch {} 39 - 40 - throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error }) 41 - } 42 - }, 43 - post(v) { 44 - emitter.emit(events.message, v) 45 - }, 46 - on(fn) { 47 - emitter.on(events.response, fn) 48 - }, 49 - timeout: -1, 50 - }) 51 - 52 - project.vitest.onCancel(reason => rpc.onCancel(reason)) 53 - 54 - const channel: TinypoolChannel = { 55 - onMessage: callback => emitter.on(events.message, callback), 56 - postMessage: message => emitter.emit(events.response, message), 57 - onClose: () => { 58 - emitter.removeAllListeners() 59 - rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc')) 60 - }, 61 - } 62 - 63 - return channel 64 - } 65 - 66 - export function createForksPool( 67 - vitest: Vitest, 68 - { execArgv, env }: PoolProcessOptions, 69 - specifications: TestSpecification[], 70 - ): ProcessPool { 71 - const numCpus 72 - = typeof nodeos.availableParallelism === 'function' 73 - ? nodeos.availableParallelism() 74 - : nodeos.cpus().length 75 - 76 - const threadsCount = vitest.config.watch 77 - ? Math.max(Math.floor(numCpus / 2), 1) 78 - : Math.max(numCpus - 1, 1) 79 - 80 - const recommendedCount = vitest.config.watch 81 - ? threadsCount 82 - : Math.min(threadsCount, specifications.length) 83 - 84 - const poolOptions = vitest.config.poolOptions?.forks ?? {} 85 - 86 - const maxThreads 87 - = poolOptions.maxForks ?? vitest.config.maxWorkers ?? recommendedCount 88 - const minThreads = vitest.config.watch 89 - ? Math.min(recommendedCount, maxThreads) 90 - // avoid recreating threads when tests are finished 91 - : 0 92 - 93 - const options: TinypoolOptions = { 94 - runtime: 'child_process', 95 - filename: resolve(vitest.distPath, 'worker-base.js'), 96 - teardown: 'teardown', 97 - 98 - maxThreads, 99 - minThreads, 100 - 101 - env, 102 - execArgv: [...(poolOptions.execArgv ?? []), ...execArgv], 103 - 104 - terminateTimeout: vitest.config.teardownTimeout, 105 - concurrentTasksPerWorker: 1, 106 - } 107 - 108 - const isolated = poolOptions.isolate ?? true 109 - 110 - if (isolated) { 111 - options.isolateWorkers = true 112 - } 113 - 114 - if (poolOptions.singleFork || !vitest.config.fileParallelism) { 115 - options.maxThreads = 1 116 - options.minThreads = 1 117 - } 118 - 119 - const pool = new Tinypool(options) 120 - 121 - const runWithFiles = (name: string): RunWithFiles => { 122 - let id = 0 123 - 124 - async function runFiles( 125 - project: TestProject, 126 - config: SerializedConfig, 127 - files: FileSpecification[], 128 - environment: ContextTestEnvironment, 129 - invalidates: string[] = [], 130 - ) { 131 - const paths = files.map(f => f.filepath) 132 - vitest.state.clearFiles(project, paths) 133 - 134 - const channel = createChildProcessChannel(project, name === 'collect') 135 - const workerId = ++id 136 - const data: ContextRPC = { 137 - pool: 'forks', 138 - config, 139 - files, 140 - invalidates, 141 - environment, 142 - workerId, 143 - projectName: project.name, 144 - providedContext: project.getProvidedContext(), 145 - } 146 - try { 147 - await pool.run(data, { name, channel }) 148 - } 149 - catch (error) { 150 - // Worker got stuck and won't terminate - this may cause process to hang 151 - if ( 152 - error instanceof Error 153 - && /Failed to terminate worker/.test(error.message) 154 - ) { 155 - vitest.state.addProcessTimeoutCause( 156 - `Failed to terminate worker while running ${paths.join(', ')}.`, 157 - ) 158 - } 159 - // Intentionally cancelled 160 - else if ( 161 - vitest.isCancelling 162 - && error instanceof Error 163 - && /The task has been cancelled/.test(error.message) 164 - ) { 165 - vitest.state.cancelFiles(paths, project) 166 - } 167 - else { 168 - throw error 169 - } 170 - } 171 - } 172 - 173 - return async (specs, invalidates) => { 174 - // Cancel pending tasks from pool when possible 175 - vitest.onCancel(() => pool.cancelPendingTasks()) 176 - 177 - const configs = new WeakMap<TestProject, SerializedConfig>() 178 - const getConfig = (project: TestProject): SerializedConfig => { 179 - if (configs.has(project)) { 180 - return configs.get(project)! 181 - } 182 - 183 - const _config = project.serializedConfig 184 - const config = wrapSerializableConfig(_config) 185 - 186 - configs.set(project, config) 187 - return config 188 - } 189 - 190 - const singleFork = specs.filter( 191 - spec => spec.project.config.poolOptions?.forks?.singleFork, 192 - ) 193 - const multipleForks = specs.filter( 194 - spec => !spec.project.config.poolOptions?.forks?.singleFork, 195 - ) 196 - 197 - if (multipleForks.length) { 198 - const filesByEnv = await groupFilesByEnv(multipleForks) 199 - const files = Object.values(filesByEnv).flat() 200 - const results: PromiseSettledResult<void>[] = [] 201 - 202 - if (isolated) { 203 - results.push( 204 - ...(await Promise.allSettled( 205 - files.map(({ file, environment, project }) => 206 - runFiles( 207 - project, 208 - getConfig(project), 209 - [file], 210 - environment, 211 - invalidates, 212 - ), 213 - ), 214 - )), 215 - ) 216 - } 217 - else { 218 - // When isolation is disabled, we still need to isolate environments and workspace projects from each other. 219 - // Tasks are still running parallel but environments are isolated between tasks. 220 - const grouped = groupBy( 221 - files, 222 - ({ project, environment }) => 223 - project.name 224 - + environment.name 225 - + JSON.stringify(environment.options), 226 - ) 227 - 228 - for (const group of Object.values(grouped)) { 229 - // Push all files to pool's queue 230 - results.push( 231 - ...(await Promise.allSettled( 232 - group.map(({ file, environment, project }) => 233 - runFiles( 234 - project, 235 - getConfig(project), 236 - [file], 237 - environment, 238 - invalidates, 239 - ), 240 - ), 241 - )), 242 - ) 243 - 244 - // Once all tasks are running or finished, recycle worker for isolation. 245 - // On-going workers will run in the previous environment. 246 - await new Promise<void>(resolve => 247 - pool.queueSize === 0 ? resolve() : pool.once('drain', resolve), 248 - ) 249 - await pool.recycleWorkers() 250 - } 251 - } 252 - 253 - const errors = results 254 - .filter((r): r is PromiseRejectedResult => r.status === 'rejected') 255 - .map(r => r.reason) 256 - if (errors.length > 0) { 257 - throw new AggregateError( 258 - errors, 259 - 'Errors occurred while running tests. For more information, see serialized error.', 260 - ) 261 - } 262 - } 263 - 264 - if (singleFork.length) { 265 - const filesByEnv = await groupFilesByEnv(singleFork) 266 - const envs = envsOrder.concat( 267 - Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), 268 - ) 269 - 270 - for (const env of envs) { 271 - const files = filesByEnv[env] 272 - 273 - if (!files?.length) { 274 - continue 275 - } 276 - 277 - const filesByOptions = groupBy( 278 - files, 279 - ({ project, environment }) => 280 - project.name + JSON.stringify(environment.options), 281 - ) 282 - 283 - for (const files of Object.values(filesByOptions)) { 284 - // Always run environments isolated between each other 285 - await pool.recycleWorkers() 286 - 287 - const filenames = files.map(f => f.file) 288 - await runFiles( 289 - files[0].project, 290 - getConfig(files[0].project), 291 - filenames, 292 - files[0].environment, 293 - invalidates, 294 - ) 295 - } 296 - } 297 - } 298 - } 299 - } 300 - 301 - return { 302 - name: 'forks', 303 - runTests: runWithFiles('run'), 304 - collectTests: runWithFiles('collect'), 305 - close: () => pool.destroy(), 306 - } 307 - }
+297
packages/vitest/src/node/pools/pool.ts
··· 1 + import type { Logger } from '../logger' 2 + import type { StateManager } from '../state' 3 + import type { PoolOptions, PoolTask, WorkerResponse } from './types' 4 + import { PoolRunner } from './poolRunner' 5 + import { ForksPoolWorker } from './workers/forksWorker' 6 + import { ThreadsPoolWorker } from './workers/threadsWorker' 7 + import { TypecheckPoolWorker } from './workers/typecheckWorker' 8 + import { VmForksPoolWorker } from './workers/vmForksWorker' 9 + import { VmThreadsPoolWorker } from './workers/vmThreadsWorker' 10 + 11 + const WORKER_START_TIMEOUT = 5_000 12 + 13 + interface Options { 14 + distPath: string 15 + teardownTimeout: number 16 + state: StateManager 17 + } 18 + 19 + interface QueuedTask { 20 + task: PoolTask 21 + resolver: ReturnType<typeof withResolvers> 22 + method: 'run' | 'collect' 23 + } 24 + 25 + interface ActiveTask extends QueuedTask { 26 + cancelTask: () => Promise<void> 27 + } 28 + 29 + export class Pool { 30 + private maxWorkers: number = 0 31 + private workerIds = new Map<number, boolean>() 32 + 33 + private queue: QueuedTask[] = [] 34 + private activeTasks: ActiveTask[] = [] 35 + private sharedRunners: PoolRunner[] = [] 36 + private exitPromises: Promise<void>[] = [] 37 + private _isCancelling: boolean = false 38 + 39 + constructor(private options: Options, private logger: Logger) {} 40 + 41 + setMaxWorkers(maxWorkers: number): void { 42 + this.maxWorkers = maxWorkers 43 + 44 + this.workerIds = new Map( 45 + Array.from({ length: maxWorkers }).fill(0).map((_, i) => [i + 1, true]), 46 + ) 47 + } 48 + 49 + async run(task: PoolTask, method: 'run' | 'collect'): Promise<void> { 50 + // Prevent new tasks from being queued during cancellation 51 + if (this._isCancelling) { 52 + throw new Error('[vitest-pool]: Cannot run tasks while pool is cancelling') 53 + } 54 + 55 + // Every runner related failure should make this promise reject so that it's picked by pool. 56 + // This resolver is used to make the error handling in recursive queue easier. 57 + const testFinish = withResolvers() 58 + 59 + this.queue.push({ task, resolver: testFinish, method }) 60 + void this.schedule() 61 + 62 + await testFinish.promise 63 + } 64 + 65 + private async schedule(): Promise<void> { 66 + if (this.queue.length === 0 || this.activeTasks.length >= this.maxWorkers) { 67 + return 68 + } 69 + 70 + const { task, resolver, method } = this.queue.shift()! 71 + 72 + try { 73 + let isMemoryLimitReached = false 74 + const runner = this.getPoolRunner(task, method) 75 + 76 + const activeTask = { task, resolver, method, cancelTask } 77 + this.activeTasks.push(activeTask) 78 + 79 + runner.on('error', (error) => { 80 + resolver.reject( 81 + new Error(`[vitest-pool]: Worker ${task.worker} emitted error.`, { cause: error }), 82 + ) 83 + }) 84 + 85 + async function cancelTask() { 86 + await runner.stop() 87 + resolver.reject(new Error('Cancelled')) 88 + } 89 + 90 + const onFinished = (message: WorkerResponse) => { 91 + if (message?.__vitest_worker_response__ && message.type === 'testfileFinished') { 92 + if (task.memoryLimit && message.usedMemory) { 93 + isMemoryLimitReached = message.usedMemory >= task.memoryLimit 94 + } 95 + if (message.error) { 96 + this.options.state.catchError(message.error, 'Test Run Error') 97 + } 98 + 99 + runner.off('message', onFinished) 100 + resolver.resolve() 101 + } 102 + } 103 + 104 + runner.on('message', onFinished) 105 + 106 + if (!runner.isStarted) { 107 + const id = setTimeout( 108 + () => resolver.reject(new Error(`[vitest-pool]: Timeout starting ${task.worker} runner.`)), 109 + WORKER_START_TIMEOUT, 110 + ) 111 + 112 + await runner.start().finally(() => clearTimeout(id)) 113 + } 114 + 115 + const poolId = runner.poolId ?? this.getWorkerId() 116 + runner.poolId = poolId 117 + 118 + // Start running the test in the worker 119 + runner.postMessage({ 120 + __vitest_worker_request__: true, 121 + type: method, 122 + context: task.context, 123 + poolId, 124 + }) 125 + 126 + await resolver.promise 127 + 128 + const index = this.activeTasks.indexOf(activeTask) 129 + if (index !== -1) { 130 + this.activeTasks.splice(index, 1) 131 + } 132 + 133 + if ( 134 + !task.isolate 135 + && !isMemoryLimitReached 136 + && this.queue[0]?.task.isolate === false 137 + && isEqualRunner(runner, this.queue[0].task) 138 + ) { 139 + this.sharedRunners.push(runner) 140 + return this.schedule() 141 + } 142 + 143 + // Runner terminations are started but not awaited until the end of full run. 144 + // Runner termination can also already start from task cancellation. 145 + if (!runner.isTerminated) { 146 + const id = setTimeout( 147 + () => this.logger.error(`[vitest-pool]: Timeout terminating ${task.worker} worker for test files ${formatFiles(task)}.`), 148 + this.options.teardownTimeout, 149 + ) 150 + 151 + this.exitPromises.push( 152 + runner.stop() 153 + .then(() => clearTimeout(id)) 154 + .catch(error => this.logger.error(`[vitest-pool]: Failed to terminate ${task.worker} worker for test files ${formatFiles(task)}.`, error)), 155 + ) 156 + } 157 + 158 + this.freeWorkerId(poolId) 159 + } 160 + 161 + // This is mostly to avoid zombie workers when/if Vitest internals run into errors 162 + catch (error) { 163 + return resolver.reject(error) 164 + } 165 + 166 + return this.schedule() 167 + } 168 + 169 + async cancel(): Promise<void> { 170 + // Set flag to prevent new tasks from being queued 171 + this._isCancelling = true 172 + 173 + const pendingTasks = this.queue.splice(0) 174 + 175 + if (pendingTasks.length) { 176 + const error = new Error('Cancelled') 177 + pendingTasks.forEach(task => task.resolver.reject(error)) 178 + } 179 + 180 + const activeTasks = this.activeTasks.splice(0) 181 + await Promise.all(activeTasks.map(task => task.cancelTask())) 182 + 183 + const sharedRunners = this.sharedRunners.splice(0) 184 + await Promise.all(sharedRunners.map(runner => runner.stop())) 185 + 186 + await Promise.all(this.exitPromises.splice(0)) 187 + 188 + this.workerIds.forEach((_, id) => this.freeWorkerId(id)) 189 + 190 + // Reset flag after cancellation completes 191 + this._isCancelling = false 192 + } 193 + 194 + async close(): Promise<void> { 195 + await this.cancel() 196 + } 197 + 198 + private getPoolRunner(task: PoolTask, method: 'run' | 'collect'): PoolRunner { 199 + if (task.isolate === false) { 200 + const index = this.sharedRunners.findIndex(runner => isEqualRunner(runner, task)) 201 + 202 + if (index !== -1) { 203 + return this.sharedRunners.splice(index, 1)[0] 204 + } 205 + } 206 + 207 + const options: PoolOptions = { 208 + distPath: this.options.distPath, 209 + project: task.project, 210 + method, 211 + environment: task.context.environment.name, 212 + env: task.env, 213 + execArgv: task.execArgv, 214 + } 215 + 216 + switch (task.worker) { 217 + case 'forks': 218 + return new PoolRunner(options, new ForksPoolWorker(options)) 219 + 220 + case 'vmForks': 221 + return new PoolRunner(options, new VmForksPoolWorker(options)) 222 + 223 + case 'threads': 224 + return new PoolRunner(options, new ThreadsPoolWorker(options)) 225 + 226 + case 'vmThreads': 227 + return new PoolRunner(options, new VmThreadsPoolWorker(options)) 228 + 229 + case 'typescript': 230 + return new PoolRunner(options, new TypecheckPoolWorker(options)) 231 + } 232 + 233 + const customPool = task.project.config.poolRunner 234 + if (customPool != null && customPool.name === task.worker) { 235 + return new PoolRunner(options, customPool.createPoolWorker(options)) 236 + } 237 + 238 + throw new Error(`Runner ${task.worker} is not supported. Test files: ${formatFiles(task)}.`) 239 + } 240 + 241 + private getWorkerId() { 242 + let workerId = 0 243 + 244 + this.workerIds.forEach((state, id) => { 245 + if (state && !workerId) { 246 + workerId = id 247 + this.workerIds.set(id, false) 248 + } 249 + }) 250 + 251 + return workerId 252 + } 253 + 254 + private freeWorkerId(id: number) { 255 + this.workerIds.set(id, true) 256 + } 257 + } 258 + 259 + function withResolvers() { 260 + let resolve = () => {} 261 + let reject = (_error: unknown) => {} 262 + 263 + const promise = new Promise<void>((res, rej) => { 264 + resolve = res 265 + reject = rej 266 + }) 267 + 268 + return { resolve, reject, promise } 269 + } 270 + 271 + function formatFiles(task: PoolTask) { 272 + return task.context.files.map(file => file.filepath).join(', ') 273 + } 274 + 275 + function isEqualRunner(runner: PoolRunner, task: PoolTask) { 276 + if (task.isolate) { 277 + throw new Error('Isolated tasks should not share runners') 278 + } 279 + 280 + return ( 281 + runner.worker.name === task.worker 282 + && runner.project === task.project 283 + && runner.environment === task.context.environment.name 284 + && runner.worker.execArgv.every((arg, index) => task.execArgv[index] === arg) 285 + && isEnvEqual(runner.worker.env, task.env) 286 + ) 287 + } 288 + 289 + function isEnvEqual(a: PoolOptions['env'], b: PoolTask['env']) { 290 + const keys = Object.keys(a) 291 + 292 + if (keys.length !== Object.keys(b).length) { 293 + return false 294 + } 295 + 296 + return keys.every(key => a[key] === b[key]) 297 + }
+274
packages/vitest/src/node/pools/poolRunner.ts
··· 1 + import type { DeferPromise } from '@vitest/utils/helpers' 2 + import type { BirpcReturn } from 'birpc' 3 + import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 + import type { TestProject } from '../project' 5 + import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from './types' 6 + import { EventEmitter } from 'node:events' 7 + import { createDefer } from '@vitest/utils/helpers' 8 + import { createBirpc } from 'birpc' 9 + import { createMethodsRPC } from './rpc' 10 + 11 + enum RunnerState { 12 + IDLE = 'idle', 13 + STARTING = 'starting', 14 + STARTED = 'started', 15 + STOPPING = 'stopping', 16 + STOPPED = 'stopped', 17 + } 18 + 19 + const START_TIMEOUT = 10_000 20 + const STOP_TIMEOUT = 10_000 21 + 22 + /** @experimental */ 23 + export class PoolRunner { 24 + /** Exposed to test runner as `VITEST_POOL_ID`. Value is between 1-`maxWorkers`. */ 25 + public poolId: number | undefined = undefined 26 + 27 + public readonly project: TestProject 28 + public readonly environment: string 29 + 30 + private _state: RunnerState = RunnerState.IDLE 31 + private _operationLock: DeferPromise<void> | null = null 32 + 33 + private _eventEmitter: EventEmitter<{ 34 + message: [WorkerResponse] 35 + error: [Error] 36 + rpc: [unknown] 37 + }> = new EventEmitter() 38 + 39 + private _rpc: BirpcReturn<RunnerRPC, RuntimeRPC> 40 + 41 + public get isTerminated(): boolean { 42 + return this._state === RunnerState.STOPPED 43 + } 44 + 45 + public get isStarted(): boolean { 46 + return this._state === RunnerState.STARTED 47 + } 48 + 49 + constructor(options: PoolOptions, public worker: PoolWorker) { 50 + this.project = options.project 51 + this.environment = options.environment 52 + this._rpc = createBirpc<RunnerRPC, RuntimeRPC>( 53 + createMethodsRPC(this.project, { 54 + collect: options.method === 'collect', 55 + cacheFs: worker.cacheFs, 56 + }), 57 + { 58 + eventNames: ['onCancel'], 59 + post: request => this.postMessage(request), 60 + on: callback => this._eventEmitter.on('rpc', callback), 61 + timeout: -1, 62 + }, 63 + ) 64 + 65 + this.project.vitest.onCancel(reason => this._rpc.onCancel(reason)) 66 + } 67 + 68 + postMessage(message: WorkerRequest): void { 69 + // Only send messages when runner is active (not fully stopped) 70 + // Allow sending during STOPPING state for the 'stop' message itself 71 + if (this._state !== RunnerState.STOPPED) { 72 + return this.worker.send(message) 73 + } 74 + } 75 + 76 + async start(): Promise<void> { 77 + // Wait for any ongoing operation to complete 78 + if (this._operationLock) { 79 + await this._operationLock 80 + } 81 + 82 + // If already started or starting, return early 83 + if (this._state === RunnerState.STARTED || this._state === RunnerState.STARTING) { 84 + return 85 + } 86 + 87 + // If stopped, cannot restart 88 + if (this._state === RunnerState.STOPPED) { 89 + throw new Error('[vitest-pool-runner]: Cannot start a stopped runner') 90 + } 91 + 92 + // Create operation lock to prevent concurrent start/stop 93 + this._operationLock = createDefer() 94 + 95 + try { 96 + this._state = RunnerState.STARTING 97 + 98 + await this.worker.start() 99 + 100 + // Attach event listeners AFTER starting worker to avoid issues 101 + // if worker.start() fails 102 + this.worker.on('error', this.emitWorkerError) 103 + this.worker.on('exit', this.emitUnexpectedExit) 104 + this.worker.on('message', this.emitWorkerMessage) 105 + 106 + // Wait for 'started' message with timeout 107 + const startPromise = this.withTimeout(this.waitForStart(), START_TIMEOUT) 108 + 109 + this.postMessage({ 110 + type: 'start', 111 + __vitest_worker_request__: true, 112 + options: { 113 + reportMemory: this.worker.reportMemory ?? false, 114 + }, 115 + }) 116 + 117 + await startPromise 118 + 119 + this._state = RunnerState.STARTED 120 + } 121 + catch (error) { 122 + this._state = RunnerState.IDLE 123 + throw error 124 + } 125 + finally { 126 + this._operationLock.resolve() 127 + this._operationLock = null 128 + } 129 + } 130 + 131 + async stop(): Promise<void> { 132 + // Wait for any ongoing operation to complete 133 + if (this._operationLock) { 134 + await this._operationLock 135 + } 136 + 137 + // If already stopped or stopping, return early 138 + if (this._state === RunnerState.STOPPED || this._state === RunnerState.STOPPING) { 139 + return 140 + } 141 + 142 + // If never started, just mark as stopped 143 + if (this._state === RunnerState.IDLE) { 144 + this._state = RunnerState.STOPPED 145 + return 146 + } 147 + 148 + // Create operation lock to prevent concurrent start/stop 149 + this._operationLock = createDefer() 150 + 151 + try { 152 + this._state = RunnerState.STOPPING 153 + 154 + // Remove exit listener early to avoid "unexpected exit" errors during shutdown 155 + this.worker.off('exit', this.emitUnexpectedExit) 156 + 157 + // Wait for 'stopped' message with timeout 158 + await this.withTimeout( 159 + new Promise<void>((resolve) => { 160 + const onStop = (response: WorkerResponse) => { 161 + if (response.type === 'stopped') { 162 + if (response.error) { 163 + this.project.vitest.state.catchError( 164 + response.error, 165 + 'Teardown Error', 166 + ) 167 + } 168 + 169 + resolve() 170 + this.off('message', onStop) 171 + } 172 + } 173 + 174 + this.on('message', onStop) 175 + this.postMessage({ type: 'stop', __vitest_worker_request__: true }) 176 + }), 177 + STOP_TIMEOUT, 178 + ) 179 + 180 + // Clean up internal event emitter and RPC 181 + // This implicitly cleans up all message listeners 182 + this._eventEmitter.removeAllListeners() 183 + this._rpc.$close(new Error('[vitest-pool-runner]: Pending methods while closing rpc')) 184 + 185 + // Stop the worker process (this sets _fork/_thread to undefined) 186 + // Worker's event listeners (error, message) are implicitly removed when worker terminates 187 + await this.worker.stop() 188 + 189 + this._state = RunnerState.STOPPED 190 + } 191 + catch (error) { 192 + // Ensure we transition to stopped state even on error 193 + this._state = RunnerState.STOPPED 194 + throw error 195 + } 196 + finally { 197 + this._operationLock.resolve() 198 + this._operationLock = null 199 + } 200 + } 201 + 202 + on(event: 'message', callback: (message: WorkerResponse) => void): void 203 + on(event: 'error', callback: (error: Error) => void): void 204 + on(event: 'message' | 'error', callback: (arg: any) => void): void { 205 + this._eventEmitter.on(event, callback) 206 + } 207 + 208 + off(event: 'message', callback: (message: WorkerResponse) => void): void 209 + off(event: 'error', callback: (error: Error) => void): void 210 + off(event: 'message' | 'error', callback: (arg: any) => void): void { 211 + this._eventEmitter.off(event, callback) 212 + } 213 + 214 + private emitWorkerError = (maybeError: unknown): void => { 215 + const error = maybeError instanceof Error ? maybeError : new Error(String(maybeError)) 216 + 217 + this._eventEmitter.emit('error', error) 218 + } 219 + 220 + private emitWorkerMessage = (response: WorkerResponse | { m: string; __vitest_worker_response__: false }): void => { 221 + try { 222 + const message = this.worker.deserialize(response) as WorkerResponse 223 + 224 + if (typeof message === 'object' && message != null && message.__vitest_worker_response__) { 225 + this._eventEmitter.emit('message', message) 226 + } 227 + else { 228 + this._eventEmitter.emit('rpc', message) 229 + } 230 + } 231 + catch (error) { 232 + this._eventEmitter.emit('error', error as Error) 233 + } 234 + } 235 + 236 + private emitUnexpectedExit = (): void => { 237 + const error = new Error('Worker exited unexpectedly') 238 + 239 + this._eventEmitter.emit('error', error) 240 + } 241 + 242 + private waitForStart() { 243 + return new Promise<void>((resolve) => { 244 + const onStart = (message: WorkerResponse) => { 245 + if (message.type === 'started') { 246 + this.off('message', onStart) 247 + resolve() 248 + } 249 + } 250 + 251 + this.on('message', onStart) 252 + }) 253 + } 254 + 255 + private withTimeout(promise: Promise<unknown>, timeout: number) { 256 + return new Promise<unknown>((resolve_, reject_) => { 257 + const timer = setTimeout( 258 + () => reject(new Error('[vitest-pool-runner]: Timeout waiting for worker to respond')), 259 + timeout, 260 + ) 261 + 262 + function resolve(value: unknown) { 263 + clearTimeout(timer) 264 + resolve_(value) 265 + } 266 + function reject(error: Error) { 267 + clearTimeout(timer) 268 + reject_(error) 269 + } 270 + 271 + promise.then(resolve, reject) 272 + }) 273 + } 274 + }
-292
packages/vitest/src/node/pools/threads.ts
··· 1 - import type { FileSpecification } from '@vitest/runner/types/runner' 2 - import type { Options as TinypoolOptions } from 'tinypool' 3 - import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 - import type { ContextTestEnvironment } from '../../types/worker' 5 - import type { Vitest } from '../core' 6 - import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' 7 - import type { TestProject } from '../project' 8 - import type { TestSpecification } from '../spec' 9 - import type { SerializedConfig } from '../types/config' 10 - import type { WorkerContext } from '../types/worker' 11 - import * as nodeos from 'node:os' 12 - import { resolve } from 'node:path' 13 - import { MessageChannel } from 'node:worker_threads' 14 - import { createBirpc } from 'birpc' 15 - import Tinypool from 'tinypool' 16 - import { groupBy } from '../../utils/base' 17 - import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' 18 - import { createMethodsRPC } from './rpc' 19 - 20 - function createWorkerChannel(project: TestProject, collect: boolean) { 21 - const channel = new MessageChannel() 22 - const port = channel.port2 23 - const workerPort = channel.port1 24 - 25 - const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { collect }), { 26 - eventNames: ['onCancel'], 27 - post(v) { 28 - port.postMessage(v) 29 - }, 30 - on(fn) { 31 - port.on('message', fn) 32 - }, 33 - timeout: -1, 34 - }) 35 - 36 - project.vitest.onCancel(reason => rpc.onCancel(reason)) 37 - 38 - const onClose = () => { 39 - port.close() 40 - workerPort.close() 41 - rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc')) 42 - } 43 - 44 - return { workerPort, port, onClose } 45 - } 46 - 47 - export function createThreadsPool( 48 - vitest: Vitest, 49 - { execArgv, env }: PoolProcessOptions, 50 - specifications: TestSpecification[], 51 - ): ProcessPool { 52 - const numCpus 53 - = typeof nodeos.availableParallelism === 'function' 54 - ? nodeos.availableParallelism() 55 - : nodeos.cpus().length 56 - 57 - const threadsCount = vitest.config.watch 58 - ? Math.max(Math.floor(numCpus / 2), 1) 59 - : Math.max(numCpus - 1, 1) 60 - 61 - const recommendedCount = vitest.config.watch 62 - ? threadsCount 63 - : Math.min(threadsCount, specifications.length) 64 - 65 - const poolOptions = vitest.config.poolOptions?.threads ?? {} 66 - 67 - const maxThreads 68 - = poolOptions.maxThreads ?? vitest.config.maxWorkers ?? recommendedCount 69 - const minThreads = vitest.config.watch 70 - ? Math.min(recommendedCount, maxThreads) 71 - // avoid recreating forks when tests are finished 72 - : 0 73 - 74 - const options: TinypoolOptions = { 75 - filename: resolve(vitest.distPath, 'worker-base.js'), 76 - teardown: 'teardown', 77 - // TODO: investigate further 78 - // It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191 79 - useAtomics: poolOptions.useAtomics ?? false, 80 - 81 - maxThreads, 82 - minThreads, 83 - 84 - env, 85 - execArgv: [...(poolOptions.execArgv ?? []), ...execArgv], 86 - 87 - terminateTimeout: vitest.config.teardownTimeout, 88 - concurrentTasksPerWorker: 1, 89 - } 90 - 91 - const isolated = poolOptions.isolate ?? true 92 - 93 - if (isolated) { 94 - options.isolateWorkers = true 95 - } 96 - 97 - if (poolOptions.singleThread || !vitest.config.fileParallelism) { 98 - options.maxThreads = 1 99 - options.minThreads = 1 100 - } 101 - 102 - const pool = new Tinypool(options) 103 - 104 - const runWithFiles = (name: string): RunWithFiles => { 105 - let id = 0 106 - 107 - async function runFiles( 108 - project: TestProject, 109 - config: SerializedConfig, 110 - files: FileSpecification[], 111 - environment: ContextTestEnvironment, 112 - invalidates: string[] = [], 113 - ) { 114 - const paths = files.map(f => f.filepath) 115 - vitest.state.clearFiles(project, paths) 116 - 117 - const { workerPort, onClose } = createWorkerChannel(project, name === 'collect') 118 - 119 - const workerId = ++id 120 - const data: WorkerContext = { 121 - pool: 'threads', 122 - port: workerPort, 123 - config, 124 - files, 125 - invalidates, 126 - environment, 127 - workerId, 128 - projectName: project.name, 129 - providedContext: project.getProvidedContext(), 130 - } 131 - try { 132 - await pool.run(data, { transferList: [workerPort], name, channel: { onClose } }) 133 - } 134 - catch (error) { 135 - // Worker got stuck and won't terminate - this may cause process to hang 136 - if ( 137 - error instanceof Error 138 - && /Failed to terminate worker/.test(error.message) 139 - ) { 140 - vitest.state.addProcessTimeoutCause( 141 - `Failed to terminate worker while running ${paths.join( 142 - ', ', 143 - )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, 144 - ) 145 - } 146 - // Intentionally cancelled 147 - else if ( 148 - vitest.isCancelling 149 - && error instanceof Error 150 - && /The task has been cancelled/.test(error.message) 151 - ) { 152 - vitest.state.cancelFiles(paths, project) 153 - } 154 - else { 155 - throw error 156 - } 157 - } 158 - } 159 - 160 - return async (specs, invalidates) => { 161 - // Cancel pending tasks from pool when possible 162 - vitest.onCancel(() => pool.cancelPendingTasks()) 163 - 164 - const configs = new WeakMap<TestProject, SerializedConfig>() 165 - const getConfig = (project: TestProject): SerializedConfig => { 166 - if (configs.has(project)) { 167 - return configs.get(project)! 168 - } 169 - 170 - const config = project.serializedConfig 171 - configs.set(project, config) 172 - return config 173 - } 174 - 175 - const singleThreads = specs.filter( 176 - spec => spec.project.config.poolOptions?.threads?.singleThread, 177 - ) 178 - const multipleThreads = specs.filter( 179 - spec => !spec.project.config.poolOptions?.threads?.singleThread, 180 - ) 181 - 182 - if (multipleThreads.length) { 183 - const filesByEnv = await groupFilesByEnv(multipleThreads) 184 - const files = Object.values(filesByEnv).flat() 185 - const results: PromiseSettledResult<void>[] = [] 186 - 187 - if (isolated) { 188 - results.push( 189 - ...(await Promise.allSettled( 190 - files.map(({ file, environment, project }) => 191 - runFiles( 192 - project, 193 - getConfig(project), 194 - [file], 195 - environment, 196 - invalidates, 197 - ), 198 - ), 199 - )), 200 - ) 201 - } 202 - else { 203 - // When isolation is disabled, we still need to isolate environments and workspace projects from each other. 204 - // Tasks are still running parallel but environments are isolated between tasks. 205 - const grouped = groupBy( 206 - files, 207 - ({ project, environment }) => 208 - project.name 209 - + environment.name 210 - + JSON.stringify(environment.options), 211 - ) 212 - 213 - for (const group of Object.values(grouped)) { 214 - // Push all files to pool's queue 215 - results.push( 216 - ...(await Promise.allSettled( 217 - group.map(({ file, environment, project }) => 218 - runFiles( 219 - project, 220 - getConfig(project), 221 - [file], 222 - environment, 223 - invalidates, 224 - ), 225 - ), 226 - )), 227 - ) 228 - 229 - // Once all tasks are running or finished, recycle worker for isolation. 230 - // On-going workers will run in the previous environment. 231 - await new Promise<void>(resolve => 232 - pool.queueSize === 0 ? resolve() : pool.once('drain', resolve), 233 - ) 234 - await pool.recycleWorkers() 235 - } 236 - } 237 - 238 - const errors = results 239 - .filter((r): r is PromiseRejectedResult => r.status === 'rejected') 240 - .map(r => r.reason) 241 - if (errors.length > 0) { 242 - throw new AggregateError( 243 - errors, 244 - 'Errors occurred while running tests. For more information, see serialized error.', 245 - ) 246 - } 247 - } 248 - 249 - if (singleThreads.length) { 250 - const filesByEnv = await groupFilesByEnv(singleThreads) 251 - const envs = envsOrder.concat( 252 - Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)), 253 - ) 254 - 255 - for (const env of envs) { 256 - const files = filesByEnv[env] 257 - 258 - if (!files?.length) { 259 - continue 260 - } 261 - 262 - const filesByOptions = groupBy( 263 - files, 264 - ({ project, environment }) => 265 - project.name + JSON.stringify(environment.options), 266 - ) 267 - 268 - for (const files of Object.values(filesByOptions)) { 269 - // Always run environments isolated between each other 270 - await pool.recycleWorkers() 271 - 272 - const filenames = files.map(f => f.file) 273 - await runFiles( 274 - files[0].project, 275 - getConfig(files[0].project), 276 - filenames, 277 - files[0].environment, 278 - invalidates, 279 - ) 280 - } 281 - } 282 - } 283 - } 284 - } 285 - 286 - return { 287 - name: 'threads', 288 - runTests: runWithFiles('run'), 289 - collectTests: runWithFiles('collect'), 290 - close: () => pool.destroy(), 291 - } 292 - }
-183
packages/vitest/src/node/pools/typecheck.ts
··· 1 - import type { DeferPromise } from '@vitest/utils' 2 - import type { TypecheckResults } from '../../typecheck/typechecker' 3 - import type { Vitest } from '../core' 4 - import type { ProcessPool } from '../pool' 5 - import type { TestProject } from '../project' 6 - import type { TestSpecification } from '../spec' 7 - import type { TestRunEndReason } from '../types/reporter' 8 - import { hasFailed } from '@vitest/runner/utils' 9 - import { createDefer } from '@vitest/utils/helpers' 10 - import { Typechecker } from '../../typecheck/typechecker' 11 - import { groupBy } from '../../utils/base' 12 - 13 - export function createTypecheckPool(vitest: Vitest): ProcessPool { 14 - const promisesMap = new WeakMap<TestProject, DeferPromise<void>>() 15 - const rerunTriggered = new WeakSet<TestProject>() 16 - 17 - async function onParseEnd( 18 - project: TestProject, 19 - { files, sourceErrors }: TypecheckResults, 20 - ) { 21 - const checker = project.typechecker! 22 - 23 - const { packs, events } = checker.getTestPacksAndEvents() 24 - await vitest._testRun.updated(packs, events) 25 - 26 - if (!project.config.typecheck.ignoreSourceErrors) { 27 - sourceErrors.forEach(error => 28 - vitest.state.catchError(error, 'Unhandled Source Error'), 29 - ) 30 - } 31 - 32 - const processError 33 - = !hasFailed(files) && !sourceErrors.length && checker.getExitCode() 34 - if (processError) { 35 - const error = new Error(checker.getOutput()) 36 - error.stack = '' 37 - vitest.state.catchError(error, 'Typecheck Error') 38 - } 39 - 40 - promisesMap.get(project)?.resolve() 41 - 42 - rerunTriggered.delete(project) 43 - 44 - // triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case 45 - if (vitest.config.watch && !vitest.runningPromise) { 46 - const modules = files.map(file => vitest.state.getReportedEntity(file)).filter(e => e?.type === 'module') 47 - 48 - const state: TestRunEndReason = vitest.isCancelling 49 - ? 'interrupted' 50 - : modules.some(m => !m.ok()) 51 - ? 'failed' 52 - : 'passed' 53 - 54 - await vitest.report('onTestRunEnd', modules, [], state) 55 - await vitest.report('onWatcherStart', files, [ 56 - ...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), 57 - ...vitest.state.getUnhandledErrors(), 58 - ]) 59 - } 60 - } 61 - 62 - async function createWorkspaceTypechecker( 63 - project: TestProject, 64 - files: string[], 65 - ) { 66 - const checker = project.typechecker ?? new Typechecker(project) 67 - if (project.typechecker) { 68 - return checker 69 - } 70 - 71 - project.typechecker = checker 72 - checker.setFiles(files) 73 - 74 - checker.onParseStart(async () => { 75 - const files = checker.getTestFiles() 76 - for (const file of files) { 77 - await vitest._testRun.enqueued(project, file) 78 - } 79 - await vitest._testRun.collected(project, files) 80 - }) 81 - 82 - checker.onParseEnd(result => onParseEnd(project, result)) 83 - 84 - checker.onWatcherRerun(async () => { 85 - rerunTriggered.add(project) 86 - 87 - if (!vitest.runningPromise) { 88 - vitest.state.clearErrors() 89 - await vitest.report( 90 - 'onWatcherRerun', 91 - files, 92 - 'File change detected. Triggering rerun.', 93 - ) 94 - } 95 - 96 - await checker.collectTests() 97 - 98 - const testFiles = checker.getTestFiles() 99 - for (const file of testFiles) { 100 - await vitest._testRun.enqueued(project, file) 101 - } 102 - await vitest._testRun.collected(project, testFiles) 103 - 104 - const { packs, events } = checker.getTestPacksAndEvents() 105 - await vitest._testRun.updated(packs, events) 106 - }) 107 - 108 - return checker 109 - } 110 - 111 - async function startTypechecker(project: TestProject, files: string[]) { 112 - if (project.typechecker) { 113 - return 114 - } 115 - const checker = await createWorkspaceTypechecker(project, files) 116 - await checker.collectTests() 117 - await checker.start() 118 - } 119 - 120 - async function collectTests(specs: TestSpecification[]) { 121 - const specsByProject = groupBy(specs, spec => spec.project.name) 122 - for (const name in specsByProject) { 123 - const project = specsByProject[name][0].project 124 - const files = specsByProject[name].map(spec => spec.moduleId) 125 - const checker = await createWorkspaceTypechecker(project, files) 126 - checker.setFiles(files) 127 - await checker.collectTests() 128 - const testFiles = checker.getTestFiles() 129 - vitest.state.collectFiles(project, testFiles) 130 - } 131 - } 132 - 133 - async function runTests(specs: TestSpecification[]) { 134 - const specsByProject = groupBy(specs, spec => spec.project.name) 135 - const promises: Promise<void>[] = [] 136 - 137 - for (const name in specsByProject) { 138 - const project = specsByProject[name][0].project 139 - const files = specsByProject[name].map(spec => spec.moduleId) 140 - const promise = createDefer<void>() 141 - // check that watcher actually triggered rerun 142 - const _p = new Promise<boolean>((resolve) => { 143 - const _i = setInterval(() => { 144 - if (!project.typechecker || rerunTriggered.has(project)) { 145 - resolve(true) 146 - clearInterval(_i) 147 - } 148 - }) 149 - setTimeout(() => { 150 - resolve(false) 151 - clearInterval(_i) 152 - }, 500).unref() 153 - }) 154 - const triggered = await _p 155 - if (project.typechecker && !triggered) { 156 - const testFiles = project.typechecker.getTestFiles() 157 - for (const file of testFiles) { 158 - await vitest._testRun.enqueued(project, file) 159 - } 160 - await vitest._testRun.collected(project, testFiles) 161 - await onParseEnd(project, project.typechecker.getResult()) 162 - continue 163 - } 164 - promises.push(promise) 165 - promisesMap.set(project, promise) 166 - promises.push(startTypechecker(project, files)) 167 - } 168 - 169 - await Promise.all(promises) 170 - } 171 - 172 - return { 173 - name: 'typescript', 174 - runTests, 175 - collectTests, 176 - async close() { 177 - const promises = vitest.projects.map(project => 178 - project.typechecker?.stop(), 179 - ) 180 - await Promise.all(promises) 181 - }, 182 - } 183 - }
+59
packages/vitest/src/node/pools/types.ts
··· 1 + import type { ContextRPC } from '../../types/worker' 2 + import type { TestProject } from '../project' 3 + 4 + export interface PoolRunnerInitializer { 5 + readonly name: string 6 + createPoolWorker: (options: PoolOptions) => PoolWorker 7 + } 8 + 9 + export interface PoolOptions { 10 + distPath: string 11 + project: TestProject 12 + method: 'run' | 'collect' 13 + cacheFs?: boolean 14 + environment: string 15 + execArgv: string[] 16 + env: Record<string, string> 17 + } 18 + 19 + export interface PoolWorker { 20 + readonly name: string 21 + readonly execArgv: string[] 22 + readonly env: Record<string, string> 23 + readonly reportMemory?: boolean 24 + readonly cacheFs?: boolean 25 + 26 + on: (event: string, callback: (arg: any) => void) => void 27 + off: (event: string, callback: (arg: any) => void) => void 28 + send: (message: WorkerRequest) => void 29 + deserialize: (data: unknown) => unknown 30 + 31 + start: () => Promise<void> 32 + stop: () => Promise<void> 33 + } 34 + 35 + export interface PoolTask { 36 + worker: 'forks' | 'threads' | 'vmForks' | 'vmThreads' | (string & {}) 37 + project: TestProject 38 + isolate: boolean 39 + env: Record<string, string> 40 + execArgv: string[] 41 + context: ContextRPC 42 + memoryLimit: number | null 43 + } 44 + 45 + export type WorkerRequest 46 + = { __vitest_worker_request__: true } & ( 47 + | { type: 'start'; options: { reportMemory: boolean } } 48 + | { type: 'stop' } 49 + | { type: 'run'; context: ContextRPC; poolId: number } 50 + | { type: 'collect'; context: ContextRPC; poolId: number } 51 + | { type: 'cancel' } 52 + ) 53 + 54 + export type WorkerResponse 55 + = { __vitest_worker_response__: true } & ( 56 + | { type: 'started' } 57 + | { type: 'stopped'; error?: unknown } 58 + | { type: 'testfileFinished'; usedMemory?: number; error?: unknown } 59 + )
-247
packages/vitest/src/node/pools/vmForks.ts
··· 1 - import type { FileSpecification } from '@vitest/runner' 2 - import type { TinypoolChannel, Options as TinypoolOptions } from 'tinypool' 3 - import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 - import type { ContextRPC, ContextTestEnvironment } from '../../types/worker' 5 - import type { Vitest } from '../core' 6 - import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' 7 - import type { TestProject } from '../project' 8 - import type { TestSpecification } from '../spec' 9 - import type { ResolvedConfig, SerializedConfig } from '../types/config' 10 - import EventEmitter from 'node:events' 11 - import * as nodeos from 'node:os' 12 - import { resolve } from 'node:path' 13 - import v8 from 'node:v8' 14 - import { createBirpc } from 'birpc' 15 - import Tinypool from 'tinypool' 16 - import { wrapSerializableConfig } from '../../utils/config-helpers' 17 - import { getWorkerMemoryLimit, stringToBytes } from '../../utils/memory-limit' 18 - import { groupFilesByEnv } from '../../utils/test-helpers' 19 - import { createMethodsRPC } from './rpc' 20 - 21 - function createChildProcessChannel(project: TestProject, collect: boolean) { 22 - const emitter = new EventEmitter() 23 - 24 - const events = { message: 'message', response: 'response' } 25 - 26 - const rpc = createBirpc<RunnerRPC, RuntimeRPC>( 27 - createMethodsRPC(project, { cacheFs: true, collect }), 28 - { 29 - eventNames: ['onCancel'], 30 - serialize: v8.serialize, 31 - deserialize: (v) => { 32 - try { 33 - return v8.deserialize(Buffer.from(v)) 34 - } 35 - catch (error) { 36 - let stringified = '' 37 - 38 - try { 39 - stringified = `\nReceived value: ${JSON.stringify(v)}` 40 - } 41 - catch {} 42 - 43 - throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error }) 44 - } 45 - }, 46 - post(v) { 47 - emitter.emit(events.message, v) 48 - }, 49 - on(fn) { 50 - emitter.on(events.response, fn) 51 - }, 52 - timeout: -1, 53 - }, 54 - ) 55 - 56 - project.vitest.onCancel(reason => rpc.onCancel(reason)) 57 - 58 - const channel = { 59 - onMessage: callback => emitter.on(events.message, callback), 60 - postMessage: message => emitter.emit(events.response, message), 61 - onClose: () => { 62 - emitter.removeAllListeners() 63 - rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc')) 64 - }, 65 - } satisfies TinypoolChannel 66 - 67 - return { channel } 68 - } 69 - 70 - export function createVmForksPool( 71 - vitest: Vitest, 72 - { execArgv, env }: PoolProcessOptions, 73 - specifications: TestSpecification[], 74 - ): ProcessPool { 75 - const numCpus 76 - = typeof nodeos.availableParallelism === 'function' 77 - ? nodeos.availableParallelism() 78 - : nodeos.cpus().length 79 - 80 - const threadsCount = vitest.config.watch 81 - ? Math.max(Math.floor(numCpus / 2), 1) 82 - : Math.max(numCpus - 1, 1) 83 - 84 - const recommendedCount = vitest.config.watch 85 - ? threadsCount 86 - : Math.min(threadsCount, specifications.length) 87 - 88 - const poolOptions = vitest.config.poolOptions?.vmForks ?? {} 89 - 90 - const maxThreads 91 - = poolOptions.maxForks ?? vitest.config.maxWorkers ?? recommendedCount 92 - const minThreads = vitest.config.watch 93 - ? Math.min(recommendedCount, maxThreads) 94 - // avoid recreating forks when tests are finished 95 - : 0 96 - 97 - const options: TinypoolOptions = { 98 - runtime: 'child_process', 99 - filename: resolve(vitest.distPath, 'worker-vm.js'), 100 - 101 - maxThreads, 102 - minThreads, 103 - 104 - env, 105 - execArgv: [ 106 - '--experimental-vm-modules', 107 - ...(poolOptions.execArgv ?? []), 108 - ...execArgv, 109 - ], 110 - 111 - terminateTimeout: vitest.config.teardownTimeout, 112 - concurrentTasksPerWorker: 1, 113 - maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined, 114 - } 115 - 116 - if (poolOptions.singleFork || !vitest.config.fileParallelism) { 117 - options.maxThreads = 1 118 - options.minThreads = 1 119 - } 120 - 121 - const pool = new Tinypool(options) 122 - 123 - const runWithFiles = (name: string): RunWithFiles => { 124 - let id = 0 125 - 126 - async function runFiles( 127 - project: TestProject, 128 - config: SerializedConfig, 129 - files: FileSpecification[], 130 - environment: ContextTestEnvironment, 131 - invalidates: string[] = [], 132 - ) { 133 - const paths = files.map(f => f.filepath) 134 - vitest.state.clearFiles(project, paths) 135 - 136 - const { channel } = createChildProcessChannel(project, name === 'collect') 137 - const workerId = ++id 138 - const data: ContextRPC = { 139 - pool: 'vmForks', 140 - config, 141 - files, 142 - invalidates, 143 - environment, 144 - workerId, 145 - projectName: project.name, 146 - providedContext: project.getProvidedContext(), 147 - } 148 - try { 149 - await pool.run(data, { name, channel }) 150 - } 151 - catch (error) { 152 - // Worker got stuck and won't terminate - this may cause process to hang 153 - if ( 154 - error instanceof Error 155 - && /Failed to terminate worker/.test(error.message) 156 - ) { 157 - vitest.state.addProcessTimeoutCause( 158 - `Failed to terminate worker while running ${paths.join(', ')}.`, 159 - ) 160 - } 161 - // Intentionally cancelled 162 - else if ( 163 - vitest.isCancelling 164 - && error instanceof Error 165 - && /The task has been cancelled/.test(error.message) 166 - ) { 167 - vitest.state.cancelFiles(paths, project) 168 - } 169 - else { 170 - throw error 171 - } 172 - } 173 - finally { 174 - channel.onClose() 175 - } 176 - } 177 - 178 - return async (specs, invalidates) => { 179 - // Cancel pending tasks from pool when possible 180 - vitest.onCancel(() => pool.cancelPendingTasks()) 181 - 182 - const configs = new Map<TestProject, SerializedConfig>() 183 - const getConfig = (project: TestProject): SerializedConfig => { 184 - if (configs.has(project)) { 185 - return configs.get(project)! 186 - } 187 - 188 - const _config = project.serializedConfig 189 - const config = wrapSerializableConfig(_config) 190 - 191 - configs.set(project, config) 192 - return config 193 - } 194 - 195 - const filesByEnv = await groupFilesByEnv(specs) 196 - const promises = Object.values(filesByEnv).flat() 197 - const results = await Promise.allSettled( 198 - promises.map(({ file, environment, project }) => 199 - runFiles( 200 - project, 201 - getConfig(project), 202 - [file], 203 - environment, 204 - invalidates, 205 - ), 206 - ), 207 - ) 208 - 209 - const errors = results 210 - .filter((r): r is PromiseRejectedResult => r.status === 'rejected') 211 - .map(r => r.reason) 212 - if (errors.length > 0) { 213 - throw new AggregateError( 214 - errors, 215 - 'Errors occurred while running tests. For more information, see serialized error.', 216 - ) 217 - } 218 - } 219 - } 220 - 221 - return { 222 - name: 'vmForks', 223 - runTests: runWithFiles('run'), 224 - collectTests: runWithFiles('collect'), 225 - close: () => pool.destroy(), 226 - } 227 - } 228 - 229 - function getMemoryLimit(config: ResolvedConfig) { 230 - const memory = nodeos.totalmem() 231 - const limit = getWorkerMemoryLimit(config, 'vmForks') 232 - 233 - if (typeof memory === 'number') { 234 - return stringToBytes(limit, config.watch ? memory / 2 : memory) 235 - } 236 - 237 - // If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%" 238 - if ( 239 - (typeof limit === 'number' && limit > 1) 240 - || (typeof limit === 'string' && limit.at(-1) !== '%') 241 - ) { 242 - return stringToBytes(limit) 243 - } 244 - 245 - // just ignore "memoryLimit" value because we cannot detect memory limit 246 - return null 247 - }
-227
packages/vitest/src/node/pools/vmThreads.ts
··· 1 - import type { FileSpecification } from '@vitest/runner' 2 - import type { Options as TinypoolOptions } from 'tinypool' 3 - import type { RunnerRPC, RuntimeRPC } from '../../types/rpc' 4 - import type { ContextTestEnvironment } from '../../types/worker' 5 - import type { Vitest } from '../core' 6 - import type { PoolProcessOptions, ProcessPool, RunWithFiles } from '../pool' 7 - import type { TestProject } from '../project' 8 - import type { TestSpecification } from '../spec' 9 - import type { ResolvedConfig, SerializedConfig } from '../types/config' 10 - import type { WorkerContext } from '../types/worker' 11 - import * as nodeos from 'node:os' 12 - import { resolve } from 'node:path' 13 - import { MessageChannel } from 'node:worker_threads' 14 - import { createBirpc } from 'birpc' 15 - import Tinypool from 'tinypool' 16 - import { getWorkerMemoryLimit, stringToBytes } from '../../utils/memory-limit' 17 - import { groupFilesByEnv } from '../../utils/test-helpers' 18 - import { createMethodsRPC } from './rpc' 19 - 20 - function createWorkerChannel(project: TestProject, collect: boolean) { 21 - const channel = new MessageChannel() 22 - const port = channel.port2 23 - const workerPort = channel.port1 24 - 25 - const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { collect }), { 26 - eventNames: ['onCancel'], 27 - post(v) { 28 - port.postMessage(v) 29 - }, 30 - on(fn) { 31 - port.on('message', fn) 32 - }, 33 - timeout: -1, 34 - }) 35 - 36 - project.vitest.onCancel(reason => rpc.onCancel(reason)) 37 - 38 - function onClose() { 39 - workerPort.close() 40 - port.close() 41 - rpc.$close(new Error('[vitest-pool]: Pending methods while closing rpc')) 42 - } 43 - 44 - return { workerPort, onClose } 45 - } 46 - 47 - export function createVmThreadsPool( 48 - vitest: Vitest, 49 - { execArgv, env }: PoolProcessOptions, 50 - specifications: TestSpecification[], 51 - ): ProcessPool { 52 - const numCpus 53 - = typeof nodeos.availableParallelism === 'function' 54 - ? nodeos.availableParallelism() 55 - : nodeos.cpus().length 56 - 57 - const threadsCount = vitest.config.watch 58 - ? Math.max(Math.floor(numCpus / 2), 1) 59 - : Math.max(numCpus - 1, 1) 60 - 61 - const recommendedCount = vitest.config.watch 62 - ? threadsCount 63 - : Math.min(threadsCount, specifications.length) 64 - 65 - const poolOptions = vitest.config.poolOptions?.vmThreads ?? {} 66 - 67 - const maxThreads 68 - = poolOptions.maxThreads ?? vitest.config.maxWorkers ?? recommendedCount 69 - const minThreads = vitest.config.watch 70 - ? Math.min(recommendedCount, maxThreads) 71 - // avoid recreating threads when tests are finished 72 - : 0 73 - 74 - const options: TinypoolOptions = { 75 - filename: resolve(vitest.distPath, 'worker-vm.js'), 76 - // TODO: investigate further 77 - // It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191 78 - useAtomics: poolOptions.useAtomics ?? false, 79 - 80 - maxThreads, 81 - minThreads, 82 - 83 - env, 84 - execArgv: [ 85 - '--experimental-vm-modules', 86 - ...(poolOptions.execArgv ?? []), 87 - ...execArgv, 88 - ], 89 - 90 - terminateTimeout: vitest.config.teardownTimeout, 91 - concurrentTasksPerWorker: 1, 92 - maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined, 93 - } 94 - 95 - if (poolOptions.singleThread || !vitest.config.fileParallelism) { 96 - options.maxThreads = 1 97 - options.minThreads = 1 98 - } 99 - 100 - const pool = new Tinypool(options) 101 - 102 - const runWithFiles = (name: string): RunWithFiles => { 103 - let id = 0 104 - 105 - async function runFiles( 106 - project: TestProject, 107 - config: SerializedConfig, 108 - files: FileSpecification[], 109 - environment: ContextTestEnvironment, 110 - invalidates: string[] = [], 111 - ) { 112 - const paths = files.map(f => f.filepath) 113 - vitest.state.clearFiles(project, paths) 114 - 115 - const { workerPort, onClose } = createWorkerChannel(project, name === 'collect') 116 - const workerId = ++id 117 - const data: WorkerContext = { 118 - pool: 'vmThreads', 119 - port: workerPort, 120 - config, 121 - files: paths, 122 - invalidates, 123 - environment, 124 - workerId, 125 - projectName: project.name, 126 - providedContext: project.getProvidedContext(), 127 - } 128 - try { 129 - await pool.run(data, { transferList: [workerPort], name }) 130 - } 131 - catch (error) { 132 - // Worker got stuck and won't terminate - this may cause process to hang 133 - if ( 134 - error instanceof Error 135 - && /Failed to terminate worker/.test(error.message) 136 - ) { 137 - vitest.state.addProcessTimeoutCause( 138 - `Failed to terminate worker while running ${paths.join( 139 - ', ', 140 - )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, 141 - ) 142 - } 143 - // Intentionally cancelled 144 - else if ( 145 - vitest.isCancelling 146 - && error instanceof Error 147 - && /The task has been cancelled/.test(error.message) 148 - ) { 149 - vitest.state.cancelFiles(paths, project) 150 - } 151 - else { 152 - throw error 153 - } 154 - } 155 - finally { 156 - onClose() 157 - } 158 - } 159 - 160 - return async (specs, invalidates) => { 161 - // Cancel pending tasks from pool when possible 162 - vitest.onCancel(() => pool.cancelPendingTasks()) 163 - 164 - const configs = new Map<TestProject, SerializedConfig>() 165 - const getConfig = (project: TestProject): SerializedConfig => { 166 - if (configs.has(project)) { 167 - return configs.get(project)! 168 - } 169 - 170 - const config = project.serializedConfig 171 - configs.set(project, config) 172 - return config 173 - } 174 - 175 - const filesByEnv = await groupFilesByEnv(specs) 176 - const promises = Object.values(filesByEnv).flat() 177 - const results = await Promise.allSettled( 178 - promises.map(({ file, environment, project }) => 179 - runFiles( 180 - project, 181 - getConfig(project), 182 - [file], 183 - environment, 184 - invalidates, 185 - ), 186 - ), 187 - ) 188 - 189 - const errors = results 190 - .filter((r): r is PromiseRejectedResult => r.status === 'rejected') 191 - .map(r => r.reason) 192 - if (errors.length > 0) { 193 - throw new AggregateError( 194 - errors, 195 - 'Errors occurred while running tests. For more information, see serialized error.', 196 - ) 197 - } 198 - } 199 - } 200 - 201 - return { 202 - name: 'vmThreads', 203 - runTests: runWithFiles('run'), 204 - collectTests: runWithFiles('collect'), 205 - close: () => pool.destroy(), 206 - } 207 - } 208 - 209 - function getMemoryLimit(config: ResolvedConfig) { 210 - const memory = nodeos.totalmem() 211 - const limit = getWorkerMemoryLimit(config, 'vmThreads') 212 - 213 - if (typeof memory === 'number') { 214 - return stringToBytes(limit, config.watch ? memory / 2 : memory) 215 - } 216 - 217 - // If totalmem is not supported we cannot resolve percentage based values like 0.5, "50%" 218 - if ( 219 - (typeof limit === 'number' && limit > 1) 220 - || (typeof limit === 'string' && limit.at(-1) !== '%') 221 - ) { 222 - return stringToBytes(limit) 223 - } 224 - 225 - // just ignore "memoryLimit" value because we cannot detect memory limit 226 - return null 227 - }
-1
packages/vitest/src/node/types/browser.ts
··· 65 65 | 'sequence' 66 66 | 'root' 67 67 | 'pool' 68 - | 'poolOptions' 69 68 // browser mode doesn't support a custom runner 70 69 | 'runner' 71 70 // non-browser options
+41 -27
packages/vitest/src/node/types/config.ts
··· 10 10 import type { LabelColor, ParsedStack, ProvidedContext, TestError } from '../../types/general' 11 11 import type { HappyDOMOptions } from '../../types/happy-dom-options' 12 12 import type { JSDOMOptions } from '../../types/jsdom-options' 13 + import type { PoolRunnerInitializer } from '../pools/types' 13 14 import type { 14 15 BuiltinReporterOptions, 15 16 BuiltinReporters, ··· 20 21 import type { BenchmarkUserOptions } from './benchmark' 21 22 import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser' 22 23 import type { CoverageOptions, ResolvedCoverageOptions } from './coverage' 23 - import type { Pool, PoolOptions, ResolvedPoolOptions } from './pool-options' 24 24 import type { Reporter } from './reporter' 25 25 26 26 export type { CoverageOptions, ResolvedCoverageOptions } ··· 39 39 export type VitestEnvironment 40 40 = | BuiltinEnvironment 41 41 | (string & Record<never, never>) 42 - export type { Pool, PoolOptions } 43 42 export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped' 44 43 45 44 export type ApiConfig = Pick< ··· 205 204 context: ResolveSnapshotPathHandlerContext 206 205 ) => string 207 206 207 + export type BuiltinPool 208 + = | 'browser' 209 + | 'threads' 210 + | 'forks' 211 + | 'vmThreads' 212 + | 'vmForks' 213 + | 'typescript' 214 + 215 + export type Pool = BuiltinPool | (string & {}) 216 + 208 217 export interface InlineConfig { 209 218 /** 210 219 * Name of the project. Will be used to display in the reporter. ··· 298 307 /** 299 308 * Run tests in an isolated environment. This option has no effect on vmThreads pool. 300 309 * 301 - * Disabling this option might improve performance if your code doesn't rely on side effects. 310 + * Disabling this option improves performance if your code doesn't rely on side effects. 302 311 * 303 312 * @default true 304 313 */ 305 314 isolate?: boolean 306 315 307 316 /** 317 + * Pass additional arguments to `node` process when spawning the worker. 318 + * 319 + * See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 320 + * 321 + * Set to `process.execArgv` to pass all arguments of the current process. 322 + * 323 + * Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103 324 + * 325 + * @default [] // no execution arguments are passed 326 + */ 327 + execArgv?: string[] 328 + 329 + /** 330 + * Specifies the memory limit for `worker_thread` or `child_process` before they are recycled. 331 + * If you see memory leaks, try to tinker this value. 332 + */ 333 + vmMemoryLimit?: string | number 334 + 335 + /** 308 336 * Pool used to run tests in. 309 337 * 310 - * Supports 'threads', 'forks', 'vmThreads' 338 + * Supports 'threads', 'forks', 'vmThreads', 'vmForks' 311 339 * 312 340 * @default 'forks' 313 341 */ 314 - pool?: Exclude<Pool, 'browser'> 342 + pool?: Exclude<Pool, 'browser'> | PoolRunnerInitializer 315 343 316 344 /** 317 - * Pool options 318 - */ 319 - poolOptions?: PoolOptions 320 - 321 - /** 322 - * Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. 345 + * Maximum number or percentage of workers to run tests in. 323 346 */ 324 347 maxWorkers?: number | string 325 348 ··· 667 690 * Debug tests by opening `node:inspector` in worker / child process. 668 691 * Provides similar experience as `--inspect` Node CLI argument. 669 692 * 670 - * Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`. 693 + * Requires `fileParallelism: false`. 671 694 */ 672 695 inspect?: boolean | string 673 696 ··· 675 698 * Debug tests by opening `node:inspector` in worker / child process and wait for debugger to connect. 676 699 * Provides similar experience as `--inspect-brk` Node CLI argument. 677 700 * 678 - * Requires `poolOptions.threads.singleThread: true` OR `poolOptions.forks.singleFork: true`. 701 + * Requires `fileParallelism: false`. 679 702 */ 680 703 inspectBrk?: boolean | string 681 704 ··· 953 976 | 'sequence' 954 977 | 'typecheck' 955 978 | 'runner' 956 - | 'poolOptions' 957 979 | 'pool' 958 980 | 'cliExclude' 959 981 | 'diff' ··· 961 983 | 'snapshotEnvironment' 962 984 | 'bail' 963 985 | 'name' 986 + | 'vmMemoryLimit' 964 987 > { 965 988 mode: VitestRunMode 966 989 ··· 983 1006 984 1007 browser: ResolvedBrowserOptions 985 1008 pool: Pool 986 - poolOptions?: ResolvedPoolOptions 1009 + poolRunner?: PoolRunnerInitializer 987 1010 988 1011 reporters: (InlineReporter | ReporterWithOptions)[] 989 1012 ··· 1028 1051 1029 1052 maxWorkers: number 1030 1053 1054 + vmMemoryLimit?: UserConfig['vmMemoryLimit'] 1031 1055 dumpDir?: string 1032 1056 } 1033 1057 ··· 1046 1070 | 'ui' 1047 1071 | 'open' 1048 1072 | 'uiBase' 1049 - // TODO: allow snapshot options 1073 + // TODO: allow snapshot options 1050 1074 | 'snapshotFormat' 1051 1075 | 'resolveSnapshotPath' 1052 1076 | 'passWithNoTests' ··· 1057 1081 | 'inspect' 1058 1082 | 'inspectBrk' 1059 1083 | 'coverage' 1060 - | 'maxWorkers' 1061 - | 'fileParallelism' 1062 1084 | 'watchTriggerPatterns' 1063 1085 1064 1086 export interface ServerDepsOptions { ··· 1091 1113 NonProjectOptions 1092 1114 | 'sequencer' 1093 1115 | 'deps' 1094 - | 'poolOptions' 1095 1116 > & { 1096 1117 mode?: string 1097 1118 sequencer?: Omit<SequenceOptions, 'sequencer' | 'seed'> 1098 1119 deps?: Omit<DepsOptions, 'moduleDirectories'> 1099 - poolOptions?: { 1100 - threads?: Pick< 1101 - NonNullable<PoolOptions['threads']>, 1102 - 'singleThread' | 'isolate' 1103 - > 1104 - vmThreads?: Pick<NonNullable<PoolOptions['vmThreads']>, 'singleThread'> 1105 - forks?: Pick<NonNullable<PoolOptions['forks']>, 'singleFork' | 'isolate'> 1106 - } 1120 + fileParallelism?: boolean 1107 1121 } 1108 1122 1109 1123 export type ResolvedProjectConfig = Omit<
-137
packages/vitest/src/node/types/pool-options.ts
··· 1 - export type BuiltinPool 2 - = | 'browser' 3 - | 'threads' 4 - | 'forks' 5 - | 'vmThreads' 6 - | 'vmForks' 7 - | 'typescript' 8 - export type Pool = BuiltinPool | (string & {}) 9 - 10 - export interface PoolOptions extends Record<string, unknown> { 11 - /** 12 - * Run tests in `node:worker_threads`. 13 - * 14 - * Test isolation (when enabled) is done by spawning a new thread for each test file. 15 - * 16 - * This pool is used by default. 17 - */ 18 - threads?: ThreadsOptions & WorkerContextOptions 19 - 20 - /** 21 - * Run tests in `node:child_process` using [`fork()`](https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options) 22 - * 23 - * Test isolation (when enabled) is done by spawning a new child process for each test file. 24 - */ 25 - forks?: ForksOptions & WorkerContextOptions 26 - 27 - /** 28 - * Run tests in isolated `node:vm`. 29 - * Test files are run parallel using `node:worker_threads`. 30 - * 31 - * This makes tests run faster, but VM module is unstable. Your tests might leak memory. 32 - */ 33 - vmThreads?: ThreadsOptions & VmOptions 34 - 35 - /** 36 - * Run tests in isolated `node:vm`. 37 - * 38 - * Test files are run parallel using `node:child_process` [`fork()`](https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options) 39 - * 40 - * This makes tests run faster, but VM module is unstable. Your tests might leak memory. 41 - */ 42 - vmForks?: ForksOptions & VmOptions 43 - } 44 - 45 - export interface ResolvedPoolOptions extends PoolOptions { 46 - threads?: ResolvedThreadsOptions & WorkerContextOptions 47 - forks?: ResolvedForksOptions & WorkerContextOptions 48 - vmThreads?: ResolvedThreadsOptions & VmOptions 49 - vmForks?: ResolvedForksOptions & VmOptions 50 - } 51 - 52 - export interface ThreadsOptions { 53 - /** Maximum amount of threads to use */ 54 - maxThreads?: number | string 55 - 56 - /** 57 - * Run tests inside a single thread. 58 - * 59 - * @default false 60 - */ 61 - singleThread?: boolean 62 - 63 - /** 64 - * Use Atomics to synchronize threads 65 - * 66 - * This can improve performance in some cases, but might cause segfault in older Node versions. 67 - * 68 - * @default false 69 - */ 70 - useAtomics?: boolean 71 - } 72 - 73 - export interface ResolvedThreadsOptions extends ThreadsOptions { 74 - maxThreads?: number 75 - } 76 - 77 - export interface ForksOptions { 78 - /** Maximum amount of child processes to use */ 79 - maxForks?: number | string 80 - 81 - /** 82 - * Run tests inside a single fork. 83 - * 84 - * @default false 85 - */ 86 - singleFork?: boolean 87 - } 88 - 89 - export interface ResolvedForksOptions extends ForksOptions { 90 - maxForks?: number 91 - } 92 - 93 - export interface WorkerContextOptions { 94 - /** 95 - * Isolate test environment by recycling `worker_threads` or `child_process` after each test 96 - * 97 - * @default true 98 - */ 99 - isolate?: boolean 100 - 101 - /** 102 - * Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`. 103 - * 104 - * See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 105 - * 106 - * Set to `process.execArgv` to pass all arguments of the current process. 107 - * 108 - * Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103 109 - * 110 - * @default [] // no execution arguments are passed 111 - */ 112 - execArgv?: string[] 113 - } 114 - 115 - export interface VmOptions { 116 - /** 117 - * Specifies the memory limit for `worker_thread` or `child_process` before they are recycled. 118 - * If you see memory leaks, try to tinker this value. 119 - */ 120 - memoryLimit?: string | number 121 - 122 - /** Isolation is always enabled */ 123 - isolate?: true 124 - 125 - /** 126 - * Pass additional arguments to `node` process when spawning `worker_threads` or `child_process`. 127 - * 128 - * See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information. 129 - * 130 - * Set to `process.execArgv` to pass all arguments of the current process. 131 - * 132 - * Be careful when using, it as some options may crash worker, e.g. --prof, --title. See https://github.com/nodejs/node/issues/41103 133 - * 134 - * @default [] // no execution arguments are passed 135 - */ 136 - execArgv?: string[] 137 - }
+1 -4
packages/vitest/src/node/types/worker.ts
··· 1 - import type { MessagePort } from 'node:worker_threads' 2 1 import type { ContextRPC } from '../../types/worker' 3 2 4 - export interface WorkerContext extends ContextRPC { 5 - port: MessagePort 6 - } 3 + export interface WorkerContext extends ContextRPC {}
+1
packages/vitest/src/runtime/workers/base.ts
··· 23 23 return _moduleRunner 24 24 } 25 25 26 + /** @experimental */ 26 27 export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 27 28 const { ctx } = state 28 29 // state has new context, but we want to reuse existing ones
+2 -34
packages/vitest/src/runtime/workers/forks.ts
··· 1 - import type { WorkerGlobalState } from '../../types/worker' 2 - import type { VitestWorker, WorkerRpcOptions } from './types' 3 - import v8 from 'node:v8' 4 1 import { runBaseTests } from './base' 5 - import { createForksRpcOptions, unwrapSerializableConfig } from './utils' 2 + import workerInit from './init-forks' 6 3 7 - class ForksBaseWorker implements VitestWorker { 8 - getRpcOptions(): WorkerRpcOptions { 9 - return createForksRpcOptions(v8) 10 - } 11 - 12 - async executeTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 13 - // TODO: don't rely on reassigning process.exit 14 - // https://github.com/vitest-dev/vitest/pull/4441#discussion_r1443771486 15 - const exit = process.exit 16 - state.ctx.config = unwrapSerializableConfig(state.ctx.config) 17 - 18 - try { 19 - await runBaseTests(method, state) 20 - } 21 - finally { 22 - process.exit = exit 23 - } 24 - } 25 - 26 - runTests(state: WorkerGlobalState): Promise<void> { 27 - return this.executeTests('run', state) 28 - } 29 - 30 - collectTests(state: WorkerGlobalState): Promise<void> { 31 - return this.executeTests('collect', state) 32 - } 33 - } 34 - 35 - const worker: ForksBaseWorker = new ForksBaseWorker() 36 - export default worker 4 + workerInit({ runTests: runBaseTests })
+107
packages/vitest/src/runtime/workers/init-forks.ts
··· 1 + import type { ResolvedConfig, SerializedConfig } from '../../node/types/config' 2 + import type { WorkerGlobalState } from '../../types/worker' 3 + import v8 from 'node:v8' 4 + import { init } from './init' 5 + 6 + if (!process.send) { 7 + throw new Error('Expected worker to be run in node:child_process') 8 + } 9 + 10 + // Store globals in case tests overwrite them 11 + const processExit = process.exit.bind(process) 12 + const processSend = process.send.bind(process) 13 + const processOn = process.on.bind(process) 14 + const processOff = process.off.bind(process) 15 + const processRemoveAllListeners = process.removeAllListeners.bind(process) 16 + 17 + const isProfiling = process.execArgv.some( 18 + execArg => 19 + execArg.startsWith('--prof') 20 + || execArg.startsWith('--cpu-prof') 21 + || execArg.startsWith('--heap-prof') 22 + || execArg.startsWith('--diagnostic-dir'), 23 + ) 24 + 25 + // Work-around for nodejs/node#55094 26 + if (isProfiling) { 27 + processOn('SIGTERM', () => processExit()) 28 + } 29 + 30 + export default function workerInit(options: { 31 + runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void> 32 + }): void { 33 + const { runTests } = options 34 + 35 + init({ 36 + post: v => processSend(v), 37 + on: cb => processOn('message', cb), 38 + off: cb => processOff('message', cb), 39 + teardown: () => processRemoveAllListeners('message'), 40 + serialize: v8.serialize, 41 + deserialize: v => v8.deserialize(Buffer.from(v)), 42 + runTests: state => executeTests('run', state), 43 + collectTests: state => executeTests('collect', state), 44 + }) 45 + 46 + async function executeTests(method: 'run' | 'collect', state: WorkerGlobalState) { 47 + state.ctx.config = unwrapSerializableConfig(state.ctx.config) 48 + 49 + try { 50 + await runTests(method, state) 51 + } 52 + finally { 53 + process.exit = processExit 54 + } 55 + } 56 + } 57 + 58 + /** 59 + * Reverts the wrapping done by `wrapSerializableConfig` in {@link file://./../../node/pool/runtimes/forks.ts} 60 + */ 61 + function unwrapSerializableConfig(config: SerializedConfig): SerializedConfig { 62 + if (config.testNamePattern && typeof config.testNamePattern === 'string') { 63 + const testNamePattern = config.testNamePattern as string 64 + 65 + if (testNamePattern.startsWith('$$vitest:')) { 66 + config.testNamePattern = parseRegexp(testNamePattern.slice('$$vitest:'.length)) 67 + } 68 + } 69 + 70 + if ( 71 + config.defines 72 + && Array.isArray(config.defines.keys) 73 + && config.defines.original 74 + ) { 75 + const { keys, original } = config.defines 76 + const defines: ResolvedConfig['defines'] = {} 77 + 78 + // Apply all keys from the original. Entries which had undefined value are missing from original now 79 + for (const key of keys) { 80 + defines[key] = original[key] 81 + } 82 + 83 + config.defines = defines 84 + } 85 + 86 + return config 87 + } 88 + 89 + function parseRegexp(input: string): RegExp { 90 + // Parse input 91 + // eslint-disable-next-line regexp/no-misleading-capturing-group 92 + const m = input.match(/(\/?)(.+)\1([a-z]*)/i) 93 + 94 + // match nothing 95 + if (!m) { 96 + return /$^/ 97 + } 98 + 99 + // Invalid flags 100 + // eslint-disable-next-line regexp/optimal-quantifier-concatenation 101 + if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) { 102 + return new RegExp(input) 103 + } 104 + 105 + // Create the regular expression 106 + return new RegExp(m[2], m[3]) 107 + }
+22
packages/vitest/src/runtime/workers/init-threads.ts
··· 1 + import type { WorkerGlobalState } from '../../types/worker' 2 + import { isMainThread, parentPort } from 'node:worker_threads' 3 + import { init } from './init' 4 + 5 + if (isMainThread || !parentPort) { 6 + throw new Error('Expected worker to be run in node:worker_threads') 7 + } 8 + 9 + export default function workerInit(options: { 10 + runTests: (method: 'run' | 'collect', state: WorkerGlobalState) => Promise<void> 11 + }): void { 12 + const { runTests } = options 13 + 14 + init({ 15 + post: response => parentPort!.postMessage(response), 16 + on: callback => parentPort!.on('message', callback), 17 + off: callback => parentPort!.off('message', callback), 18 + teardown: () => parentPort!.removeAllListeners('message'), 19 + runTests: async state => runTests('run', state), 20 + collectTests: async state => runTests('collect', state), 21 + }) 22 + }
+124
packages/vitest/src/runtime/workers/init.ts
··· 1 + import type { WorkerRequest, WorkerResponse } from '../../node/pools/types' 2 + import type { VitestWorker } from './types' 3 + import { serializeError } from '@vitest/utils/error' 4 + import * as entrypoint from '../worker' 5 + 6 + interface Options extends VitestWorker { 7 + teardown?: () => void 8 + } 9 + 10 + const __vitest_worker_response__ = true 11 + const memoryUsage = process.memoryUsage.bind(process) 12 + let reportMemory = false 13 + 14 + /** @experimental */ 15 + export function init(worker: Options): void { 16 + worker.on(onMessage) 17 + 18 + let runPromise: Promise<unknown> | undefined 19 + let isRunning = false 20 + 21 + function send(response: WorkerResponse) { 22 + worker.post(worker.serialize ? worker.serialize(response) : response) 23 + } 24 + 25 + async function onMessage(rawMessage: unknown) { 26 + const message: WorkerRequest = worker.deserialize 27 + ? worker.deserialize(rawMessage) 28 + : rawMessage 29 + 30 + if (message?.__vitest_worker_request__ !== true) { 31 + return 32 + } 33 + 34 + switch (message.type) { 35 + case 'start': { 36 + reportMemory = message.options.reportMemory 37 + send({ type: 'started', __vitest_worker_response__ }) 38 + 39 + break 40 + } 41 + 42 + case 'run': { 43 + // Prevent concurrent execution if worker is already running 44 + if (isRunning) { 45 + send({ 46 + type: 'testfileFinished', 47 + __vitest_worker_response__, 48 + error: serializeError(new Error('[vitest-worker]: Worker is already running tests')), 49 + }) 50 + return 51 + } 52 + 53 + isRunning = true 54 + process.env.VITEST_POOL_ID = String(message.poolId) 55 + process.env.VITEST_WORKER_ID = String(message.context.workerId) 56 + 57 + try { 58 + runPromise = entrypoint.run(message.context, worker) 59 + .catch(error => serializeError(error)) 60 + const error = await runPromise 61 + 62 + send({ 63 + type: 'testfileFinished', 64 + __vitest_worker_response__, 65 + error, 66 + usedMemory: reportMemory ? memoryUsage().heapUsed : undefined, 67 + }) 68 + } 69 + finally { 70 + runPromise = undefined 71 + isRunning = false 72 + } 73 + 74 + break 75 + } 76 + 77 + case 'collect': { 78 + // Prevent concurrent execution if worker is already running 79 + if (isRunning) { 80 + send({ 81 + type: 'testfileFinished', 82 + __vitest_worker_response__, 83 + error: serializeError(new Error('[vitest-worker]: Worker is already running tests')), 84 + }) 85 + return 86 + } 87 + 88 + isRunning = true 89 + process.env.VITEST_POOL_ID = String(message.poolId) 90 + process.env.VITEST_WORKER_ID = String(message.context.workerId) 91 + 92 + try { 93 + runPromise = entrypoint.collect(message.context, worker) 94 + .catch(error => serializeError(error)) 95 + const error = await runPromise 96 + 97 + send({ 98 + type: 'testfileFinished', 99 + __vitest_worker_response__, 100 + error, 101 + usedMemory: reportMemory ? memoryUsage().heapUsed : undefined, 102 + }) 103 + } 104 + finally { 105 + runPromise = undefined 106 + isRunning = false 107 + } 108 + 109 + break 110 + } 111 + 112 + case 'stop': { 113 + await runPromise 114 + const error = await entrypoint.teardown() 115 + .catch(error => serializeError(error)) 116 + 117 + send({ type: 'stopped', error, __vitest_worker_response__ }) 118 + worker.teardown?.() 119 + 120 + break 121 + } 122 + } 123 + } 124 + }
+2 -20
packages/vitest/src/runtime/workers/threads.ts
··· 1 - import type { WorkerContext } from '../../node/types/worker' 2 - import type { ContextRPC, WorkerGlobalState } from '../../types/worker' 3 - import type { VitestWorker, WorkerRpcOptions } from './types' 4 1 import { runBaseTests } from './base' 5 - import { createThreadsRpcOptions } from './utils' 2 + import workerInit from './init-threads' 6 3 7 - class ThreadsBaseWorker implements VitestWorker { 8 - getRpcOptions(ctx: ContextRPC): WorkerRpcOptions { 9 - return createThreadsRpcOptions(ctx as WorkerContext) 10 - } 11 - 12 - runTests(state: WorkerGlobalState): unknown { 13 - return runBaseTests('run', state) 14 - } 15 - 16 - collectTests(state: WorkerGlobalState): unknown { 17 - return runBaseTests('collect', state) 18 - } 19 - } 20 - 21 - const worker: ThreadsBaseWorker = new ThreadsBaseWorker() 22 - export default worker 4 + workerInit({ runTests: runBaseTests })
+4 -5
packages/vitest/src/runtime/workers/types.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 2 import type { BirpcOptions } from 'birpc' 3 3 import type { RuntimeRPC } from '../../types/rpc' 4 - import type { ContextRPC, WorkerGlobalState } from '../../types/worker' 4 + import type { WorkerGlobalState } from '../../types/worker' 5 5 6 - export type WorkerRpcOptions = Pick< 6 + type WorkerRpcOptions = Pick< 7 7 BirpcOptions<RuntimeRPC>, 8 - 'on' | 'post' | 'serialize' | 'deserialize' 8 + 'on' | 'off' | 'post' | 'serialize' | 'deserialize' 9 9 > 10 10 11 - export interface VitestWorker { 12 - getRpcOptions: (ctx: ContextRPC) => WorkerRpcOptions 11 + export interface VitestWorker extends WorkerRpcOptions { 13 12 runTests: (state: WorkerGlobalState) => Awaitable<unknown> 14 13 collectTests: (state: WorkerGlobalState) => Awaitable<unknown> 15 14 }
-112
packages/vitest/src/runtime/workers/utils.ts
··· 1 - import type { TinypoolWorkerMessage } from 'tinypool' 2 - import type { ResolvedConfig, SerializedConfig } from '../../node/types/config' 3 - import type { WorkerContext } from '../../node/types/worker' 4 - import type { WorkerRpcOptions } from './types' 5 - 6 - const REGEXP_WRAP_PREFIX = '$$vitest:' 7 - 8 - // Store global APIs in case process is overwritten by tests 9 - const processSend = process.send?.bind(process) 10 - const processOn = process.on?.bind(process) 11 - const processOff = process.off?.bind(process) 12 - const dispose: (() => void)[] = [] 13 - 14 - export function createThreadsRpcOptions({ 15 - port, 16 - }: WorkerContext): WorkerRpcOptions { 17 - return { 18 - post: (v) => { 19 - port.postMessage(v) 20 - }, 21 - on: (fn) => { 22 - port.addListener('message', fn) 23 - }, 24 - } 25 - } 26 - 27 - export function disposeInternalListeners(): void { 28 - for (const fn of dispose) { 29 - try { 30 - fn() 31 - } 32 - catch {} 33 - } 34 - dispose.length = 0 35 - } 36 - 37 - export function createForksRpcOptions( 38 - nodeV8: typeof import('v8'), 39 - ): WorkerRpcOptions { 40 - return { 41 - serialize: nodeV8.serialize, 42 - deserialize: v => nodeV8.deserialize(Buffer.from(v)), 43 - post(v) { 44 - processSend!(v) 45 - }, 46 - on(fn) { 47 - const handler = (message: any, ...extras: any) => { 48 - // Do not react on Tinypool's internal messaging 49 - if ((message as TinypoolWorkerMessage)?.__tinypool_worker_message__) { 50 - return 51 - } 52 - 53 - return fn(message, ...extras) 54 - } 55 - processOn('message', handler) 56 - dispose.push(() => processOff('message', handler)) 57 - }, 58 - } 59 - } 60 - 61 - /** 62 - * Reverts the wrapping done by `utils/config-helpers.ts`'s `wrapSerializableConfig` 63 - */ 64 - export function unwrapSerializableConfig(config: SerializedConfig): SerializedConfig { 65 - if (config.testNamePattern && typeof config.testNamePattern === 'string') { 66 - const testNamePattern = config.testNamePattern as string 67 - 68 - if (testNamePattern.startsWith(REGEXP_WRAP_PREFIX)) { 69 - config.testNamePattern = parseRegexp( 70 - testNamePattern.slice(REGEXP_WRAP_PREFIX.length), 71 - ) 72 - } 73 - } 74 - 75 - if ( 76 - config.defines 77 - && Array.isArray(config.defines.keys) 78 - && config.defines.original 79 - ) { 80 - const { keys, original } = config.defines 81 - const defines: ResolvedConfig['defines'] = {} 82 - 83 - // Apply all keys from the original. Entries which had undefined value are missing from original now 84 - for (const key of keys) { 85 - defines[key] = original[key] 86 - } 87 - 88 - config.defines = defines 89 - } 90 - 91 - return config 92 - } 93 - 94 - function parseRegexp(input: string): RegExp { 95 - // Parse input 96 - // eslint-disable-next-line regexp/no-misleading-capturing-group 97 - const m = input.match(/(\/?)(.+)\1([a-z]*)/i) 98 - 99 - // match nothing 100 - if (!m) { 101 - return /$^/ 102 - } 103 - 104 - // Invalid flags 105 - // eslint-disable-next-line regexp/optimal-quantifier-concatenation 106 - if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) { 107 - return new RegExp(input) 108 - } 109 - 110 - // Create the regular expression 111 - return new RegExp(m[2], m[3]) 112 - }
+2 -32
packages/vitest/src/runtime/workers/vmForks.ts
··· 1 - import type { WorkerGlobalState } from '../../types/worker' 2 - import type { VitestWorker, WorkerRpcOptions } from './types' 3 - import v8 from 'node:v8' 4 - import { createForksRpcOptions, unwrapSerializableConfig } from './utils' 1 + import workerInit from './init-forks' 5 2 import { runVmTests } from './vm' 6 3 7 - class ForksVmWorker implements VitestWorker { 8 - getRpcOptions(): WorkerRpcOptions { 9 - return createForksRpcOptions(v8) 10 - } 11 - 12 - async executeTests(method: 'run' | 'collect', state: WorkerGlobalState): Promise<void> { 13 - const exit = process.exit 14 - state.ctx.config = unwrapSerializableConfig(state.ctx.config) 15 - 16 - try { 17 - await runVmTests(method, state) 18 - } 19 - finally { 20 - process.exit = exit 21 - } 22 - } 23 - 24 - runTests(state: WorkerGlobalState): Promise<void> { 25 - return this.executeTests('run', state) 26 - } 27 - 28 - collectTests(state: WorkerGlobalState): Promise<void> { 29 - return this.executeTests('collect', state) 30 - } 31 - } 32 - 33 - const worker: ForksVmWorker = new ForksVmWorker() 34 - export default worker 4 + workerInit({ runTests: runVmTests })
+2 -20
packages/vitest/src/runtime/workers/vmThreads.ts
··· 1 - import type { WorkerContext } from '../../node/types/worker' 2 - import type { ContextRPC, WorkerGlobalState } from '../../types/worker' 3 - import type { VitestWorker, WorkerRpcOptions } from './types' 4 - import { createThreadsRpcOptions } from './utils' 1 + import workerInit from './init-threads' 5 2 import { runVmTests } from './vm' 6 3 7 - class ThreadsVmWorker implements VitestWorker { 8 - getRpcOptions(ctx: ContextRPC): WorkerRpcOptions { 9 - return createThreadsRpcOptions(ctx as WorkerContext) 10 - } 11 - 12 - runTests(state: WorkerGlobalState): unknown { 13 - return runVmTests('run', state) 14 - } 15 - 16 - collectTests(state: WorkerGlobalState): unknown { 17 - return runVmTests('collect', state) 18 - } 19 - } 20 - 21 - const worker: ThreadsVmWorker = new ThreadsVmWorker() 22 - export default worker 4 + workerInit({ runTests: runVmTests })
+80 -12
test/cli/fixtures/custom-pool/pool/custom-pool.ts
··· 1 1 import type { RunnerTestCase } from 'vitest' 2 - import type { ProcessPool, Vitest } from 'vitest/node' 2 + import type { PoolWorker, PoolRunnerInitializer, TestProject, Vitest, WorkerRequest, WorkerResponse, PoolOptions } from 'vitest/node' 3 3 import { createFileTask } from '@vitest/runner/utils' 4 4 import { normalize } from 'pathe' 5 + import EventEmitter from 'node:events'; 5 6 6 - export default (vitest: Vitest): ProcessPool => { 7 - const options = vitest.config.poolOptions?.custom as any 7 + interface OptionsCustomPool { 8 + print: any; 9 + array: any; 10 + } 11 + 12 + export function createCustomPool(settings: OptionsCustomPool): PoolRunnerInitializer { 8 13 return { 9 14 name: 'custom', 10 - async collectTests() { 11 - throw new Error('Not implemented') 12 - }, 13 - async runTests(specs) { 15 + createPoolWorker: (options) => new CustomRuntimeWorker(options, settings), 16 + } 17 + } 18 + 19 + export class CustomRuntimeWorker implements PoolWorker { 20 + public readonly name = 'custom' 21 + private vitest: Vitest 22 + private customEvents = new EventEmitter() 23 + readonly execArgv: string[] 24 + readonly env: Record<string, string> 25 + private project: TestProject 26 + 27 + constructor(options: PoolOptions, private settings: OptionsCustomPool) { 28 + this.execArgv = options.execArgv 29 + this.env = options.env 30 + this.vitest = options.project.vitest 31 + this.project = options.project 32 + } 33 + 34 + send(request: WorkerRequest) { 35 + void onMessage(request, this.project, this.settings).then((response) => { 36 + if (response) { 37 + this.customEvents.emit('message', response) 38 + } 39 + }) 40 + } 41 + 42 + on(event: string, callback: (arg: any) => void): void { 43 + this.customEvents.on(event, callback) 44 + } 45 + 46 + off(event: string, callback: (arg: any) => void): void { 47 + this.customEvents.off(event, callback) 48 + } 49 + 50 + deserialize(data: unknown): unknown { 51 + return data 52 + } 53 + 54 + async start() { 55 + // noop 56 + } 57 + 58 + async stop() { 59 + this.vitest.logger.console.warn('[pool] custom pool is closed!') 60 + } 61 + } 62 + 63 + const __vitest_worker_response__ = true 64 + 65 + async function onMessage(message: WorkerRequest, project: TestProject, options: OptionsCustomPool): Promise<WorkerResponse | void> { 66 + if (message?.__vitest_worker_request__ !== true) { 67 + return undefined 68 + } 69 + const vitest = project.vitest 70 + 71 + switch (message.type) { 72 + case 'start': { 73 + return { type: 'started', __vitest_worker_response__ } 74 + } 75 + 76 + case 'run': { 14 77 vitest.logger.console.warn('[pool] printing:', options.print) 15 78 vitest.logger.console.warn('[pool] array option', options.array) 16 - for (const { project, moduleId } of specs) { 79 + for (const { filepath: moduleId } of message.context.files) { 17 80 vitest.state.clearFiles(project) 18 81 vitest.logger.console.warn('[pool] running tests for', project.name, 'in', normalize(moduleId).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) 19 82 const taskFile = createFileTask( ··· 42 105 taskFile.tasks.push(taskTest) 43 106 await vitest._reportFileTask(taskFile) 44 107 } 45 - }, 46 - close() { 47 - vitest.logger.console.warn('[pool] custom pool is closed!') 48 - }, 108 + 109 + return { type: 'testfileFinished', __vitest_worker_response__ } 110 + } 111 + 112 + case 'stop': { 113 + return { type: 'stopped', __vitest_worker_response__ } 114 + } 49 115 } 116 + 117 + throw new Error(`Unexpected message ${JSON.stringify(message, null, 2)}`) 50 118 }
+133
packages/vitest/src/node/pools/workers/forksWorker.ts
··· 1 + import type { ChildProcess } from 'node:child_process' 2 + import type { SerializedConfig } from '../../types/config' 3 + import type { PoolOptions, PoolWorker, WorkerRequest } from '../types' 4 + import { fork } from 'node:child_process' 5 + import { resolve } from 'node:path' 6 + import v8 from 'node:v8' 7 + 8 + const SIGKILL_TIMEOUT = 500 /** jest does 500ms by default, let's follow it */ 9 + 10 + /** @experimental */ 11 + export class ForksPoolWorker implements PoolWorker { 12 + public readonly name: string = 'forks' 13 + public readonly execArgv: string[] 14 + public readonly env: Record<string, string> 15 + public readonly cacheFs: boolean = true 16 + 17 + protected readonly entrypoint: string 18 + 19 + private _fork?: ChildProcess 20 + 21 + constructor(options: PoolOptions) { 22 + this.execArgv = options.execArgv 23 + this.env = options.env 24 + /** Loads {@link file://./../../../runtime/workers/forks.ts} */ 25 + this.entrypoint = resolve(options.distPath, 'workers/forks.js') 26 + } 27 + 28 + on(event: string, callback: (arg: any) => void): void { 29 + this.fork.on(event, callback) 30 + } 31 + 32 + off(event: string, callback: (arg: any) => void): void { 33 + this.fork.off(event, callback) 34 + } 35 + 36 + send(message: WorkerRequest): void { 37 + if ('context' in message) { 38 + message = { 39 + ...message, 40 + context: { 41 + ...message.context, 42 + config: wrapSerializableConfig(message.context.config), 43 + }, 44 + } 45 + } 46 + 47 + this.fork.send(v8.serialize(message)) 48 + } 49 + 50 + async start(): Promise<void> { 51 + this._fork ||= fork(this.entrypoint, [], { 52 + env: this.env, 53 + execArgv: this.execArgv, 54 + }) 55 + } 56 + 57 + async stop(): Promise<void> { 58 + const fork = this.fork 59 + const waitForExit = new Promise<void>((resolve) => { 60 + if (fork.exitCode != null) { 61 + resolve() 62 + } 63 + else { 64 + fork.once('exit', resolve) 65 + } 66 + }) 67 + 68 + /* 69 + * If process running user's code does not stop on SIGTERM, send SIGKILL. 70 + * This is similar to 71 + * - https://github.com/jestjs/jest/blob/25a8785584c9d54a05887001ee7f498d489a5441/packages/jest-worker/src/workers/ChildProcessWorker.ts#L463-L477 72 + * - https://github.com/tinylibs/tinypool/blob/40b4b3eb926dabfbfd3d0a7e3d1222d4dd1c0d2d/src/runtime/process-worker.ts#L56 73 + */ 74 + const sigkillTimeout = setTimeout( 75 + () => fork.kill('SIGKILL'), 76 + SIGKILL_TIMEOUT, 77 + ) 78 + 79 + fork.kill() 80 + await waitForExit 81 + clearTimeout(sigkillTimeout) 82 + 83 + this._fork = undefined 84 + } 85 + 86 + deserialize(data: unknown): unknown { 87 + try { 88 + return v8.deserialize(Buffer.from(data as ArrayBuffer)) 89 + } 90 + catch (error) { 91 + let stringified = '' 92 + 93 + try { 94 + stringified = `\nReceived value: ${JSON.stringify(data)}` 95 + } 96 + catch {} 97 + 98 + throw new Error(`[vitest-pool]: Unexpected call to process.send(). Make sure your test cases are not interfering with process's channel.${stringified}`, { cause: error }) 99 + } 100 + } 101 + 102 + private get fork() { 103 + if (!this._fork) { 104 + throw new Error(`The child process was torn down or never initialized. This is a bug in Vitest.`) 105 + } 106 + return this._fork 107 + } 108 + } 109 + 110 + /** 111 + * Prepares `SerializedConfig` for serialization, e.g. `node:v8.serialize` 112 + * - Unwrapping done in {@link file://./../../../runtime/workers/init-forks.ts} 113 + */ 114 + function wrapSerializableConfig(config: SerializedConfig) { 115 + let testNamePattern = config.testNamePattern 116 + let defines = config.defines 117 + 118 + // v8 serialize does not support regex 119 + if (testNamePattern && typeof testNamePattern !== 'string') { 120 + testNamePattern = `$$vitest:${testNamePattern.toString()}` as unknown as RegExp 121 + } 122 + 123 + // v8 serialize drops properties with undefined value 124 + if (defines) { 125 + defines = { keys: Object.keys(defines), original: defines } 126 + } 127 + 128 + return { 129 + ...config, 130 + testNamePattern, 131 + defines, 132 + } as SerializedConfig 133 + }
+57
packages/vitest/src/node/pools/workers/threadsWorker.ts
··· 1 + import type { PoolOptions, PoolWorker, WorkerRequest } from '../types' 2 + import { resolve } from 'node:path' 3 + import { Worker } from 'node:worker_threads' 4 + 5 + /** @experimental */ 6 + export class ThreadsPoolWorker implements PoolWorker { 7 + public readonly name: string = 'threads' 8 + public readonly execArgv: string[] 9 + public readonly env: Record<string, string> 10 + protected readonly entrypoint: string 11 + 12 + private _thread?: Worker 13 + 14 + constructor(options: PoolOptions) { 15 + this.execArgv = options.execArgv 16 + this.env = options.env 17 + /** Loads {@link file://./../../../runtime/workers/threads.ts} */ 18 + this.entrypoint = resolve(options.distPath, 'workers/threads.js') 19 + } 20 + 21 + on(event: string, callback: (arg: any) => void): void { 22 + this.thread.on(event, callback) 23 + } 24 + 25 + off(event: string, callback: (arg: any) => void): void { 26 + this.thread.off(event, callback) 27 + } 28 + 29 + send(message: WorkerRequest): void { 30 + this.thread.postMessage(message) 31 + } 32 + 33 + async start(): Promise<void> { 34 + // This can be called multiple times if the runtime is shared. 35 + this._thread ||= new Worker(this.entrypoint, { 36 + env: this.env, 37 + execArgv: this.execArgv, 38 + }) 39 + } 40 + 41 + async stop(): Promise<void> { 42 + await this.thread.terminate().then(() => { 43 + this._thread = undefined 44 + }) 45 + } 46 + 47 + deserialize(data: unknown): unknown { 48 + return data 49 + } 50 + 51 + private get thread() { 52 + if (!this._thread) { 53 + throw new Error(`The worker thread was torn down or never initialized. This is a bug in Vitest.`) 54 + } 55 + return this._thread 56 + } 57 + }
+257
packages/vitest/src/node/pools/workers/typecheckWorker.ts
··· 1 + import type { FileSpecification } from '@vitest/runner' 2 + import type { DeferPromise } from '@vitest/utils' 3 + import type { TypecheckResults } from '../../../typecheck/typechecker' 4 + import type { Vitest } from '../../core' 5 + import type { TestProject } from '../../project' 6 + import type { TestRunEndReason } from '../../types/reporter' 7 + import type { PoolOptions, PoolWorker, WorkerRequest, WorkerResponse } from '../types' 8 + import EventEmitter from 'node:events' 9 + import { hasFailed } from '@vitest/runner/utils' 10 + import { createDefer } from '@vitest/utils/helpers' 11 + import { Typechecker } from '../../../typecheck/typechecker' 12 + 13 + /** @experimental */ 14 + export class TypecheckPoolWorker implements PoolWorker { 15 + public readonly name: string = 'typecheck' 16 + public readonly execArgv: string[] 17 + public readonly env: Record<string, string> 18 + private readonly project: TestProject 19 + 20 + private _eventEmitter = new EventEmitter() 21 + 22 + constructor(options: PoolOptions) { 23 + this.execArgv = options.execArgv 24 + this.env = options.env 25 + this.project = options.project 26 + } 27 + 28 + async start(): Promise<void> { 29 + // noop, onMessage handles it 30 + } 31 + 32 + async stop(): Promise<void> { 33 + // noop, onMessage handles it 34 + } 35 + 36 + send(message: WorkerRequest): void { 37 + void onMessage(message, this.project).then((response) => { 38 + if (response) { 39 + this._eventEmitter.emit('message', response) 40 + } 41 + }) 42 + } 43 + 44 + on(event: string, callback: (arg: any) => any): void { 45 + this._eventEmitter.on(event, callback) 46 + } 47 + 48 + off(event: string, callback: (arg: any) => any): void { 49 + this._eventEmitter.on(event, callback) 50 + } 51 + 52 + deserialize(data: unknown): unknown { 53 + return data 54 + } 55 + } 56 + 57 + const __vitest_worker_response__ = true 58 + const runners = new WeakMap<Vitest, ReturnType<typeof createRunner>>() 59 + 60 + async function onMessage(message: WorkerRequest, project: TestProject): Promise<WorkerResponse | void> { 61 + if (message?.__vitest_worker_request__ !== true) { 62 + return undefined 63 + } 64 + 65 + let runner = runners.get(project.vitest) 66 + if (!runner) { 67 + runner = createRunner(project.vitest) 68 + runners.set(project.vitest, runner) 69 + } 70 + 71 + let runPromise: Promise<unknown> | undefined 72 + 73 + switch (message.type) { 74 + case 'start': { 75 + return { type: 'started', __vitest_worker_response__ } 76 + } 77 + 78 + case 'run': { 79 + runPromise = runner.runTests(message.context.files, project) 80 + .catch(error => error) 81 + const error = await runPromise 82 + 83 + return { type: 'testfileFinished', error, __vitest_worker_response__ } 84 + } 85 + 86 + case 'collect': { 87 + runPromise = runner.collectTests(message.context.files, project) 88 + .catch(error => error) 89 + const error = await runPromise 90 + 91 + return { type: 'testfileFinished', error, __vitest_worker_response__ } 92 + } 93 + 94 + case 'stop': { 95 + await runPromise 96 + await project.typechecker?.stop() 97 + return { type: 'stopped', __vitest_worker_response__ } 98 + } 99 + } 100 + 101 + throw new Error(`Unexpected message ${JSON.stringify(message, null, 2)}`) 102 + } 103 + 104 + function createRunner(vitest: Vitest) { 105 + const promisesMap = new WeakMap<TestProject, DeferPromise<void>>() 106 + const rerunTriggered = new WeakSet<TestProject>() 107 + 108 + async function onParseEnd( 109 + project: TestProject, 110 + { files, sourceErrors }: TypecheckResults, 111 + ) { 112 + const checker = project.typechecker! 113 + 114 + const { packs, events } = checker.getTestPacksAndEvents() 115 + await vitest._testRun.updated(packs, events) 116 + 117 + if (!project.config.typecheck.ignoreSourceErrors) { 118 + sourceErrors.forEach(error => 119 + vitest.state.catchError(error, 'Unhandled Source Error'), 120 + ) 121 + } 122 + 123 + const processError = !hasFailed(files) && !sourceErrors.length && checker.getExitCode() 124 + if (processError) { 125 + const error = new Error(checker.getOutput()) 126 + error.stack = '' 127 + vitest.state.catchError(error, 'Typecheck Error') 128 + } 129 + 130 + promisesMap.get(project)?.resolve() 131 + 132 + rerunTriggered.delete(project) 133 + 134 + // triggered by TSC watcher, not Vitest watcher, so we need to emulate what Vitest does in this case 135 + if (vitest.config.watch && !vitest.runningPromise) { 136 + const modules = files.map(file => vitest.state.getReportedEntity(file)).filter(e => e?.type === 'module') 137 + 138 + const state: TestRunEndReason = vitest.isCancelling 139 + ? 'interrupted' 140 + : modules.some(m => !m.ok()) 141 + ? 'failed' 142 + : 'passed' 143 + 144 + await vitest.report('onTestRunEnd', modules, [], state) 145 + await vitest.report('onWatcherStart', files, [ 146 + ...(project.config.typecheck.ignoreSourceErrors ? [] : sourceErrors), 147 + ...vitest.state.getUnhandledErrors(), 148 + ]) 149 + } 150 + } 151 + 152 + async function createWorkspaceTypechecker( 153 + project: TestProject, 154 + files: string[], 155 + ) { 156 + const checker = project.typechecker ?? new Typechecker(project) 157 + if (project.typechecker) { 158 + return checker 159 + } 160 + 161 + project.typechecker = checker 162 + checker.setFiles(files) 163 + 164 + checker.onParseStart(async () => { 165 + const files = checker.getTestFiles() 166 + for (const file of files) { 167 + await vitest._testRun.enqueued(project, file) 168 + } 169 + await vitest._testRun.collected(project, files) 170 + }) 171 + 172 + checker.onParseEnd(result => onParseEnd(project, result)) 173 + 174 + checker.onWatcherRerun(async () => { 175 + rerunTriggered.add(project) 176 + 177 + if (!vitest.runningPromise) { 178 + vitest.state.clearErrors() 179 + await vitest.report( 180 + 'onWatcherRerun', 181 + files, 182 + 'File change detected. Triggering rerun.', 183 + ) 184 + } 185 + 186 + await checker.collectTests() 187 + 188 + const testFiles = checker.getTestFiles() 189 + for (const file of testFiles) { 190 + await vitest._testRun.enqueued(project, file) 191 + } 192 + await vitest._testRun.collected(project, testFiles) 193 + 194 + const { packs, events } = checker.getTestPacksAndEvents() 195 + await vitest._testRun.updated(packs, events) 196 + }) 197 + 198 + return checker 199 + } 200 + 201 + async function startTypechecker(project: TestProject, files: string[]) { 202 + if (project.typechecker) { 203 + return 204 + } 205 + const checker = await createWorkspaceTypechecker(project, files) 206 + await checker.collectTests() 207 + await checker.start() 208 + } 209 + 210 + async function collectTests(specs: FileSpecification[], project: TestProject) { 211 + const files = specs.map(spec => spec.filepath) 212 + const checker = await createWorkspaceTypechecker(project, files) 213 + checker.setFiles(files) 214 + await checker.collectTests() 215 + const testFiles = checker.getTestFiles() 216 + vitest.state.collectFiles(project, testFiles) 217 + } 218 + 219 + async function runTests(specs: FileSpecification[], project: TestProject) { 220 + const promises: Promise<void>[] = [] 221 + 222 + const files = specs.map(spec => spec.filepath) 223 + const promise = createDefer<void>() 224 + // check that watcher actually triggered rerun 225 + const _p = new Promise<boolean>((resolve) => { 226 + const _i = setInterval(() => { 227 + if (!project.typechecker || rerunTriggered.has(project)) { 228 + resolve(true) 229 + clearInterval(_i) 230 + } 231 + }) 232 + setTimeout(() => { 233 + resolve(false) 234 + clearInterval(_i) 235 + }, 500).unref() 236 + }) 237 + const triggered = await _p 238 + if (project.typechecker && !triggered) { 239 + const testFiles = project.typechecker.getTestFiles() 240 + for (const file of testFiles) { 241 + await vitest._testRun.enqueued(project, file) 242 + } 243 + await vitest._testRun.collected(project, testFiles) 244 + await onParseEnd(project, project.typechecker.getResult()) 245 + } 246 + promises.push(promise) 247 + promisesMap.set(project, promise) 248 + promises.push(startTypechecker(project, files)) 249 + 250 + await Promise.all(promises) 251 + } 252 + 253 + return { 254 + runTests, 255 + collectTests, 256 + } 257 + }
+17
packages/vitest/src/node/pools/workers/vmForksWorker.ts
··· 1 + import type { PoolOptions } from '../types' 2 + import { resolve } from 'node:path' 3 + import { ForksPoolWorker } from './forksWorker' 4 + 5 + /** @experimental */ 6 + export class VmForksPoolWorker extends ForksPoolWorker { 7 + public readonly name = 'vmForks' 8 + public readonly reportMemory = true 9 + protected readonly entrypoint: string 10 + 11 + constructor(options: PoolOptions) { 12 + super({ ...options, execArgv: [...options.execArgv, '--experimental-vm-modules'] }) 13 + 14 + /** Loads {@link file://./../../../runtime/workers/vmForks.ts} */ 15 + this.entrypoint = resolve(options.distPath, 'workers/vmForks.js') 16 + } 17 + }
+17
packages/vitest/src/node/pools/workers/vmThreadsWorker.ts
··· 1 + import type { PoolOptions } from '../types' 2 + import { resolve } from 'node:path' 3 + import { ThreadsPoolWorker } from './threadsWorker' 4 + 5 + /** @experimental */ 6 + export class VmThreadsPoolWorker extends ThreadsPoolWorker { 7 + public readonly name = 'vmThreads' 8 + public readonly reportMemory = true 9 + protected readonly entrypoint: string 10 + 11 + constructor(options: PoolOptions) { 12 + super({ ...options, execArgv: [...options.execArgv, '--experimental-vm-modules'] }) 13 + 14 + /** Loads {@link file://./../../../runtime/workers/vmThreads.ts} */ 15 + this.entrypoint = resolve(options.distPath, 'workers/vmThreads.js') 16 + } 17 + }