├── .tool-versions ├── .npmrc ├── pnpm-workspace.yaml ├── assets └── tenbin-abstract.png ├── packages ├── core │ ├── README.md │ ├── vitest.config.mts │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── index.test.ts │ │ └── index.ts ├── jest │ ├── src │ │ ├── utils.ts │ │ ├── reporter.ts │ │ └── sequencer.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── vitest │ ├── src │ │ ├── utils.ts │ │ ├── reporter.ts │ │ └── sequencer.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md └── playwright │ ├── tsconfig.json │ ├── package.json │ ├── src │ └── index.ts │ └── README.md ├── examples ├── jest │ ├── tests │ │ ├── helper.ts │ │ ├── 1s-1.test.ts │ │ ├── 1s-2.test.ts │ │ ├── 1s-3.test.ts │ │ ├── 1s-4.test.ts │ │ ├── 1s-5.test.ts │ │ ├── 1s-6.test.ts │ │ ├── 1s-7.test.ts │ │ ├── 1s-8.test.ts │ │ ├── 1s-9.test.ts │ │ ├── 2s-1.test.ts │ │ ├── 2s-2.test.ts │ │ ├── 2s-3.test.ts │ │ ├── 4s-1.test.ts │ │ ├── 1s-10.test.ts │ │ ├── 6s-2.test.ts │ │ ├── 4s-2.test.ts │ │ ├── 2s-4.test.ts │ │ ├── 8s-1.test.ts │ │ ├── 2s-5.test.ts │ │ └── 6s-1.test.ts │ ├── jest.config.js │ ├── jest-with-tenbin.config.js │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── playwright │ ├── tests │ │ ├── helper.ts │ │ ├── 1s-1.test.ts │ │ ├── 1s-2.test.ts │ │ ├── 1s-3.test.ts │ │ ├── 1s-4.test.ts │ │ ├── 1s-5.test.ts │ │ ├── 1s-6.test.ts │ │ ├── 1s-7.test.ts │ │ ├── 1s-8.test.ts │ │ ├── 1s-9.test.ts │ │ ├── 2s-1.test.ts │ │ ├── 2s-2.test.ts │ │ ├── 2s-3.test.ts │ │ ├── 4s-1.test.ts │ │ ├── 1s-10.test.ts │ │ ├── 6s-2.test.ts │ │ ├── 4s-2.test.ts │ │ ├── 2s-4.test.ts │ │ ├── 8s-1.test.ts │ │ ├── 2s-5.test.ts │ │ └── 6s-1.test.ts │ ├── playwright.config.js │ ├── package.json │ ├── tsconfig.json │ ├── README.md │ └── playwright-with-tenbin.config.js └── vitest │ ├── tests │ ├── helper.ts │ ├── 1s-1.test.ts │ ├── 1s-10.test.ts │ ├── 1s-2.test.ts │ ├── 1s-3.test.ts │ ├── 1s-4.test.ts │ ├── 1s-5.test.ts │ ├── 1s-6.test.ts │ ├── 1s-7.test.ts │ ├── 1s-8.test.ts │ ├── 1s-9.test.ts │ ├── 2s-1.test.ts │ ├── 2s-2.test.ts │ ├── 2s-3.test.ts │ ├── 4s-1.test.ts │ ├── 6s-2.test.ts │ ├── 4s-2.test.ts │ ├── 2s-4.test.ts │ ├── 8s-1.test.ts │ ├── 2s-5.test.ts │ └── 6s-1.test.ts │ ├── vitest.config.mts │ ├── package.json │ ├── tsconfig.json │ ├── README.md │ └── vitest-with-tenbin.config.mts ├── scripts └── release.sh ├── biome.jsonc ├── package.json ├── .github ├── workflows │ ├── main.yaml │ ├── release-drafter.yaml │ ├── release.yaml │ ├── example-jest.yaml │ ├── example-vitest.yaml │ └── example-playwright.yaml ├── actions │ └── setup │ │ └── action.yaml └── release-drafter.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md └── .gitignore /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.12.2 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/**" 4 | -------------------------------------------------------------------------------- /assets/tenbin-abstract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nissy-dev/tenbin/HEAD/assets/tenbin-abstract.png -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/core 2 | 3 | `@tenbin/core` provides core algorithms for sharding. 4 | -------------------------------------------------------------------------------- /packages/core/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /examples/jest/tests/helper.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (second: number) => 2 | new Promise((resolve) => setTimeout(resolve, 1000 * second)); 3 | -------------------------------------------------------------------------------- /examples/playwright/tests/helper.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (second: number) => 2 | new Promise((resolve) => setTimeout(resolve, 1000 * second)); 3 | -------------------------------------------------------------------------------- /examples/vitest/tests/helper.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (second: number) => 2 | new Promise((resolve) => setTimeout(resolve, 1000 * second)); 3 | -------------------------------------------------------------------------------- /examples/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("jest").Config} */ 2 | module.exports = { 3 | transform: { 4 | "^.+\\.(t|j)sx?$": "@swc/jest", 5 | }, 6 | maxWorkers: 1, 7 | }; 8 | -------------------------------------------------------------------------------- /examples/vitest/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | fileParallelism: false, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-1", () => { 4 | it("2s", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-2", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-3", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-4", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-5", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-6.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-6", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-7.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-7", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-8.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-8", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-9.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-9", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/2s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-1", () => { 4 | it("2s", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/2s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-2", () => { 4 | it("2s", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/2s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-3", () => { 4 | it("2s", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/4s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-4s-1", () => { 4 | it("4s", async () => { 5 | await sleep(4); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/jest/tests/1s-10.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-1s-10", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/jest/src/utils.ts: -------------------------------------------------------------------------------- 1 | import pc from "picocolors"; 2 | 3 | export const REPORT_FILENAME = "tenbin-report.json"; 4 | 5 | export const logger = (log: string) => 6 | console.log(pc.gray(`\n[tenbin]: ${log}`)); 7 | -------------------------------------------------------------------------------- /packages/vitest/src/utils.ts: -------------------------------------------------------------------------------- 1 | import pc from "picocolors"; 2 | 3 | export const REPORT_FILENAME = "tenbin-report.json"; 4 | 5 | export const logger = (log: string) => 6 | console.log(pc.gray(`\n[tenbin]: ${log}`)); 7 | -------------------------------------------------------------------------------- /examples/playwright/playwright.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | fullyParallel: false, 5 | workers: 1, 6 | testMatch: ["tests/**.test.ts"], 7 | reporter: [["blob"]], 8 | }); 9 | -------------------------------------------------------------------------------- /examples/vitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/vitest-example", 3 | "private": true, 4 | "scripts": { 5 | "test": "vitest" 6 | }, 7 | "devDependencies": { 8 | "@tenbin/vitest": "workspace:*", 9 | "vitest": "2.1.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-1", () => { 5 | it("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-10.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-10", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-2", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-3", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-4", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-5", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-6.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-6", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-7.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-7", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-8.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-8", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/1s-9.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-1s-9", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/2s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-1", () => { 5 | it("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/2s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-2", () => { 5 | it("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/2s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-3", () => { 5 | it("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vitest/tests/4s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-4s-1", () => { 5 | it("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-1", () => { 5 | test("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-2", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-3", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-4", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-5", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-6.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-6", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-7.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-7", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-8.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-8", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-9.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-9", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/2s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-1", () => { 5 | test("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/2s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-2", () => { 5 | test("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/2s-3.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-3", () => { 5 | test("2s", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/4s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-4s-1", () => { 5 | test("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/tests/1s-10.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-1s-10", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/playwright-example", 3 | "private": true, 4 | "scripts": { 5 | "test": "playwright" 6 | }, 7 | "devDependencies": { 8 | "@playwright/test": "1.48.1", 9 | "@tenbin/playwright": "workspace:*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/jest/jest-with-tenbin.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("jest").Config} */ 2 | module.exports = { 3 | transform: { 4 | "^.+\\.(t|j)sx?$": "@swc/jest", 5 | }, 6 | maxWorkers: 1, 7 | testSequencer: "@tenbin/jest/sequencer", 8 | reporters: ["default", "@tenbin/jest/reporter"], 9 | }; 10 | -------------------------------------------------------------------------------- /examples/jest/tests/6s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-6s-2", () => { 4 | it("4s", async () => { 5 | await sleep(4); 6 | expect(1).toBe(1); 7 | }); 8 | 9 | it("2s", async () => { 10 | await sleep(2); 11 | expect(1).toBe(1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/jest/tests/4s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-4s-2", () => { 4 | it("2s-a", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | 9 | it("2s-b", async () => { 10 | await sleep(2); 11 | expect(1).toBe(1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["tests"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/vitest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["tests"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": true, 8 | "noEmit": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": true, 8 | "noEmit": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/playwright/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["tests"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/playwright/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": true, 8 | "noEmit": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vitest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": true, 8 | "noEmit": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/jest/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/jest-example 2 | 3 | ## Result 4 | 5 | | shard | default | use tenbin | 6 | | ----- | ------- | -----------| 7 | | 1/3 | 26s | 20s | 8 | | 2/3 | 18s | 19s | 9 | | 3/3 | 16s | 20s | 10 | 11 | see: https://github.com/nissy-dev/tenbin/actions/workflows/example-jest.yaml 12 | -------------------------------------------------------------------------------- /examples/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/jest-example", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "@swc/core": "1.7.26", 9 | "@swc/jest": "0.2.36", 10 | "@tenbin/jest": "workspace:*", 11 | "@types/jest": "29.5.13", 12 | "jest": "29.7.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vitest/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/vitest-example 2 | 3 | ## Result 4 | 5 | | shard | default | use tenbin | 6 | | ----- | ------- | -----------| 7 | | 1/3 | 23s | 19s | 8 | | 2/3 | 19s | 19s | 9 | | 3/3 | 15s | 20s | 10 | 11 | see: https://github.com/nissy-dev/tenbin/actions/workflows/example-vitest.yaml 12 | -------------------------------------------------------------------------------- /examples/jest/tests/2s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-4-a", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | 10 | describe("test-2s-4-b", () => { 11 | it("1s", async () => { 12 | await sleep(1); 13 | expect(1).toBe(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/jest/tests/8s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-8s-1-a", () => { 4 | it("4s", async () => { 5 | await sleep(4); 6 | expect(1).toBe(1); 7 | }); 8 | }); 9 | 10 | describe("test-8s-1-b", () => { 11 | it("4s", async () => { 12 | await sleep(4); 13 | expect(1).toBe(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/vitest/tests/6s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-6s-2", () => { 5 | it("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | it("2s", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/jest/tests/2s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-2s-5-a", () => { 4 | it("1s", async () => { 5 | await sleep(1); 6 | expect(1).toBe(1); 7 | }); 8 | 9 | describe("test-2s-5-b", () => { 10 | it("1s", async () => { 11 | await sleep(1); 12 | expect(1).toBe(1); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/playwright/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/playwright-example 2 | 3 | ## Result 4 | 5 | | shard | default | use tenbin | 6 | | ----- | ------- | -----------| 7 | | 1/3 | 12s | 17s | 8 | | 2/3 | 17s | 17s | 9 | | 3/3 | 26s | 19s | 10 | 11 | see: https://github.com/nissy-dev/tenbin/actions/workflows/example-playwright.yaml 12 | -------------------------------------------------------------------------------- /examples/vitest/tests/4s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-4s-2", () => { 5 | it("2s-a", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | it("2s-b", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/playwright/tests/6s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-6s-2", () => { 5 | test("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | test("2s", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/playwright/tests/4s-2.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-4s-2", () => { 5 | test("2s-a", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | test("2s-b", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # -u: Fail on when existing unset variables 3 | # -e -o pipefail: Fail on when happening command errors 4 | set -ueo pipefail 5 | 6 | # Release packages 7 | for PKG in packages/* ; do 8 | pushd $PKG 9 | echo "Publishing $PKG ..." 10 | cp ../../LICENSE . 11 | pnpm publish --access public --no-git-checks 12 | popd > /dev/null 13 | done 14 | -------------------------------------------------------------------------------- /examples/vitest/tests/2s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-4-a", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | 11 | describe("test-2s-4-b", () => { 12 | it("1s", async () => { 13 | await sleep(1); 14 | expect(1).toBe(1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/vitest/tests/8s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-8s-1-a", () => { 5 | it("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | 11 | describe("test-8s-1-b", () => { 12 | it("4s", async () => { 13 | await sleep(4); 14 | expect(1).toBe(1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "files": { 4 | "ignore": ["dist/**"] 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "indentStyle": "space" 9 | }, 10 | "organizeImports": { 11 | "enabled": true 12 | }, 13 | "linter": { 14 | "enabled": true, 15 | "rules": { 16 | "recommended": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/jest/tests/6s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "./helper"; 2 | 3 | describe("test-6s-1", () => { 4 | it("2s-a", async () => { 5 | await sleep(2); 6 | expect(1).toBe(1); 7 | }); 8 | 9 | it("2s-b", async () => { 10 | await sleep(2); 11 | expect(1).toBe(1); 12 | }); 13 | 14 | it("2s-c", async () => { 15 | await sleep(2); 16 | expect(1).toBe(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tenbin", 3 | "scripts": { 4 | "dev": "pnpm -r dev", 5 | "build": "pnpm -r build", 6 | "lint": "biome ci", 7 | "test": "pnpm --filter './packages/**' test", 8 | "typecheck": "pnpm --filter './packages/**' typecheck" 9 | }, 10 | "devDependencies": { 11 | "@biomejs/biome": "1.9.2", 12 | "tsup": "8.3.0", 13 | "typescript": "5.6.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/vitest/tests/2s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-2s-5-a", () => { 5 | it("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | describe("test-2s-5-b", () => { 11 | it("1s", async () => { 12 | await sleep(1); 13 | expect(1).toBe(1); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/playwright/tests/2s-4.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-4-a", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | 11 | test.describe("test-2s-4-b", () => { 12 | test("1s", async () => { 13 | await sleep(1); 14 | expect(1).toBe(1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/playwright/tests/8s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-8s-1-a", () => { 5 | test("4s", async () => { 6 | await sleep(4); 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | 11 | test.describe("test-8s-1-b", () => { 12 | test("4s", async () => { 13 | await sleep(4); 14 | expect(1).toBe(1); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/vitest/vitest-with-tenbin.config.mts: -------------------------------------------------------------------------------- 1 | import TenbinReporter from "@tenbin/vitest/reporter"; 2 | import TenbinSequencer from "@tenbin/vitest/sequencer"; 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | fileParallelism: false, 8 | reporters: [new TenbinReporter()], 9 | sequence: { 10 | sequencer: TenbinSequencer, 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/playwright/tests/2s-5.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-2s-5-a", () => { 5 | test("1s", async () => { 6 | await sleep(1); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | test.describe("test-2s-5-b", () => { 11 | test("1s", async () => { 12 | await sleep(1); 13 | expect(1).toBe(1); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/vitest/tests/6s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { sleep } from "./helper"; 3 | 4 | describe("test-6s-1", () => { 5 | it("2s-a", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | it("2s-b", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | 15 | it("2s-c", async () => { 16 | await sleep(2); 17 | expect(1).toBe(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main workflow 2 | on: 3 | push: 4 | 5 | jobs: 6 | main: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: ./.github/actions/setup 11 | - name: Run build 12 | run: pnpm run build 13 | - name: Run lint 14 | run: pnpm run lint 15 | - name: Run typecheck 16 | run: pnpm run typecheck 17 | - name: Run test 18 | run: pnpm run test 19 | -------------------------------------------------------------------------------- /examples/playwright/tests/6s-1.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { sleep } from "./helper"; 3 | 4 | test.describe("test-6s-1", () => { 5 | test("2s-a", async () => { 6 | await sleep(2); 7 | expect(1).toBe(1); 8 | }); 9 | 10 | test("2s-b", async () => { 11 | await sleep(2); 12 | expect(1).toBe(1); 13 | }); 14 | 15 | test("2s-c", async () => { 16 | await sleep(2); 17 | expect(1).toBe(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/playwright/playwright-with-tenbin.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@playwright/test"; 2 | import { splitTests } from "@tenbin/playwright"; 3 | 4 | export default defineConfig({ 5 | fullyParallel: false, 6 | workers: 1, 7 | testMatch: splitTests({ 8 | shard: process.env.TENBIN_SHARD, 9 | pattern: ["tests/**.test.ts"], 10 | reportFile: "./test-results.json", 11 | }), 12 | reporter: [["blob", { fileName: process.env.REPORT_FILE_NAME }]], 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | name: release drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | update_release_draft: 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: release-drafter/release-drafter@v6 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Install pnpm 7 | uses: pnpm/action-setup@v4 8 | with: 9 | version: "9" 10 | - name: Use Node.js 11 | uses: actions/setup-node@v4 12 | with: 13 | registry-url: "https://registry.npmjs.org" 14 | node-version-file: ".tool-versions" 15 | cache: "pnpm" 16 | - name: Install dependencies 17 | shell: bash 18 | run: pnpm install 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[json]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "[jsonc]": { 13 | "editor.defaultFormatter": "biomejs.biome" 14 | }, 15 | "editor.codeActionsOnSave": { 16 | "quickfix.biome": "explicit", 17 | "source.organizeImports.biome": "explicit" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: ./.github/actions/setup 16 | - name: Run build 17 | run: pnpm run build 18 | - name: Publish 19 | run: ./scripts/release.sh 20 | # use trusted publish 21 | # env: 22 | # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | # NPM_CONFIG_PROVENANCE: true 24 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/core", 3 | "version": "0.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nissy-dev/tenbin.git", 7 | "directory": "packages/core" 8 | }, 9 | "license": "MIT", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.mjs", 14 | "require": "./dist/index.js" 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | 19 | "scripts": { 20 | "dev": "tsup src/index.ts --dts --format esm,cjs --watch", 21 | "build": "tsup src/index.ts --dts --format esm,cjs", 22 | "test": "vitest", 23 | "typecheck": "tsc" 24 | }, 25 | "devDependencies": { 26 | "vitest": "2.1.9" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/playwright", 3 | "version": "0.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nissy-dev/tenbin.git", 7 | "directory": "packages/playwright" 8 | }, 9 | "license": "MIT", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.mjs", 14 | "require": "./dist/index.js" 15 | }, 16 | "./package.json": "./package.json" 17 | }, 18 | "scripts": { 19 | "dev": "tsup src/index.ts --dts --format esm,cjs --watch", 20 | "build": "tsup src/index.ts --dts --format esm,cjs", 21 | "typecheck": "tsc" 22 | }, 23 | "dependencies": { 24 | "@tenbin/core": "workspace:*", 25 | "glob": "^11.0.0", 26 | "picocolors": "^1.1.0" 27 | }, 28 | "devDependencies": { 29 | "@playwright/test": "1.48.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/vitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/vitest", 3 | "version": "0.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nissy-dev/tenbin.git", 7 | "directory": "packages/vitest" 8 | }, 9 | "license": "MIT", 10 | "exports": { 11 | "./reporter": { 12 | "types": "./dist/reporter.d.ts", 13 | "import": "./dist/reporter.mjs", 14 | "require": "./dist/reporter.js" 15 | }, 16 | "./sequencer": { 17 | "types": "./dist/sequencer.d.ts", 18 | "import": "./dist/sequencer.mjs", 19 | "require": "./dist/sequencer.js" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "scripts": { 24 | "dev": "tsup --entry src/reporter.ts --entry src/sequencer.ts --dts --format esm,cjs --watch", 25 | "build": "tsup --entry src/reporter.ts --entry src/sequencer.ts --dts --format esm,cjs", 26 | "typecheck": "tsc" 27 | }, 28 | "dependencies": { 29 | "@tenbin/core": "workspace:*", 30 | "picocolors": "^1.1.0", 31 | "vitest": "^2.1.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daiki Nishikawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tenbin/jest", 3 | "version": "0.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/nissy-dev/tenbin.git", 7 | "directory": "packages/jest" 8 | }, 9 | "license": "MIT", 10 | "exports": { 11 | "./reporter": { 12 | "types": "./dist/reporter.d.ts", 13 | "import": "./dist/reporter.mjs", 14 | "require": "./dist/reporter.js" 15 | }, 16 | "./sequencer": { 17 | "types": "./dist/sequencer.d.ts", 18 | "import": "./dist/sequencer.mjs", 19 | "require": "./dist/sequencer.js" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "scripts": { 24 | "dev": "tsup --entry src/reporter.ts --entry src/sequencer.ts --dts --format esm,cjs --watch", 25 | "build": "tsup --entry src/reporter.ts --entry src/sequencer.ts --dts --format esm,cjs", 26 | "typecheck": "tsc" 27 | }, 28 | "dependencies": { 29 | "@jest/test-sequencer": "^29.7.0", 30 | "@tenbin/core": "workspace:*", 31 | "picocolors": "^1.1.0" 32 | }, 33 | "devDependencies": { 34 | "@jest/reporters": "29.7.0", 35 | "@jest/test-result": "29.7.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vitest/src/reporter.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import type { RunnerTestFile } from "vitest"; 4 | import { DefaultReporter } from "vitest/reporters"; 5 | import { REPORT_FILENAME, logger } from "./utils"; 6 | 7 | export default class TenbinReporter extends DefaultReporter { 8 | private durations: Record = {}; 9 | 10 | onFinished(files?: RunnerTestFile[]): void { 11 | if (!files) return; 12 | for (const file of files) { 13 | this.durations[file.name] = this.getDuration(file) / 1000; 14 | } 15 | const reportFilePath = path.join(process.cwd(), REPORT_FILENAME); 16 | try { 17 | fs.writeFileSync(reportFilePath, JSON.stringify(this.durations)); 18 | logger(`tenbin-report.json written to ${reportFilePath}`); 19 | } catch (err) { 20 | logger("Failed to generate tenbin-report.json"); 21 | console.error(err); 22 | } 23 | } 24 | 25 | private getDuration(file: RunnerTestFile): number { 26 | let duration = 0; 27 | for (const task of file.tasks) { 28 | duration += task.result?.duration ?? 0; 29 | } 30 | return duration; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { partition } from "./index"; 3 | 4 | const range = (start: number, end: number) => { 5 | const array: number[] = []; 6 | for (let i = start; i <= end; i++) { 7 | array.push(i); 8 | } 9 | return array; 10 | }; 11 | 12 | describe("partition", () => { 13 | test("number array", () => { 14 | expect(partition([4, 5, 6, 7, 8], 2)).toEqual([ 15 | [6, 8], 16 | [4, 5, 7], 17 | ]); 18 | expect(partition([4, 5, 6, 7, 8], 3)).toEqual([[8], [4, 7], [5, 6]]); 19 | expect(partition([5, 6, 4, 8, 7], 3)).toEqual([[8], [4, 7], [5, 6]]); 20 | expect(partition(range(10, 30), 3)).toEqual([ 21 | [10, 15, 16, 21, 22, 27, 28], // = 129 22 | [11, 14, 17, 20, 23, 26, 29], // = 130 23 | [12, 13, 18, 19, 24, 25, 30], // = 131 24 | ]); 25 | }); 26 | 27 | test("object array", () => { 28 | expect( 29 | partition( 30 | [{ value: 4 }, { value: 5 }, { value: 6 }, { value: 7 }, { value: 8 }], 31 | 2, 32 | (item) => item.value, 33 | ), 34 | ).toEqual([ 35 | [{ value: 6 }, { value: 8 }], 36 | [{ value: 4 }, { value: 5 }, { value: 7 }], 37 | ]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "Release v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | categories: 4 | - title: "🚀 Features" 5 | label: "feature" 6 | - title: "🐛 Bug Fixes" 7 | label: "bug" 8 | - title: "♻️ Refactor" 9 | label: "refactor" 10 | - title: "📝 Documentation" 11 | label: "documentation" 12 | - title: "🧰 Maintenance" 13 | labels: 14 | - "chore" 15 | - "dependencies" 16 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 17 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 18 | version-resolver: 19 | major: 20 | labels: 21 | - "major" 22 | minor: 23 | labels: 24 | - "minor" 25 | patch: 26 | labels: 27 | - "patch" 28 | default: patch 29 | template: | 30 | ## Changes 31 | 32 | $CHANGES 33 | autolabeler: 34 | - label: feature 35 | branch: 36 | - "/^feat(ure)?[/-].+/" 37 | - label: bug 38 | branch: 39 | - "/^fix[/-].+/" 40 | - label: refactor 41 | branch: 42 | - "/(refactor|refactoring)[/-].+/" 43 | - label: documentation 44 | branch: 45 | - "/doc(s|umentation)[/-].+/" 46 | - label: chore 47 | branch: 48 | - "/^chore[/-].+/" 49 | -------------------------------------------------------------------------------- /packages/jest/src/reporter.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import type { 4 | AggregatedResult, 5 | Reporter, 6 | Test, 7 | TestResult, 8 | } from "@jest/reporters"; 9 | import { REPORT_FILENAME, logger } from "./utils"; 10 | 11 | export default class TenbinReporter implements Reporter { 12 | private durations: Record = {}; 13 | 14 | onTestResult(test: Test, testResult: TestResult): void { 15 | const relativePath = path.relative(process.cwd(), test.path); 16 | this.durations[relativePath] = this.getDuration(test, testResult) / 1000; 17 | } 18 | 19 | onRunComplete(_: unknown, results: AggregatedResult): void { 20 | const reportFilePath = path.join(process.cwd(), REPORT_FILENAME); 21 | try { 22 | fs.writeFileSync(reportFilePath, JSON.stringify(this.durations)); 23 | logger(`tenbin-report.json written to ${reportFilePath}`); 24 | } catch (err) { 25 | logger("Failed to generate tenbin-report.json"); 26 | console.error(err); 27 | } 28 | } 29 | 30 | private getDuration(test: Test, testResult: TestResult): number { 31 | if (test.duration !== undefined) { 32 | return test.duration; 33 | } 34 | const { start, end } = testResult.perfStats; 35 | return start && end ? end - start : 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tenbin ⚖️ 2 | 3 | Tenbin provides tools to minimize the differences in test execution times across shards. 4 | 5 | ![Tenbin provides tools to minimize the differences in test execution times across shards.](./assets/tenbin-abstract.png) 6 | 7 | ## Usage 8 | 9 | - [Jest](./packages/jest/README.md) 10 | - [Vitest](./packages/vitest/README.md) 11 | - [Playwright](./packages/playwright/README.md) 12 | 13 | ## Why 14 | 15 | A typical test runner implements a sharding feature that splits tests to run on different machines. However, the sharding algorithm often randomly splits tests, leading to uneven execution times across shards. 16 | 17 | Tenbin provides tools to minimize the differences in execution time across shards. 18 | It uses the execution times of past test run when splitting tests. 19 | 20 | For example: 21 | 22 | | shard | default | use tenbin | 23 | | ----- | ------- | -----------| 24 | | 1/3 | 3min | 4min | 25 | | 2/3 | 5min | 4min | 26 | | 3/3 | 4min | 4min | 27 | 28 | The optimization of the sharding algorithm is considered in E2E testing tools where test execution time is a more critical issue. 29 | In Playwright, this is being discussed in [playwright#17969](https://github.com/microsoft/playwright/issues/17969), and [it seems that some implementation is in progress](https://github.com/microsoft/playwright/pull/30962). 30 | In Cypress, the ["load-balancing strategy"](https://docs.cypress.io/guides/cloud/smart-orchestration/load-balancing) is available. 31 | -------------------------------------------------------------------------------- /packages/jest/src/sequencer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import type { Test } from "@jest/test-result"; 4 | import Sequencer from "@jest/test-sequencer"; 5 | import type { ShardOptions } from "@jest/test-sequencer"; 6 | import { partition } from "@tenbin/core"; 7 | import { REPORT_FILENAME, logger } from "./utils"; 8 | 9 | // if value is 0, partition won't work correctly 10 | const FALLBACK_DURATION = 0.1; 11 | 12 | export default class TenbinSequencer extends Sequencer { 13 | private durations: Record = {}; 14 | 15 | constructor() { 16 | super(); 17 | this.durations = this.loadDurations(); 18 | } 19 | 20 | shard( 21 | tests: Array, 22 | options: ShardOptions, 23 | ): Array | Promise> { 24 | for (const test of tests) { 25 | const relativePath = path.relative(process.cwd(), test.path); 26 | test.duration = this.durations[relativePath] ?? FALLBACK_DURATION; 27 | } 28 | const partitions = partition( 29 | tests, 30 | options.shardCount, 31 | (test) => test.duration ?? FALLBACK_DURATION, 32 | ); 33 | return partitions[options.shardIndex - 1]; 34 | } 35 | 36 | private loadDurations(): Record { 37 | const filePath = path.join(process.cwd(), REPORT_FILENAME); 38 | try { 39 | const durations = JSON.parse(fs.readFileSync(filePath, "utf8")); 40 | logger("Load tenbin-report.json successfully"); 41 | return durations; 42 | } catch (_err) { 43 | logger(`Failed to load tenbin-report.json from ${filePath}`); 44 | return {}; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/vitest/src/sequencer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import { partition } from "@tenbin/core"; 4 | import { BaseSequencer, type Vitest, type WorkspaceSpec } from "vitest/node"; 5 | import { REPORT_FILENAME, logger } from "./utils"; 6 | 7 | // if value is 0, partition won't work correctly 8 | const FALLBACK_DURATION = 0.1; 9 | 10 | export default class TenbinSequencer extends BaseSequencer { 11 | private durations: Record = {}; 12 | 13 | constructor(ctx: Vitest) { 14 | super(ctx); 15 | this.durations = this.loadDurations(); 16 | } 17 | 18 | async shard(files: WorkspaceSpec[]): Promise { 19 | const { config } = this.ctx; 20 | if (!config.shard) { 21 | return files; 22 | } 23 | const filesWithDuration = files.map((file) => { 24 | const relativePath = path.relative(process.cwd(), file.moduleId); 25 | const duration = this.durations[relativePath] ?? FALLBACK_DURATION; 26 | return { file, duration: duration }; 27 | }); 28 | const partitions = partition( 29 | filesWithDuration, 30 | config.shard.count, 31 | (test) => test.duration ?? FALLBACK_DURATION, 32 | ); 33 | return partitions[config.shard.index - 1].map((test) => test.file); 34 | } 35 | 36 | private loadDurations(): Record { 37 | const filePath = path.join(process.cwd(), REPORT_FILENAME); 38 | try { 39 | const durations = JSON.parse(fs.readFileSync(filePath, "utf8")); 40 | logger("Load tenbin-report.json successfully"); 41 | return durations; 42 | } catch (err) { 43 | logger(`Failed to load tenbin-report.json from ${filePath}`); 44 | return {}; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/example-jest.yaml: -------------------------------------------------------------------------------- 1 | name: examples jest workflow 2 | on: 3 | push: 4 | 5 | jobs: 6 | default-test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | shard: [1/3, 2/3, 3/3] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - name: Run test 15 | run: pnpx jest --shard=${{ matrix.shard }} 16 | working-directory: examples/jest 17 | 18 | use-tenbin-jest: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | shardIndex: [1, 2, 3] 24 | shardTotal: [3] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ./.github/actions/setup 28 | - name: Run build 29 | run: pnpm run build 30 | - name: Restore tenbin-report.json 31 | id: tenbin-report-cache 32 | uses: actions/cache/restore@v4 33 | with: 34 | path: tenbin-report.json 35 | key: tenbin-report 36 | restore-keys: | 37 | tenbin-report-* 38 | - name: Copy tenbin-report.json to working directory 39 | run: | 40 | if [ -e ./tenbin-report.json ]; then 41 | jq . tenbin-report.json 42 | cp tenbin-report.json examples/jest 43 | fi 44 | - name: Run test 45 | run: pnpx jest --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} -c jest-with-tenbin.config.js 46 | working-directory: examples/jest 47 | - name: Upload tenbin-report.json 48 | if: github.ref_name == 'main' 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: tenbin-report-${{ matrix.shardIndex }} 52 | path: examples/jest/tenbin-report.json 53 | 54 | merge-and-cache-tenbin-report: 55 | if: github.ref_name == 'main' 56 | runs-on: ubuntu-latest 57 | needs: [use-tenbin-jest] 58 | steps: 59 | - uses: actions/download-artifact@v4 60 | with: 61 | path: tenbin-report 62 | pattern: tenbin-report-* 63 | - name: Merge tenbin-report 64 | run: jq -s add tenbin-report/**/tenbin-report.json > tenbin-report.json 65 | - name: Cache tenbin-report.json 66 | uses: actions/cache/save@v4 67 | with: 68 | path: tenbin-report.json 69 | key: tenbin-report-${{ github.run_id }} 70 | -------------------------------------------------------------------------------- /.github/workflows/example-vitest.yaml: -------------------------------------------------------------------------------- 1 | name: examples vitest workflow 2 | on: 3 | push: 4 | 5 | jobs: 6 | default-test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | shard: [1/3, 2/3, 3/3] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - name: Run test 15 | run: pnpm dlx vitest --shard=${{ matrix.shard }} 16 | working-directory: examples/vitest 17 | 18 | use-tenbin-vitest: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | shardIndex: [1, 2, 3] 24 | shardTotal: [3] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ./.github/actions/setup 28 | - name: Run build 29 | run: pnpm run build 30 | - name: Restore tenbin-report.json 31 | id: tenbin-report-cache 32 | uses: actions/cache/restore@v4 33 | with: 34 | path: tenbin-report.json 35 | key: tenbin-report 36 | restore-keys: | 37 | tenbin-report-* 38 | - name: Copy tenbin-report.json to working directory 39 | run: | 40 | if [ -e ./tenbin-report.json ]; then 41 | jq . tenbin-report.json 42 | cp tenbin-report.json examples/vitest 43 | fi 44 | - name: Run test 45 | run: pnpx vitest --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} -c vitest-with-tenbin.config.mts 46 | working-directory: examples/vitest 47 | - name: Upload tenbin-report.json 48 | if: github.ref_name == 'main' 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: tenbin-report-${{ matrix.shardIndex }} 52 | path: examples/vitest/tenbin-report.json 53 | 54 | merge-and-cache-tenbin-report: 55 | if: github.ref_name == 'main' 56 | runs-on: ubuntu-latest 57 | needs: [use-tenbin-vitest] 58 | steps: 59 | - uses: actions/download-artifact@v4 60 | with: 61 | path: tenbin-report 62 | pattern: tenbin-report-* 63 | - name: Merge tenbin-report 64 | run: jq -s add tenbin-report/**/tenbin-report.json > tenbin-report.json 65 | - name: Cache tenbin-report.json 66 | uses: actions/cache/save@v4 67 | with: 68 | path: tenbin-report.json 69 | key: tenbin-report-${{ github.run_id }} 70 | -------------------------------------------------------------------------------- /packages/playwright/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import type { JSONReport, JSONReportSuite } from "@playwright/test/reporter"; 4 | import { partition } from "@tenbin/core"; 5 | import { globSync } from "glob"; 6 | 7 | // if value is 0, partition won't work correctly 8 | const FALLBACK_DURATION = 0.1; 9 | 10 | type Config = { 11 | shard: string; 12 | reportFile: string; 13 | pattern: string[]; 14 | }; 15 | 16 | type Test = { 17 | duration?: number; 18 | path: string; 19 | }; 20 | 21 | export function splitTests(config: Config): string[] { 22 | const { shard, reportFile, pattern } = config; 23 | const tests: Test[] = globSync(pattern).map((path) => ({ path })); 24 | const { shardCount, shardIndex } = extractShardConfig(shard); 25 | const durations = loadDurations(reportFile); 26 | for (const test of tests) { 27 | test.duration = durations[test.path] ?? FALLBACK_DURATION; 28 | } 29 | const partitions = partition( 30 | tests, 31 | shardCount, 32 | (test) => test.duration ?? FALLBACK_DURATION, 33 | ); 34 | return partitions[shardIndex - 1].map((test) => test.path); 35 | } 36 | 37 | type ShardConfig = { 38 | shardCount: number; 39 | shardIndex: number; 40 | }; 41 | 42 | function extractShardConfig(shard: string): ShardConfig { 43 | try { 44 | const [shardIndex, shardCount] = shard.split("/"); 45 | return { shardCount: Number(shardCount), shardIndex: Number(shardIndex) }; 46 | } catch (err) { 47 | throw new Error( 48 | "The shard value is invalid. The value requires '/' format.", 49 | ); 50 | } 51 | } 52 | 53 | function loadDurations(reportFile: string): Record { 54 | const filePath = path.join(process.cwd(), reportFile); 55 | try { 56 | const report: JSONReport = JSON.parse(fs.readFileSync(filePath, "utf8")); 57 | const durations: Record = {}; 58 | for (const suite of report.suites) { 59 | durations[suite.file] = calculateDuration(suite); 60 | } 61 | return durations; 62 | } catch (_err) { 63 | return {}; 64 | } 65 | } 66 | 67 | function calculateDuration(suite: JSONReportSuite): number { 68 | let duration = 0; 69 | for (const spec of suite.specs) { 70 | for (const test of spec.tests) { 71 | for (const result of test.results) { 72 | duration += result.duration / 1000; 73 | } 74 | } 75 | } 76 | for (const test of suite.suites ?? []) { 77 | duration += calculateDuration(test); 78 | } 79 | return duration; 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/example-playwright.yaml: -------------------------------------------------------------------------------- 1 | name: examples playwright workflow 2 | on: 3 | push: 4 | 5 | jobs: 6 | default-test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | shard: [1/3, 2/3, 3/3] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/setup 14 | - name: Run test 15 | run: pnpm exec playwright test --shard=${{ matrix.shard }} 16 | working-directory: examples/playwright 17 | 18 | use-tenbin-playwright: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | shardIndex: [1, 2, 3] 24 | shardTotal: [3] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: ./.github/actions/setup 28 | - name: Run build 29 | run: pnpm run build 30 | - name: Restore test-results.json 31 | uses: actions/cache/restore@v4 32 | with: 33 | path: test-results.json 34 | key: test-results 35 | restore-keys: | 36 | test-results-* 37 | - name: Copy test-results.json to working directory 38 | run: | 39 | if [ -e ./test-results.json ]; then 40 | jq . test-results.json 41 | cp test-results.json examples/playwright 42 | fi 43 | - name: Run test 44 | run: | 45 | pnpm exec playwright test -c playwright-with-tenbin.config.js 46 | working-directory: examples/playwright 47 | env: 48 | TENBIN_SHARD: ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 49 | REPORT_FILE_NAME: report-${{ matrix.shardIndex }}.zip 50 | - name: Upload blob report 51 | if: github.ref_name == 'main' 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: blob-report-${{ matrix.shardIndex }} 55 | path: examples/playwright/blob-report 56 | 57 | merge-and-cache-test-results: 58 | if: github.ref_name == 'main' 59 | runs-on: ubuntu-latest 60 | needs: [use-tenbin-playwright] 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: ./.github/actions/setup 64 | - uses: actions/download-artifact@v4 65 | with: 66 | path: all-blob-reports 67 | pattern: blob-report-* 68 | merge-multiple: true 69 | - name: Merge json reports 70 | run: | 71 | pnpm dlx playwright merge-reports --reporter json ./all-blob-reports > test-results.json 72 | - name: Cache test-results.json 73 | uses: actions/cache/save@v4 74 | with: 75 | path: test-results.json 76 | key: test-results-${{ github.run_id }} 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | tenbin-report.json 133 | test-results.json 134 | test-results 135 | all-reports 136 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | type Partition = { 2 | partition: T[][]; 3 | diff: number; 4 | }; 5 | 6 | /** 7 | * Partition data into k partitions such that the difference between the sum of the values of each partition is minimized. 8 | * cf: https://en.wikipedia.org/wiki/Largest_differencing_method 9 | */ 10 | export function partition( 11 | data: T[], 12 | k: number, 13 | keyFunc: (item: T) => number = (item) => Number(item), 14 | ): T[][] { 15 | if (k === 1) { 16 | throw new Error("k must be greater than 1"); 17 | } 18 | 19 | let partitions: Partition[] = Array.from( 20 | { length: data.length }, 21 | (_, i) => { 22 | return { 23 | partition: Array.from({ length: k }, (_, k) => 24 | k === 0 ? [data[i]] : [], 25 | ), 26 | diff: keyFunc(data[i]), 27 | }; 28 | }, 29 | ); 30 | 31 | while (partitions.length > 1) { 32 | partitions = sortBy(partitions, (item) => item.diff, "asc"); 33 | const partitionA = partitions.pop(); 34 | const partitionB = partitions.pop(); 35 | if (!partitionA || !partitionB) { 36 | throw new Error("partitionA or partitionB should not be undefined"); 37 | } 38 | const sortedPartitionA = sortBy( 39 | partitionA.partition, 40 | (item: T[]) => sum(item, keyFunc), 41 | "asc", 42 | ); 43 | const sortedPartitionB = sortBy( 44 | partitionB.partition, 45 | (item: T[]) => sum(item, keyFunc), 46 | "desc", 47 | ); 48 | const newPartition = []; 49 | for (let i = 0; i < k; i++) { 50 | newPartition.push([...sortedPartitionA[i], ...sortedPartitionB[i]]); 51 | } 52 | partitions.push({ 53 | partition: newPartition, 54 | diff: diff(newPartition.map((item) => sum(item, keyFunc))), 55 | }); 56 | } 57 | return partitions[0].partition.map((item) => sortBy(item, keyFunc)); 58 | } 59 | 60 | function sum( 61 | array: T[], 62 | keyFunc: (item: T) => number = (item) => Number(item), 63 | ) { 64 | let sum = 0; 65 | for (let i = 0; i < array.length; i++) { 66 | sum += keyFunc(array[i]); 67 | } 68 | return sum; 69 | } 70 | 71 | const diff = (array: number[]) => { 72 | return max(array) - min(array); 73 | }; 74 | 75 | const max = (array: number[]) => { 76 | let max = array[0]; 77 | for (let i = 1; i < array.length; i++) { 78 | if (array[i] > max) { 79 | max = array[i]; 80 | } 81 | } 82 | return max; 83 | }; 84 | 85 | const min = (array: number[]) => { 86 | let min = array[0]; 87 | for (let i = 1; i < array.length; i++) { 88 | if (array[i] < min) { 89 | min = array[i]; 90 | } 91 | } 92 | return min; 93 | }; 94 | 95 | const sortBy = ( 96 | array: T[], 97 | keyFunc: (item: T) => number = (item) => Number(item), 98 | order: "asc" | "desc" = "asc", 99 | ) => { 100 | return array.sort((a, b) => { 101 | const aKey = keyFunc(a); 102 | const bKey = keyFunc(b); 103 | return order === "asc" ? aKey - bKey : bKey - aKey; 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /packages/jest/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/jest 2 | 3 | `@tenbin/jest` provides custom reporter and sequencer for Jest. 4 | 5 | ## Usage 6 | 7 | This package provide two modules: 8 | 9 | ### `TenbinReporter` 10 | 11 | This module is served as the default export from `@tenbin/jest/reporter`. 12 | 13 | `TenbinReporter` generates a JSON report showing the execution time (in seconds) for each test file, as shown below: 14 | 15 | ```json:tenbin-report.json 16 | { 17 | "tests/file-a.test.ts": 1.223, 18 | "tests/file-b.test.ts": 2.334, 19 | ... 20 | } 21 | ``` 22 | 23 | The report is saved as `tenbin-report.json` in the current working directory (cwd). This file is uploaded to external storage, such as S3, and is used by `TenbinSequencer` when running next tests. In the case of GitHub Actions, the file can be stored using a cache that persists between workflows. (See the Example section for details.) 24 | 25 | ### `TenbinSequencer` 26 | 27 | This module is served as the default export from `@tenbin/jest/sequencer`. 28 | 29 | `TenbinSequencer` reads the `tenbin-report.json` file from the current working directory (cwd) and splits tests to minimize the differences in test execution times across shards For test files not listed in `tenbin-report.json`, the execution time is assumed to be 0 seconds. If the tenbin-report.json file is not found, the shards are split randomly. 30 | 31 | ## Example 32 | 33 | Install: 34 | 35 | ```sh 36 | npm i @tenbin/jest -D 37 | ``` 38 | 39 | Configuration: 40 | 41 | ```js 42 | /** @type {import('jest').Config} */ 43 | const config = { 44 | testSequencer: "@tenbin/jest/sequencer", 45 | reporters: ["default", "@tenbin/jest/reporter"], 46 | }; 47 | 48 | module.exports = config; 49 | ``` 50 | 51 | GitHub Actions: 52 | 53 | ```yaml 54 | name: examples workflow 55 | on: 56 | push: 57 | 58 | jobs: 59 | use-tenbin-jest: 60 | runs-on: ubuntu-latest 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | shardIndex: [1, 2, 3] 65 | shardTotal: [3] 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions/setup-node@v4 69 | with: 70 | node-version: 20 71 | cache: "pnpm" 72 | - uses: pnpm/action-setup@v4 73 | with: 74 | version: "9" 75 | - name: Install dependencies 76 | run: pnpm install 77 | - name: Run build 78 | run: pnpm run build 79 | # Restore stenbin-report.json file, which records the execution time of each test file. 80 | # @tenbin/jest/sequencer uses this file for sharding. 81 | - name: Restore tenbin-report.json 82 | id: tenbin-report-cache 83 | uses: actions/cache/restore@v4 84 | with: 85 | path: tenbin-report.json 86 | key: tenbin-report 87 | restore-keys: | 88 | tenbin-report-* 89 | - name: Run test 90 | run: pnpx jest --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 91 | # @tenbin/jest/reporter generates tenbin-report.json for each shard. 92 | - name: Upload tenbin-report.json 93 | if: github.ref_name == 'main' 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: tenbin-report-${{ matrix.shardIndex }} 97 | path: tenbin-report.json 98 | 99 | # Merge and cache tenbin-report.json 100 | cache-tenbin-report: 101 | if: github.ref_name == 'main' 102 | runs-on: ubuntu-latest 103 | needs: [use-tenbin-jest] 104 | steps: 105 | - uses: actions/download-artifact@v4 106 | with: 107 | path: tenbin-report 108 | pattern: tenbin-report-* 109 | - name: Merge tenbin-report 110 | run: jq -s add tenbin-report/**/tenbin-report.json > tenbin-report.json 111 | - name: Cache tenbin-report.json 112 | uses: actions/cache/save@v4 113 | with: 114 | path: tenbin-report.json 115 | key: tenbin-report-${{ github.run_id }} 116 | ``` 117 | -------------------------------------------------------------------------------- /packages/vitest/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/vitest 2 | 3 | `@tenbin/vitest` provides custom reporter and sequencer for Vitest. 4 | 5 | ## Usage 6 | 7 | This package provide two modules: 8 | 9 | ### `TenbinReporter` 10 | 11 | This module is served as the default export from `@tenbin/vitest/reporter`. 12 | 13 | `TenbinReporter` generates a JSON report showing the execution time (in seconds) for each test file, as shown below: 14 | 15 | ```json:tenbin-report.json 16 | { 17 | "tests/file-a.test.ts": 1.223, 18 | "tests/file-b.test.ts": 2.334, 19 | ... 20 | } 21 | ``` 22 | 23 | The report is saved as `tenbin-report.json` in the current working directory (cwd). This file is uploaded to external storage, such as S3, and is used by `TenbinSequencer` when running next tests. In the case of GitHub Actions, the file can be stored using a cache that persists between workflows. (See the Example section for details.) 24 | 25 | ### `TenbinSequencer` 26 | 27 | This module is served as the default export from `@tenbin/vitest/sequencer`. 28 | 29 | `TenbinSequencer` reads the `tenbin-report.json` file from the current working directory (cwd) and splits tests to minimize the differences in test execution times across shards For test files not listed in `tenbin-report.json`, the execution time is assumed to be 0 seconds. If the tenbin-report.json file is not found, the shards are split randomly. 30 | 31 | ## Example 32 | 33 | Install: 34 | 35 | ```sh 36 | npm i @tenbin/vitest -D 37 | ``` 38 | 39 | Configuration: 40 | 41 | ```ts 42 | import TenbinReporter from "@tenbin/vitest/reporter"; 43 | import TenbinSequencer from "@tenbin/vitest/sequencer"; 44 | import { defineConfig } from "vitest/config"; 45 | 46 | export default defineConfig({ 47 | test: { 48 | reporters: [new TenbinReporter()], 49 | sequence: { 50 | sequencer: TenbinSequencer, 51 | }, 52 | }, 53 | }); 54 | ``` 55 | 56 | GitHub Actions: 57 | 58 | ```yaml 59 | name: examples workflow 60 | on: 61 | push: 62 | 63 | jobs: 64 | use-tenbin-vitest: 65 | runs-on: ubuntu-latest 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | shardIndex: [1, 2, 3] 70 | shardTotal: [3] 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions/setup-node@v4 74 | with: 75 | node-version: 20 76 | cache: "pnpm" 77 | - uses: pnpm/action-setup@v4 78 | with: 79 | version: "9" 80 | - name: Install dependencies 81 | run: pnpm install 82 | - name: Run build 83 | run: pnpm run build 84 | # Restore tenbin-report.json file, which records the execution time of each test file. 85 | # @tenbin/vitest/sequencer uses this file for sharding. 86 | - name: Restore tenbin-report.json 87 | id: tenbin-report-cache 88 | uses: actions/cache/restore@v4 89 | with: 90 | path: tenbin-report.json 91 | key: tenbin-report 92 | restore-keys: | 93 | tenbin-report-* 94 | - name: Run test 95 | run: pnpx vitest --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 96 | # @tenbin/vitest/reporter generates tenbin-report.json for each shard. 97 | - name: Upload tenbin-report.json 98 | if: github.ref_name == 'main' 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: tenbin-report-${{ matrix.shardIndex }} 102 | path: tenbin-report.json 103 | 104 | # Merge and cache tenbin-report.json 105 | cache-tenbin-report: 106 | if: github.ref_name == 'main' 107 | runs-on: ubuntu-latest 108 | needs: [use-tenbin-vitest] 109 | steps: 110 | - uses: actions/download-artifact@v4 111 | with: 112 | path: tenbin-report 113 | pattern: tenbin-report-* 114 | - name: Merge tenbin-report 115 | run: jq -s add tenbin-report/**/tenbin-report.json > tenbin-report.json 116 | - name: Cache tenbin-report.json 117 | uses: actions/cache/save@v4 118 | with: 119 | path: tenbin-report.json 120 | key: tenbin-report-${{ github.run_id }} 121 | ``` 122 | -------------------------------------------------------------------------------- /packages/playwright/README.md: -------------------------------------------------------------------------------- 1 | # @tenbin/playwright 2 | 3 | `@tenbin/playwright` provides the test split function for custom sharding. 4 | 5 | ## Usage 6 | 7 | This package provides one function: 8 | 9 | ### `splitTests` 10 | 11 | `splitTests` function divides test files based on the past [Playwright JSON report](https://playwright.dev/docs/test-reporters#json-reporter) specified in `reportFile`. It considers only files matching the `pattern` and ensures that each split has a balanced total execution time. 12 | 13 | ```ts 14 | import { defineConfig } from "@playwright/test"; 15 | import { splitTests } from "@tenbin/playwright"; 16 | 17 | export default defineConfig({ 18 | testMatch: splitTests({ 19 | shard: "1/3", 20 | pattern: ["tests/**.test.ts"], 21 | reportFile: "./test-results.json", 22 | }), 23 | }); 24 | ``` 25 | 26 | Options: 27 | 28 | - `shard`: test suite shard to execute in a format of `/` 29 | - `pattern`: glob pattern that defines which test files should be executed 30 | - `reportFile`: path to previous Playwright JSON report 31 | 32 | ## Example 33 | 34 | Install: 35 | 36 | ```sh 37 | npm i @tenbin/playwright -D 38 | ``` 39 | 40 | Configuration: 41 | 42 | ```js 43 | import { defineConfig } from "@playwright/test"; 44 | import { splitTests } from "@tenbin/playwright"; 45 | 46 | export default defineConfig({ 47 | testMatch: splitTests({ 48 | shard: process.env.TENBIN_SHARD, 49 | pattern: ["tests/**.test.ts"], 50 | reportFile: "./test-results.json", 51 | }), 52 | reporter: [["blob", { fileName: process.env.REPORT_FILE_NAME }]], 53 | }); 54 | ``` 55 | 56 | GitHub Actions: 57 | 58 | ```yaml 59 | name: examples workflow 60 | on: 61 | push: 62 | 63 | jobs: 64 | use-tenbin-playwright: 65 | runs-on: ubuntu-latest 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | shardIndex: [1, 2, 3] 70 | shardTotal: [3] 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions/setup-node@v4 74 | with: 75 | node-version: 20 76 | cache: "pnpm" 77 | - uses: pnpm/action-setup@v4 78 | with: 79 | version: "9" 80 | - name: Install dependencies 81 | run: pnpm install 82 | - name: Run build 83 | run: pnpm run build 84 | # Restore test-results.json file, which records the execution time of each test file. 85 | # splitTests function uses this file for sharding. 86 | - name: Restore test-results.json 87 | uses: actions/cache/restore@v4 88 | with: 89 | path: test-results.json 90 | key: test-results 91 | restore-keys: | 92 | test-results-* 93 | - name: Run test 94 | run: pnpm exec playwright test 95 | env: 96 | # these variables are used in playwright.config.js 97 | TENBIN_SHARD: ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} 98 | REPORT_FILE_NAME: report-${{ matrix.shardIndex }}.zip 99 | # see: https://playwright.dev/docs/test-sharding#github-actions-example 100 | - name: Upload blob report 101 | if: github.ref_name == 'main' 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: blob-report-${{ matrix.shardIndex }} 105 | path: blob-report 106 | 107 | # Merge and cache test-results.json 108 | cache-test-results: 109 | if: github.ref_name == 'main' 110 | runs-on: ubuntu-latest 111 | needs: [use-tenbin-playwright] 112 | steps: 113 | - uses: actions/checkout@v4 114 | - uses: actions/setup-node@v4 115 | with: 116 | node-version: 20 117 | cache: "pnpm" 118 | - uses: pnpm/action-setup@v4 119 | with: 120 | version: "9" 121 | - uses: actions/download-artifact@v4 122 | with: 123 | path: all-blob-reports 124 | pattern: blob-report-* 125 | merge-multiple: true 126 | - name: Merge blob reports into json 127 | run: pnpm dlx playwright merge-reports --reporter json ./all-blob-reports > test-results.json 128 | - name: Cache test-results.json 129 | uses: actions/cache/save@v4 130 | with: 131 | path: test-results.json 132 | key: test-results-${{ github.run_id }} 133 | ``` 134 | --------------------------------------------------------------------------------