Skip to content

Commit

Permalink
feat: unit
Browse files Browse the repository at this point in the history
  • Loading branch information
climba03003 committed Apr 11, 2024
1 parent 5b944e7 commit f2cbd92
Show file tree
Hide file tree
Showing 10 changed files with 1,242 additions and 103 deletions.
115 changes: 115 additions & 0 deletions .github/workflows/ci-unit.yml
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
24 changes: 24 additions & 0 deletions packages/unit/.eslintrc
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"
}
}
]
}
28 changes: 28 additions & 0 deletions packages/unit/bin/run.mjs
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;
}
});
146 changes: 146 additions & 0 deletions packages/unit/lib/index.ts
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 }
}
57 changes: 57 additions & 0 deletions packages/unit/package.json
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"
}
}
Loading

0 comments on commit f2cbd92

Please sign in to comment.