-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5b944e7
commit f2cbd92
Showing
10 changed files
with
1,242 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
name: Continuous Integration - Unit | ||
|
||
on: | ||
push: | ||
paths: | ||
- ".github/workflows/ci-unit.yml" | ||
- "packages/unit/**" | ||
pull_request: | ||
paths: | ||
- ".github/workflows/ci-unit.yml" | ||
- "packages/unit/**" | ||
|
||
jobs: | ||
linter: | ||
name: Lint Code | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: read | ||
steps: | ||
- name: Check out repo | ||
uses: actions/checkout@v4 | ||
with: | ||
persist-credentials: false | ||
|
||
- name: Setup Node | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: 18 | ||
|
||
- name: Install pnpm | ||
uses: pnpm/action-setup@v3 | ||
with: | ||
version: 8 | ||
run_install: false | ||
|
||
- name: Get pnpm store directory | ||
shell: bash | ||
run: | | ||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | ||
- uses: actions/cache@v4 | ||
name: Setup pnpm cache | ||
with: | ||
path: ${{ env.STORE_PATH }} | ||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} | ||
restore-keys: | | ||
${{ runner.os }}-pnpm-store- | ||
- name: Install dependencies | ||
run: pnpm install | ||
|
||
- name: Lint code | ||
run: pnpm --filter "./packages/unit" run lint | ||
|
||
test: | ||
name: Test | ||
runs-on: ${{ matrix.os }} | ||
permissions: | ||
contents: read | ||
strategy: | ||
matrix: | ||
node-version: [18, 20] | ||
os: [macos-latest, ubuntu-latest, windows-latest] | ||
steps: | ||
- name: Check out repo | ||
uses: actions/checkout@v4 | ||
with: | ||
persist-credentials: false | ||
|
||
- name: Setup Node ${{ matrix.node-version }} | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: ${{ matrix.node-version }} | ||
|
||
- name: Install pnpm | ||
uses: pnpm/action-setup@v3 | ||
with: | ||
version: 8 | ||
run_install: false | ||
|
||
- name: Get pnpm store directory | ||
shell: bash | ||
run: | | ||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | ||
- uses: actions/cache@v4 | ||
name: Setup pnpm cache | ||
with: | ||
path: ${{ env.STORE_PATH }} | ||
key: ${{ runner.os }}-${{ matrix.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} | ||
restore-keys: | | ||
${{ runner.os }}-${{ matrix.node-version }}-pnpm-store- | ||
- name: Install dependencies | ||
run: pnpm install | ||
|
||
- name: Run tests | ||
run: pnpm --filter "./packages/unit" run test | ||
|
||
automerge: | ||
name: Automerge Dependabot PRs | ||
if: > | ||
github.event_name == 'pull_request' && | ||
github.event.pull_request.user.login == 'dependabot[bot]' | ||
needs: test | ||
permissions: | ||
pull-requests: write | ||
contents: write | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: fastify/github-action-merge-dependabot@v3 | ||
with: | ||
exclude: ${{ inputs.auto-merge-exclude }} | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
target: major |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"extends": "standard-with-typescript", | ||
"parserOptions": { | ||
"project": "./tsconfig.json" | ||
}, | ||
"rules": { | ||
// conflict between standard and standard-typescript | ||
"no-void": ["error", { "allowAsStatement": true }] | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": ["**/*.test.ts"], | ||
"rules": { | ||
"@typescript-eslint/no-floating-promises": "off" | ||
} | ||
}, | ||
{ | ||
"files": ["scripts/*.mjs"], | ||
"rules": { | ||
"@typescript-eslint/explicit-function-return-type": "off" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
#!/usr/bin/env node | ||
|
||
import { glob } from 'glob' | ||
import { compose } from 'node:stream' | ||
import { run } from 'node:test' | ||
import { spec as Spec } from 'node:test/reporters' | ||
import { parseArgs } from 'node:util' | ||
|
||
const { values } = parseArgs({ | ||
args: process.args, | ||
options: { | ||
timeout: { type: 'string' } | ||
} | ||
}) | ||
|
||
run({ | ||
concurrency: true, | ||
timeout: Number(values.timeout ?? 30_000), | ||
setup: (test) => { | ||
const reportor = new Spec() | ||
compose(test.reporter, reportor).pipe(process.stdout) | ||
}, | ||
files: await glob(['**/*.test.{js,ts}'], { ignore: 'node_modules/**' }) | ||
}).on('test:fail', (data) => { | ||
if (data.todo === undefined || data.todo === false) { | ||
process.exitCode = 1; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import assert from 'node:assert' | ||
import { | ||
test as nodeTest | ||
} from 'node:test' | ||
|
||
type TestFn = NonNullable<Parameters<typeof nodeTest>[0]> | ||
|
||
interface TestOptions { | ||
/** | ||
* If a number is provided, then that many tests would run in parallel. | ||
* If truthy, it would run (number of cpu cores - 1) tests in parallel. | ||
* For subtests, it will be `Infinity` tests in parallel. | ||
* If falsy, it would only run one test at a time. | ||
* If unspecified, subtests inherit this value from their parent. | ||
* @default false | ||
*/ | ||
concurrency?: number | boolean | undefined | ||
/** | ||
* If truthy, and the test context is configured to run `only` tests, then this test will be | ||
* run. Otherwise, the test is skipped. | ||
* @default false | ||
*/ | ||
only?: boolean | undefined | ||
/** | ||
* Allows aborting an in-progress test. | ||
* @since v18.8.0 | ||
*/ | ||
signal?: AbortSignal | undefined | ||
/** | ||
* If truthy, the test is skipped. If a string is provided, that string is displayed in the | ||
* test results as the reason for skipping the test. | ||
* @default false | ||
*/ | ||
skip?: boolean | string | undefined | ||
/** | ||
* A number of milliseconds the test will fail after. If unspecified, subtests inherit this | ||
* value from their parent. | ||
* @default Infinity | ||
* @since v18.7.0 | ||
*/ | ||
timeout?: number | undefined | ||
/** | ||
* If truthy, the test marked as `TODO`. If a string is provided, that string is displayed in | ||
* the test results as the reason why the test is `TODO`. | ||
* @default false | ||
*/ | ||
todo?: boolean | string | undefined | ||
} | ||
|
||
type TestContext = Parameters<TestFn>[0] | ||
|
||
type ExtendedTestFn = (t: ExtendedTestContext, done: (result?: any) => void) => void | Promise<void> | ||
|
||
interface Test { | ||
(name?: string, fn?: ExtendedTestFn): Promise<void> | ||
(name?: string, options?: TestOptions, fn?: ExtendedTestFn): Promise<void> | ||
(options?: TestOptions, fn?: ExtendedTestFn): Promise<void> | ||
(fn?: ExtendedTestFn): Promise<void> | ||
} | ||
|
||
interface Assert extends Omit<typeof assert, 'CallTracker' | 'AssertionError' | 'strict'> {} | ||
|
||
interface ExtendedTestContext extends Omit<TestContext, 'test'>, Assert { | ||
plan: (count: number) => void | ||
test: Test | ||
} | ||
|
||
interface DeferredPromise<T = unknown> { | ||
promise: Promise<T> | ||
resolve: (...args: any[]) => void | ||
reject: (...args: any[]) => void | ||
} | ||
function createDeferredPromise (): DeferredPromise { | ||
const promise: any = {} | ||
promise.promise = new Promise((resolve, reject) => { | ||
promise.resolve = resolve | ||
promise.reject = reject | ||
}) | ||
return promise | ||
} | ||
|
||
export const test: Test = wrapTest(nodeTest) | ||
|
||
function wrapTest (testFn: any): Test { | ||
const test: Test = async function (...args: any[]) { | ||
const fn: TestFn = args.pop() // TestFn must be the last one | ||
|
||
const customFn: ExtendedTestFn = async function (context) { | ||
const { promises: contextPromises, teardown } = wrapContext(context) | ||
|
||
// either return promise or using done | ||
const promise = createDeferredPromise() | ||
const fnPromise = fn(context as never as TestContext, () => { | ||
promise.resolve() | ||
}) | ||
|
||
// resolve sub context | ||
await Promise.all(contextPromises) | ||
// resolve current context | ||
await Promise.race([promise.promise, fnPromise]) | ||
|
||
teardown() | ||
} | ||
|
||
await testFn(...[...args, customFn] as TestFn[]) | ||
} | ||
|
||
return test | ||
} | ||
|
||
function wrapContext (context: ExtendedTestContext): { promises: Array<Promise<unknown>>, teardown: () => void } { | ||
const promises: Array<Promise<unknown>> = [] | ||
let expect = -1 | ||
let actual = 0 | ||
|
||
const teardown = (): void => { | ||
if (expect > -1) { | ||
assert.strictEqual(actual, expect) | ||
} | ||
} | ||
|
||
context.plan = function plan (num: number) { | ||
expect = num | ||
} | ||
|
||
const contextTest = context.test.bind(context) | ||
const test = async function (...args: unknown[]): Promise<void> { | ||
const promise = wrapTest(contextTest)(...(args as [])) | ||
promises.push(promise) | ||
await promise | ||
actual++ | ||
} | ||
context.test = test | ||
|
||
for (const method of Object.keys(assert)) { | ||
if (method.match(/^[a-z]/) !== null) { | ||
(context as any)[method] = (...args: any[]) => { | ||
actual++ | ||
const res = (assert as any)[method](...args) | ||
return res | ||
} | ||
} | ||
} | ||
|
||
return { promises, teardown } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
{ | ||
"name": "@kakang/unit", | ||
"version": "0.0.0", | ||
"description": "", | ||
"bin": "./bin/run.mjs", | ||
"main": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./lib/mjs/index.js", | ||
"require": "./lib/index.js" | ||
} | ||
}, | ||
"directories": { | ||
"lib": "lib" | ||
}, | ||
"scripts": { | ||
"clean": "node ../../scripts/build.mjs --clean", | ||
"lint": "eslint --ext .ts lib test", | ||
"lint:fix": "npm run lint -- --fix", | ||
"build": "node ../../scripts/build.mjs --build=\"all\"", | ||
"build:cjs": "node ../../scripts/build.mjs --build='cjs'", | ||
"build:mjs": "node ../../scripts/build.mjs --build='mjs'", | ||
"unit": "node --require ts-node/register ../../scripts/test.mjs", | ||
"test": "npm run lint && npm run unit", | ||
"coverage": "c8 node --require ts-node/register ../../scripts/test.mjs", | ||
"prepublishOnly": "npm run build", | ||
"postpublish": "npm run clean" | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"registry": "https://registry.npmjs.org/" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/kaka-ng/nodejs.git" | ||
}, | ||
"author": "KaKa <[email protected]>", | ||
"license": "GPL-3.0", | ||
"devDependencies": { | ||
"@types/node": "^20.12.7", | ||
"@typescript-eslint/eslint-plugin": "7.5.0", | ||
"@typescript-eslint/parser": "7.5.0", | ||
"eslint": "^8.57.0", | ||
"eslint-config-standard-with-typescript": "^43.0.1", | ||
"eslint-plugin-import": "^2.29.1", | ||
"eslint-plugin-n": "^16.6.2", | ||
"eslint-plugin-promise": "^6.1.1", | ||
"rimraf": "^5.0.5", | ||
"ts-node": "^10.9.2", | ||
"tsc-alias": "^1.8.8", | ||
"typescript": "~5.4.5" | ||
}, | ||
"dependencies": { | ||
"glob": "^10.3.12" | ||
} | ||
} |
Oops, something went wrong.