Skip to content

Commit

Permalink
remove depdency on config extension in runner, use provide/inject, ad…
Browse files Browse the repository at this point in the history
…d tests
  • Loading branch information
raegen committed Aug 12, 2024
1 parent 440babb commit a60224f
Show file tree
Hide file tree
Showing 25 changed files with 252 additions and 825 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ name: Checks
on:
pull_request:
branches:
- develop
- main
workflow_dispatch:

jobs:
build:
name: 'Build & Release'
name: 'Run tests'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea
node_modules
dist
__test__/.cache
.DS_Store
__test__/tests/deep/nested/dependency/variables/*
85 changes: 85 additions & 0 deletions __test__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { InlineConfig, startVitest } from 'vitest/node';
import vCache from '../src';
import fs from 'node:fs/promises';
import { resolve } from 'node:path';

const isCached = async (path: string) => {
try {
await fs.stat(path);
return fs.readdir(path).then((files) => Promise.all(files.map((file) => fs.readFile(resolve(path, file), 'utf-8').then(JSON.parse).then(({ data }) => !!data))).then((r) => r.some(Boolean)));
} catch (e) {
return false;
}
};

const run = async (config?: InlineConfig) => startVitest('test', undefined, {
watch: false,
}, {
plugins: [vCache({
dir: '__test__/.cache',
})],
test: {
include: ['__test__/tests/*.mock.ts'],
reporters: [{}],
...config,
},
}).then((vitest) => vitest.close().then(() => vitest));

describe('v-cache', () => {
beforeAll(async () => {
if (await fs.stat('__test__/.cache').catch(() => null)) {
await fs.rm('__test__/.cache', { recursive: true });
}
await fs.readdir('__test__/tests/deep/nested/dependency/variables').then(
(files) => Promise.all(
files.map(
(file) => fs.rm(resolve('__test__/tests/deep/nested/dependency/variables', file)),
),
),
);
});

it('should cache passing tests', {
timeout: 10000,
}, async () => {
await run();

expect(await isCached('__test__/.cache/pass0.mock.ts')).toBe(true);
expect(await isCached('__test__/.cache/pass1.mock.ts')).toBe(true);
expect(await isCached('__test__/.cache/variable.mock.ts')).toBe(true);
expect(await isCached('__test__/.cache/fail0.mock.ts')).toBe(false);
expect(await isCached('__test__/.cache/fail1.mock.ts')).toBe(false);
});

it('should restore cached tests from cache', async () => {
const vitest = await run();

const files = [...vitest.state.filesMap.values()].flatMap((files) => files);

for (const file of files) {
if (file.result.state === 'pass') {
expect(file).toHaveProperty('cache', true);
} else {
expect(file).not.toHaveProperty('cache');
}
}
});

it('should rerun only the tests affected by change', async () => {
await run();

const timestamp = Date.now();
await fs.writeFile(`__test__/tests/deep/nested/dependency/variables/${timestamp}.ts`, `export default '${timestamp}';`);

const vitest = await run();

const files = Object.fromEntries([...vitest.state.filesMap.values()].flatMap((files) => files).map((file) => [file.filepath.match(/__test__.*$/)?.[0], file]));

expect(files['__test__/tests/variable.mock.ts']).not.toHaveProperty('cache');
expect(files['__test__/tests/pass0.mock.ts']).toHaveProperty('cache', true);
expect(files['__test__/tests/pass1.mock.ts']).toHaveProperty('cache', true);
expect(files['__test__/tests/fail0.mock.ts']).not.toHaveProperty('cache');
expect(files['__test__/tests/fail1.mock.ts']).not.toHaveProperty('cache');
});
});
3 changes: 3 additions & 0 deletions __test__/tests/deep/deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { nested } from './nested';

export const deep = `deep/${nested}`;
1 change: 1 addition & 0 deletions __test__/tests/deep/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './deep'
1 change: 1 addition & 0 deletions __test__/tests/deep/nested/dependency/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './variable'
5 changes: 5 additions & 0 deletions __test__/tests/deep/nested/dependency/variable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const variable = JSON.stringify(
import.meta.glob<{ default: string }>('./variables/*.ts', {
eager: true,
}),
);
1 change: 1 addition & 0 deletions __test__/tests/deep/nested/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './nested';
3 changes: 3 additions & 0 deletions __test__/tests/deep/nested/nested.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { variable } from './dependency';

export const nested = `nested/${variable}`
8 changes: 8 additions & 0 deletions __test__/tests/fail0.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';


describe('fails 0', () => {
it('should fail', async () => {
expect(true).toBe(false);
});
});
11 changes: 11 additions & 0 deletions __test__/tests/fail1.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';


describe('fails 1', () => {
it('should pass', async () => {
expect(true).toBe(true);
});
it('should fail', async () => {
expect(true).toBe(false);
});
});
8 changes: 8 additions & 0 deletions __test__/tests/pass0.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';


describe('passes 0', () => {
it('should pass', async () => {
expect(true).toBe(true);
});
});
8 changes: 8 additions & 0 deletions __test__/tests/pass1.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';


describe('passes 1', () => {
it('should pass', async () => {
expect(true).toBe(true);
});
});
9 changes: 9 additions & 0 deletions __test__/tests/variable.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest';
import { deep } from './deep';


describe('passes 2', () => {
it('should pass', async () => {
expect(deep).toBe(deep);
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@raegen/vite-plugin-vitest-cache",
"version": "0.3.0",
"version": "0.3.1",
"description": "Run Vitest with test result caching.",
"repository": {
"type": "git",
Expand All @@ -13,7 +13,7 @@
"license": "MIT",
"scripts": {
"build": "tsc",
"test": "echo tests"
"test": "vitest"
},
"bugs": {
"url": "https://github.com/raegen/vitest-cache/issues"
Expand Down
15 changes: 3 additions & 12 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'node:fs/promises';
import { deserialize, serialize, SerializedRecord } from '@ungap/structured-clone';
import { inject } from 'vitest';

export interface CacheEntry {
data: SerializedRecord;
Expand All @@ -9,18 +8,10 @@ export interface CacheEntry {
timestamp: number;
}

declare module 'vitest' {
export interface ProvidedContext {
'v-cache': {
[key: string]: CacheEntry;
};
}
}

export class TaskCache<T extends { cache?: boolean }> {
store = inject('v-cache');

constructor(flag?: (cache: T) => T & { cache: true }) {
constructor(readonly store: { [key: string]: CacheEntry }, { flag }: {
flag?: (cache: T) => T & { cache: true }
} = {}) {
if (flag) {
this.flag = flag;
}
Expand Down
21 changes: 8 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Plugin } from 'vite';
import type { InlineConfig, TaskState } from 'vitest';
import type { InlineConfig } from 'vitest';
import { File } from '@vitest/runner';
import { here } from './util.js';
import type { CacheStrategy } from './strategy.js';
import { CacheOptions } from './options.js';

declare module 'vite' {
export interface UserConfig {
Expand Down Expand Up @@ -36,16 +36,7 @@ const defaults = {
silent: false,
};

export interface CacheOptions {
/* location for the caches, relative to the project root */
dir?: string; // default: '.tests'
/* default: ['pass'] - which task states (test outcomes) should be cached */
states?: TaskState[]; // default: ['pass'] by default only passing tests are cached, failing tests are always rerun
silent?: boolean; // default: false - if true, vCache will not write anything to stdout
strategy?: CacheStrategy;
}

