@application-kit/testing 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @application-kit/testing
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 48e6aec: Вход `/vitest` — раскладка ячеек CI по проектам vitest: `ProjectShardSequencer` (первая ячейка — проект client целиком, остальные — проект server LPT по размеру спек, чтобы объединение blob-отчётов покрытия двух трансформов оставалось точным), `parseShard` для `--shard=i/n` из argv и `assignShard`; `vitest` — optional peer этого входа.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @application-kit/testing
2
2
 
3
3
  Тестовая инфраструктура NestJS-приложений: стабы шин CQRS, логгер-накопитель, фейковый microservice-транспорт,
4
- сборка web-приложения для e2e и Postgres в testcontainers. Для `devDependencies` приложения, в прод не попадает.
4
+ сборка web-приложения для e2e, Postgres в testcontainers и раскладка ячеек CI по проектам vitest (вход `/vitest`).
5
+ Для `devDependencies` приложения, в прод не попадает.
5
6
 
6
7
  ## Установка
7
8
 
@@ -11,7 +12,7 @@ yarn add --dev @application-kit/testing
11
12
 
12
13
  Peer-зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`,
13
14
  `@nestjs/microservices`, `@nestjs/platform-fastify`, `@nestjs/testing`, `nestjs-pino`, `oxide.ts`, `rxjs`,
14
- `testcontainers`, `typeorm`. Все обязательные: подпутей и опциональных peers нет.
15
+ `testcontainers`, `typeorm` — обязательные для корневого входа. Вход `/vitest` несёт свой optional peer `vitest`.
15
16
 
16
17
  ## Состав
17
18
 
@@ -32,3 +33,33 @@ const { dataSource } = await testDataSource({
32
33
  namingStrategy: new TypeormPluralNamingStrategy(),
33
34
  })
34
35
  ```
