Build/autodoc generation (#32)
* feat: autodoc * docs: autodoc output docs: autodoc output * build: document generator * ci: add doc check * chore: add docs * ci: finagle with docs stale check ci: target generated docs for diff * ci: remove check * docs: add links * docs: more matcher documentation
This commit is contained in:
parent
29202908d4
commit
bc2bd85422
7 changed files with 433 additions and 15 deletions
|
@ -24,6 +24,8 @@ can peek at the opinions baked into this [here](./DESIGN_DECISIONS.md).
|
||||||
|
|
||||||
Documentation can be found [here](./docs/INDEX.md) and welcomes contributions!
|
Documentation can be found [here](./docs/INDEX.md) and welcomes contributions!
|
||||||
|
|
||||||
|
API documentation can also be found [here](./docs/API.md).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
This uses [Corepack](https://github.com/nodejs/corepack), which comes bundled with `node>=16` to manage which Yarn version to use.
|
This uses [Corepack](https://github.com/nodejs/corepack), which comes bundled with `node>=16` to manage which Yarn version to use.
|
||||||
|
|
274
docs/API.md
Normal file
274
docs/API.md
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
# API documentation
|
||||||
|
---
|
||||||
|
## ./src/worker.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Worker runtime
|
||||||
|
|
||||||
|
The worker executes the tests by called `node` on them. Since each test
|
||||||
|
is an self-contained executable file, the worker can run each of them,
|
||||||
|
collect output and relay it back to the runner process via IPC.
|
||||||
|
|
||||||
|
Each worker process is responsible for as many test files as the runner
|
||||||
|
decides to assign it and files assigned to the worker are only
|
||||||
|
touched by the worker assigned to them.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / work
|
||||||
|
```ts
|
||||||
|
async function work()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Entrypoint for the worker.
|
||||||
|
|
||||||
|
Retrieves paths assigned to the worker from the arguments passed when
|
||||||
|
calling the worker runtime and spawns processes to run the test file
|
||||||
|
pointed at by each of the paths.
|
||||||
|
|
||||||
|
This will spawn one process per file and each process will communicate back
|
||||||
|
to the worker's parent process as it finishes.
|
||||||
|
|
||||||
|
If the `TS` flag is passed, the worker runs the test file using ts-node
|
||||||
|
for Typescript compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/testComponents/matchers.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built-in matchers
|
||||||
|
|
||||||
|
All the matchers defined and exported as part of the default export
|
||||||
|
of this file are available to each `expect` statement made in tests.
|
||||||
|
|
||||||
|
A matcher's inverse is defined by its behaviour when the `negated` flag
|
||||||
|
that is passed to it is truthy.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toEqual
|
||||||
|
```ts
|
||||||
|
function toEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`toEqual` asserts whether `value` and `other` are strictly equal.
|
||||||
|
|
||||||
|
Once registered, it can be used as
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(value).toEqual(other)
|
||||||
|
```
|
||||||
|
|
||||||
|
and in negated form
|
||||||
|
|
||||||
|
```ts
|
||||||
|
expect(value).not.toEqual(other)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toNotEqual
|
||||||
|
```ts
|
||||||
|
function toNotEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`toNotEqual` is the inverse of `toEqual` and behaves the same way as
|
||||||
|
`expect(...).not.toEqual(...)`. Negating it behaves like `toEqual`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toBe
|
||||||
|
```ts
|
||||||
|
function toBe(value: unknown, other: unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Asserts whether value and other are the same entity.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toNotBe
|
||||||
|
```ts
|
||||||
|
function toNotBe(value: unknown, other: unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Inverse of toBe.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toThrow
|
||||||
|
```ts
|
||||||
|
function toThrow(func: () => unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Asserts whether the provided function throws the provided error.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toNotThrow
|
||||||
|
```ts
|
||||||
|
function toNotThrow(func: () => unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Inverse of toThrow.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / toHaveLength
|
||||||
|
```ts
|
||||||
|
function toHaveLength(value: unknown, length: unknown, negated: boolean = false): MatcherReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Validates that the `value` has a length of `length`. The value provided to `value` should
|
||||||
|
have a defined length (i.e. it can be a string or some sort of iterable).
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/testComponents/test.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / test
|
||||||
|
```ts
|
||||||
|
function test(label: TestCaseLabel, testCase: TestCaseFunction): void
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`test` defines a single test case.
|
||||||
|
|
||||||
|
```
|
||||||
|
test('My test', () => {
|
||||||
|
// Assert things.
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`it` is an alias of `test`.
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/testComponents/expect.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The `expect` function returned is the main access point
|
||||||
|
to create Expect objects. On import, all the built-in matchers
|
||||||
|
are registered, but more can be registered ad-hoc via `addMatcher`.
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/testComponents/describe.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / describe
|
||||||
|
```ts
|
||||||
|
function describe(label: TestCaseLabel, testGroup: TestCaseGroup)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`describe` facilitates grouping tests together.
|
||||||
|
|
||||||
|
```
|
||||||
|
describe('My test group', () => {
|
||||||
|
test('My first test', ...)
|
||||||
|
|
||||||
|
test('My second test', ...)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/runner.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / collectTests
|
||||||
|
```ts
|
||||||
|
async function collectTests(roots: Array<string>): Promise<Array<string>>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Collects test files recursively starting from the provided root
|
||||||
|
path.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / assignTestsToWorkers
|
||||||
|
```ts
|
||||||
|
async function assignTestsToWorkers(
|
||||||
|
context: Context,
|
||||||
|
collectedPaths: Array<string>,
|
||||||
|
workerCount: number = 1,
|
||||||
|
): Promise<{ [key: number]: WorkerReport }>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Splits the list of collected test files into `workerCount` batches and starts
|
||||||
|
worker processes.
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/logging.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
### class / Logger
|
||||||
|
|
||||||
|
Standard logger for anything that needs to print messages to the user.
|
||||||
|
|
||||||
|
This supports the same general functionality as the `Console` logger,
|
||||||
|
including `group` and various levels of logging.
|
||||||
|
|
||||||
|
|
||||||
|
## ./src/utils.ts
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / boldText
|
||||||
|
```ts
|
||||||
|
export function boldText(text: string | number): string
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Terminal text style
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / assertTsNodeInstall
|
||||||
|
```ts
|
||||||
|
export function assertTsNodeInstall()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To support typescript source directly, womm uses ts-node in
|
||||||
|
workers to execute test files.
|
||||||
|
|
||||||
|
If ts-node is not installed, this throws.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / getContext
|
||||||
|
```ts
|
||||||
|
export function getContext(runnerPath: string, ts: boolean = false): Context
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Generates a context object that contains general information
|
||||||
|
about the test runner. The parameter here should always be
|
||||||
|
`process.argv[1]`, which will allow all the other paths
|
||||||
|
to be set properly.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
### function / splitIntoBatches
|
||||||
|
```ts
|
||||||
|
export function splitIntoBatches<T>(data: Array<T>, desiredBatchCount: number = 1): Array<Array<T>>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Divides the given list into `desiredBatchCount` batches, returning
|
||||||
|
an array of arrays which add up to the given list.
|
||||||
|
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
- [Writing tests](./WRITING_TESTS.md)
|
- [Writing tests](./WRITING_TESTS.md)
|
||||||
- [Extending with custom matchers](./CUSTOM_MATCHERS.md)
|
- [Extending with custom matchers](./CUSTOM_MATCHERS.md)
|
||||||
|
- [API documentation](./API.md)
|
||||||
|
|
111
docs/generate.ts
Normal file
111
docs/generate.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import * as ts from 'typescript'
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import childProcess from 'child_process'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
interface ExtractedNode {
|
||||||
|
name?: string
|
||||||
|
snippet?: string
|
||||||
|
comment?: string
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APIDocumentationNode {
|
||||||
|
sourcePath: string
|
||||||
|
targetPath: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_DOC_PATH = './docs/API.md'
|
||||||
|
|
||||||
|
function formatFunctionDeclaration(node: ts.FunctionDeclaration, fullText: string): string {
|
||||||
|
const declarationStart = node.getStart()
|
||||||
|
const declarationEnd = node.body?.pos ?? declarationStart
|
||||||
|
return fullText.slice(declarationStart, declarationEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Documentation extraction
|
||||||
|
*
|
||||||
|
* This extracts block comments and declared entities from source files
|
||||||
|
* and reformats it so publishable documentation can be generated from it.
|
||||||
|
*/
|
||||||
|
async function generateDocumentation() {
|
||||||
|
const rootPaths = process.argv.slice(2)
|
||||||
|
const output = childProcess.execSync(`find ${rootPaths.join(' ')} -name *.ts`, { encoding: 'utf8' })
|
||||||
|
|
||||||
|
const files: Array<APIDocumentationNode> = []
|
||||||
|
|
||||||
|
const sourcePaths = output.split('\n').filter((filePath) => Boolean(filePath))
|
||||||
|
|
||||||
|
for await (const sourcePath of sourcePaths) {
|
||||||
|
const rawContent = await fs.readFile(sourcePath, { encoding: 'utf8' })
|
||||||
|
|
||||||
|
const source = ts.createSourceFile(sourcePath, rawContent, ts.ScriptTarget.ES2015, true)
|
||||||
|
const fullText = source.getFullText()
|
||||||
|
const extractedNodes: Array<ExtractedNode> = []
|
||||||
|
|
||||||
|
ts.forEachChild(source, (node: ts.Node) => {
|
||||||
|
const extractedNode: ExtractedNode = {}
|
||||||
|
|
||||||
|
const commentRanges = ts.getLeadingCommentRanges(fullText, node.getFullStart())
|
||||||
|
const blockComments = (commentRanges ?? [])
|
||||||
|
.filter((comment) => comment.kind === ts.SyntaxKind.MultiLineCommentTrivia)
|
||||||
|
.map((r) => {
|
||||||
|
return fullText.slice(r.pos, r.end)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (node.kind === ts.SyntaxKind.FunctionDeclaration) {
|
||||||
|
const functionDeclarationNode = node as ts.FunctionDeclaration
|
||||||
|
extractedNode.snippet = formatFunctionDeclaration(functionDeclarationNode, fullText)
|
||||||
|
extractedNode.name = String(functionDeclarationNode?.name?.escapedText ?? 'unknown function')
|
||||||
|
extractedNode.type = 'function'
|
||||||
|
} else if (node.kind === ts.SyntaxKind.ClassDeclaration) {
|
||||||
|
const classDeclarationNode = node as ts.ClassDeclaration
|
||||||
|
extractedNode.name = String(classDeclarationNode?.name?.escapedText ?? 'unknown class')
|
||||||
|
extractedNode.type = 'class'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockComments.length === 0) return
|
||||||
|
|
||||||
|
extractedNode.comment = blockComments[0]
|
||||||
|
|
||||||
|
extractedNodes.push(extractedNode)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (extractedNodes.length === 0) continue
|
||||||
|
|
||||||
|
const targetPath = `docs/api/${path.basename(sourcePath, '.ts')}.md`
|
||||||
|
|
||||||
|
let content = ''
|
||||||
|
|
||||||
|
content += `## ${sourcePath}\n\n`
|
||||||
|
|
||||||
|
for (const entry of extractedNodes) {
|
||||||
|
content += '---\n'
|
||||||
|
if (entry.name) content += `### ${entry?.type ?? ''} / ${entry.name}\n`
|
||||||
|
if (entry.snippet) content += `\`\`\`ts\n${entry.snippet}\n\`\`\`\n\n`
|
||||||
|
|
||||||
|
const entryDescription = (entry?.comment ?? '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.replace(/(\/\*)|(\*\/)|(\*)/g, '').trim())
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
content += `${entryDescription}\n\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
sourcePath,
|
||||||
|
targetPath,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(API_DOC_PATH, '# API documentation\n---\n')
|
||||||
|
|
||||||
|
for await (const item of files) {
|
||||||
|
await fs.appendFile(API_DOC_PATH, item.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDocumentation()
|
|
@ -16,13 +16,14 @@
|
||||||
"prepack": "yarn build",
|
"prepack": "yarn build",
|
||||||
"prebuild": "rm -rf dist",
|
"prebuild": "rm -rf dist",
|
||||||
"cli": "$SHELL ./script/run",
|
"cli": "$SHELL ./script/run",
|
||||||
"lint": "rome format src tests && rome check src tests",
|
"lint": "rome format src tests docs && rome check src tests docs",
|
||||||
"lint:fix": "rome format src tests --write && rome check src tests --apply",
|
"lint:fix": "rome format src tests docs --write && rome check src tests docs --apply",
|
||||||
"types:check": "yarn tsc --project . --noEmit",
|
"types:check": "yarn tsc --project . --noEmit",
|
||||||
"test": "$SHELL ./script/test-suite",
|
"test": "$SHELL ./script/test-suite",
|
||||||
"test:integration": "$SHELL ./script/integration-tests",
|
"test:integration": "$SHELL ./script/integration-tests",
|
||||||
"build:ts": "tsc --project .",
|
"build:ts": "tsc --project .",
|
||||||
"build": "$SHELL ./script/build"
|
"build": "$SHELL ./script/build",
|
||||||
|
"docs": "yarn dlx ts-node ./docs/generate.ts ./src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.15.10",
|
"@types/node": "^18.15.10",
|
||||||
|
|
|
@ -3,13 +3,28 @@
|
||||||
*
|
*
|
||||||
* All the matchers defined and exported as part of the default export
|
* All the matchers defined and exported as part of the default export
|
||||||
* of this file are available to each `expect` statement made in tests.
|
* of this file are available to each `expect` statement made in tests.
|
||||||
|
*
|
||||||
|
* A matcher's inverse is defined by its behaviour when the `negated` flag
|
||||||
|
* that is passed to it is truthy.
|
||||||
*/
|
*/
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
|
||||||
import { type MatcherReport, type WithLength } from '../types'
|
import { type MatcherReport, type WithLength } from '../types'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Asserts whether value and other are strictly equal.
|
* `toEqual` asserts whether `value` and `other` are strictly equal.
|
||||||
|
*
|
||||||
|
* Once registered, it can be used as
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* expect(value).toEqual(other)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* and in negated form
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* expect(value).not.toEqual(other)
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
function toEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
function toEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
||||||
if (negated) return toNotEqual(value, other)
|
if (negated) return toNotEqual(value, other)
|
||||||
|
@ -26,7 +41,8 @@ function toEqual(value: unknown, other: unknown, negated: boolean = false): Matc
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Inverse of toEqual.
|
* `toNotEqual` is the inverse of `toEqual` and behaves the same way as
|
||||||
|
* `expect(...).not.toEqual(...)`. Negating it behaves like `toEqual`.
|
||||||
*/
|
*/
|
||||||
function toNotEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
function toNotEqual(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
||||||
if (negated) return toEqual(value, other)
|
if (negated) return toEqual(value, other)
|
||||||
|
@ -50,7 +66,7 @@ function toBe(value: unknown, other: unknown, negated: boolean = false): Matcher
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Inverse ot toBe.
|
* Inverse of toBe.
|
||||||
*/
|
*/
|
||||||
function toNotBe(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
function toNotBe(value: unknown, other: unknown, negated: boolean = false): MatcherReport {
|
||||||
if (negated) return toBe(value, other)
|
if (negated) return toBe(value, other)
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
import { getContext, spawnProcess } from './utils'
|
|
||||||
|
|
||||||
// TODO: What should be message protocol / format be?
|
|
||||||
function formatMessage(results: string, failed: boolean): string {
|
|
||||||
return JSON.stringify({ results, failed })
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Worker runtime
|
* Worker runtime
|
||||||
*
|
*
|
||||||
|
@ -18,6 +9,28 @@ function formatMessage(results: string, failed: boolean): string {
|
||||||
* decides to assign it and files assigned to the worker are only
|
* decides to assign it and files assigned to the worker are only
|
||||||
* touched by the worker assigned to them.
|
* touched by the worker assigned to them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { getContext, spawnProcess } from './utils'
|
||||||
|
|
||||||
|
function formatMessage(results: string, failed: boolean): string {
|
||||||
|
return JSON.stringify({ results, failed })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Entrypoint for the worker.
|
||||||
|
*
|
||||||
|
* Retrieves paths assigned to the worker from the arguments passed when
|
||||||
|
* calling the worker runtime and spawns processes to run the test file
|
||||||
|
* pointed at by each of the paths.
|
||||||
|
*
|
||||||
|
* This will spawn one process per file and each process will communicate back
|
||||||
|
* to the worker's parent process as it finishes.
|
||||||
|
*
|
||||||
|
* If the `TS` flag is passed, the worker runs the test file using ts-node
|
||||||
|
* for Typescript compatibility.
|
||||||
|
*/
|
||||||
async function work() {
|
async function work() {
|
||||||
if (process?.send === undefined) throw Error('No process global found')
|
if (process?.send === undefined) throw Error('No process global found')
|
||||||
const tsMode = Boolean(process.env.TS === '1')
|
const tsMode = Boolean(process.env.TS === '1')
|
||||||
|
|
Reference in a new issue