export const vitestCache = (options?: CacheOptions): Plugin => ({
export const vCache = (options?: CacheOptions): Plugin => ({
name: 'vitest-cache',
config: async () => ({
test: {
Expand All @@ -59,4 +50,8 @@ export const vitestCache = (options?: CacheOptions): Plugin => ({
}),
});

export default vitestCache;
export type {
CacheOptions,
};

export default vCache;
39 changes: 25 additions & 14 deletions src/load.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import path from 'node:path';
import { OutputAsset, PluginContext, RollupOutput } from 'rollup';
import { OutputAsset, RollupOutput } from 'rollup';
import { build } from 'vite';
import type { ResolvedConfig } from 'vitest';
import { here } from './util.js';
import type { CacheEntry } from './cache.js';

export const load = (files: string[], config: ResolvedConfig) => {
const dir = path.resolve(config.vCache.dir);
const mapOutput = (output: RollupOutput['output']): {
[k: string]: CacheEntry;
} =>
Object.fromEntries(
output
.filter(
(entry): entry is OutputAsset => entry.type === 'asset',
)
.map(
({
name,
source,
}) => [name, JSON.parse(`${source}`)],
),
);

return build({
configFile: here('./tests.vite.config'),
build: {
outDir: config.vCache.dir,
rollupOptions: {
input: files,
},
export const load = (files: string[], dir: string) => build({
configFile: here('./tests.vite.config'),
build: {
outDir: dir,
rollupOptions: {
input: files,
},
}).then(({ output }: RollupOutput) => Object.fromEntries(output.filter((entry): entry is OutputAsset => entry.type === 'asset').map((cache) => [path.resolve(dir, path.dirname(cache.fileName)), JSON.parse(`${cache.source}`)])));
};
},
}).then(({ output }: RollupOutput) => mapOutput(output))
11 changes: 11 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { TaskState } from 'vitest';
import type { CacheStrategy } from './strategy.js';

export interface CacheOptions {
/* location for the caches, relative to the project root */
dir?: string; // default: '.tests'
/* default: ['pass'] - which task states (test outcomes) should be cached */
states?: TaskState[]; // default: ['pass'] by default only passing tests are cached, failing tests are always rerun
silent?: boolean; // default: false - if true, vCache will not write anything to stdout
strategy?: CacheStrategy;
}
20 changes: 16 additions & 4 deletions src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { File, Task, updateTask, VitestRunner } from '@vitest/runner';
import { VitestTestRunner } from 'vitest/runners';
import { TaskCache } from './cache.js';
import { TaskCache, CacheEntry } from './cache.js';
import { format } from './util.js';
import { inject } from 'vitest';
import { CacheOptions } from './options.js';

declare module 'vitest' {
export interface ProvidedContext {
'v-cache:data': {
[key: string]: CacheEntry;
};
'v-cache:config': Omit<CacheOptions, 'strategy'>
}
}

class CachedRunner extends VitestTestRunner implements VitestRunner {
private cache = new TaskCache<File>();
private cache = new TaskCache<File>(inject('v-cache:data'));
private options = inject('v-cache:config');

shouldCache(task: Task): boolean {
return this.config.vCache.states.includes(task.result.state);
return this.options.states.includes(task.result.state);
}

shouldLog() {
return !this.config.vCache.silent;
return !this.options.silent;
}

async onBeforeCollect(paths: string[]) {
Expand Down
Loading

0 comments on commit a60224f

Please sign in to comment.