36
+
37
+ ## Вход `/vitest`: ячейки CI по проектам
38
+
39
+ `vitest --shard=i/n` сам режет спеки хешем, и при двух проектах разных трансформов (`server` в `node`, `client` в
40
+ `jsdom`) объединение blob-отчётов покрытия неточное: непокрытые файлы одна ячейка синтезирует ssr-картой, а другая даёт
41
+ web-карту того же файла, и statements/functions задваиваются. `ProjectShardSequencer` отдаёт первой ячейке проект
42
+ `client` целиком, остальным — проект `server` по размеру спек (LPT, одна и та же раскладка в каждой ячейке без обмена
43
+ данными). `parseShard(process.argv)` читает `--shard=i/n` и `--shard i/n`; без флага, с одной ячейкой или с мусором в
44
+ значении — `undefined`, то есть обычный прогон (и `--merge-reports`, и Stryker). Каталоги проекта `client` и выбор
45
+ ячейки — у приложения:
46
+
47
+ ```ts
48
+ import { parseShard, ProjectShardSequencer } from '@application-kit/testing/vitest'
49
+
50
+ // Исходники, которые загружает только проект client: в ячейках server они не считаются вовсе.
51
+ const CLIENT_SOURCES = ['src/modules/*/user-interface/client/**']
52
+ const shard = parseShard(process.argv)
53
+ const cell = shard === undefined ? 'all' : shard.index === 1 ? 'client' : 'server'
54
+
55
+ export default defineConfig({
56
+ test: {
57
+ coverage: {
58
+ exclude: cell === 'server' ? CLIENT_SOURCES : [],
59
+ include: cell === 'client' ? CLIENT_SOURCES : ['src'],
60
+ },
61
+ projects: [serverProject, clientProject].filter((project) => cell === 'all' || project.test.name === cell),
62
+ sequence: { sequencer: ProjectShardSequencer },
63
+ },
64
+ })
65
+ ```
@@ -0,0 +1 @@
1
+ export * from './project-shard-sequencer.js';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./project-shard-sequencer.js"), exports);
@@ -0,0 +1,15 @@
1
+ import type { TestSpecification } from 'vitest/node';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ export interface ShardInterface {
4
+ readonly count: number;
5
+ readonly index: number;
6
+ }
7
+ export interface WeightedSpecInterface {
8
+ readonly path: string;
9
+ readonly weight: number;
10
+ }
11
+ export declare const parseShard: (argv: readonly string[]) => ShardInterface | undefined;
12
+ export declare const assignShard: (specs: readonly WeightedSpecInterface[], index: number, count: number) => string[];
13
+ export declare class ProjectShardSequencer extends BaseSequencer {
14
+ shard(files: TestSpecification[]): Promise<TestSpecification[]>;
15
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProjectShardSequencer = exports.assignShard = exports.parseShard = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_1 = require("vitest/node");
6
+ const SHARD_FLAG = '--shard';
7
+ const shardArgument = (argv) => {
8
+ const position = argv.findIndex((argument) => argument === SHARD_FLAG || argument.startsWith(`${SHARD_FLAG}=`));
9
+ const flag = argv[position];
10
+ if (flag === undefined) {
11
+ return undefined;
12
+ }
13
+ return flag.includes('=') ? flag.slice(SHARD_FLAG.length + 1) : argv[position + 1];
14
+ };
15
+ const isCell = (value) => Number.isSafeInteger(value) && (value ?? 0) > 0;
16
+ const isRange = (index, count) => count >= 2 && index <= count;
17
+ const toShard = (value) => {
18
+ const [index, count] = value.split('/').map(Number);
19
+ return isCell(index) && isCell(count) && isRange(index, count) ? { count, index } : undefined;
20
+ };
21
+ const parseShard = (argv) => {
22
+ const value = shardArgument(argv);
23
+ return value === undefined ? undefined : toShard(value);
24
+ };
25
+ exports.parseShard = parseShard;
26
+ const takeLightest = (weights, weight) => {
27
+ const lightest = weights.indexOf(Math.min(...weights));
28
+ weights[lightest] = (weights[lightest] ?? 0) + weight;
29
+ return lightest + 1;
30
+ };
31
+ const assignShard = (specs, index, count) => {
32
+ const weights = Array.from({ length: count }, () => 0);
33
+ const ordered = specs.toSorted((left, right) => right.weight - left.weight || left.path.localeCompare(right.path));
34
+ const placed = ordered.map((spec) => ({ path: spec.path, shard: takeLightest(weights, spec.weight) }));
35
+ return placed.filter((spec) => spec.shard === index).map((spec) => spec.path);
36
+ };
37
+ exports.assignShard = assignShard;
38
+ class ProjectShardSequencer extends node_1.BaseSequencer {
39
+ async shard(files) {
40
+ const { shard } = this.ctx.config;
41
+ if (shard === undefined || shard.index === 1) {
42
+ return files;
43
+ }
44
+ const specs = await Promise.all(files.map(async (file) => {
45
+ const { size } = await (0, promises_1.stat)(file.moduleId);
46
+ return { path: file.moduleId, weight: size };
47
+ }));
48
+ const assigned = new Set((0, exports.assignShard)(specs, shard.index - 1, shard.count - 1));
49
+ return files.filter((file) => assigned.has(file.moduleId));
50
+ }
51
+ }
52
+ exports.ProjectShardSequencer = ProjectShardSequencer;
@@ -0,0 +1 @@
1
+ export * from './project-shard-sequencer.js';
@@ -0,0 +1 @@
1
+ export * from './project-shard-sequencer.js';
@@ -0,0 +1,15 @@
1
+ import type { TestSpecification } from 'vitest/node';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ export interface ShardInterface {
4
+ readonly count: number;
5
+ readonly index: number;
6
+ }
7
+ export interface WeightedSpecInterface {
8
+ readonly path: string;
9
+ readonly weight: number;
10
+ }
11
+ export declare const parseShard: (argv: readonly string[]) => ShardInterface | undefined;
12
+ export declare const assignShard: (specs: readonly WeightedSpecInterface[], index: number, count: number) => string[];
13
+ export declare class ProjectShardSequencer extends BaseSequencer {
14
+ shard(files: TestSpecification[]): Promise<TestSpecification[]>;
15
+ }
@@ -0,0 +1,46 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { BaseSequencer } from 'vitest/node';
3
+ const SHARD_FLAG = '--shard';
4
+ const shardArgument = (argv) => {
5
+ const position = argv.findIndex((argument) => argument === SHARD_FLAG || argument.startsWith(`${SHARD_FLAG}=`));
6
+ const flag = argv[position];
7
+ if (flag === undefined) {
8
+ return undefined;
9
+ }
10
+ return flag.includes('=') ? flag.slice(SHARD_FLAG.length + 1) : argv[position + 1];
11
+ };
12
+ const isCell = (value) => Number.isSafeInteger(value) && (value ?? 0) > 0;
13
+ const isRange = (index, count) => count >= 2 && index <= count;
14
+ const toShard = (value) => {
15
+ const [index, count] = value.split('/').map(Number);
16
+ return isCell(index) && isCell(count) && isRange(index, count) ? { count, index } : undefined;
17
+ };
18
+ export const parseShard = (argv) => {
19
+ const value = shardArgument(argv);
20
+ return value === undefined ? undefined : toShard(value);
21
+ };
22
+ const takeLightest = (weights, weight) => {
23
+ const lightest = weights.indexOf(Math.min(...weights));
24
+ weights[lightest] = (weights[lightest] ?? 0) + weight;
25
+ return lightest + 1;
26
+ };
27
+ export const assignShard = (specs, index, count) => {
28
+ const weights = Array.from({ length: count }, () => 0);
29
+ const ordered = specs.toSorted((left, right) => right.weight - left.weight || left.path.localeCompare(right.path));
30
+ const placed = ordered.map((spec) => ({ path: spec.path, shard: takeLightest(weights, spec.weight) }));
31
+ return placed.filter((spec) => spec.shard === index).map((spec) => spec.path);
32
+ };
33
+ export class ProjectShardSequencer extends BaseSequencer {
34
+ async shard(files) {
35
+ const { shard } = this.ctx.config;
36
+ if (shard === undefined || shard.index === 1) {
37
+ return files;
38
+ }
39
+ const specs = await Promise.all(files.map(async (file) => {
40
+ const { size } = await stat(file.moduleId);
41
+ return { path: file.moduleId, weight: size };
42
+ }));
43
+ const assigned = new Set(assignShard(specs, shard.index - 1, shard.count - 1));
44
+ return files.filter((file) => assigned.has(file.moduleId));
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@application-kit/testing",
3
- "version": "0.1.0",
4
- "description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения и Postgres в testcontainers",
3
+ "version": "0.2.0",
4
+ "description": "Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers и раскладка ячеек CI по проектам vitest",
5
5
  "keywords": [
6
6
  "cqrs",
7
7
  "nestjs",
8
8
  "testcontainers",
9
- "testing"
9
+ "testing",
10
+ "vitest"
10
11
  ],
11
12
  "homepage": "https://gitlab.com/application-kit/application-kit/-/tree/main/packages/testing",
12
13
  "bugs": "https://gitlab.com/application-kit/application-kit/-/issues",
@@ -32,7 +33,17 @@
32
33
  "default": "./dist/cjs/index.js"
33
34
  }
34
35
  },
35
- "./package.json": "./package.json"
36
+ "./package.json": "./package.json",
37
+ "./vitest": {
38
+ "import": {
39
+ "types": "./dist/esm/vitest/index.d.ts",
40
+ "default": "./dist/esm/vitest/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/cjs/vitest/index.d.ts",
44
+ "default": "./dist/cjs/vitest/index.js"
45
+ }
46
+ }
36
47
  },
37
48
  "main": "./dist/cjs/index.js",
38
49
  "module": "./dist/esm/index.js",
@@ -41,7 +52,7 @@
41
52
  "dist"
42
53
  ],
43
54
  "scripts": {
44
- "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
55
+ "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=vitest && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
45
56
  "lint:tsc": "tsc --noEmit --pretty --project .",
46
57
  "test:mutation": "stryker run",
47
58
  "test:unit": "vitest run"
@@ -78,7 +89,13 @@
78
89
  "oxide.ts": "^1.1.0",
79
90
  "rxjs": "^7.8.2",
80
91
  "testcontainers": "^12.1.0",
81
- "typeorm": "^1.0.0"
92
+ "typeorm": "^1.0.0",
93
+ "vitest": "^4.1.0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "vitest": {
97
+ "optional": true
98
+ }
82
99
  },
83
100
  "engines": {
84
101
  "node": ">=24"