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!
|
||||
|
||||
API documentation can also be found [here](./docs/API.md).
|
||||
|
||||
## Development
|
||||
|
||||
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)
|
||||
- [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",
|
||||
"prebuild": "rm -rf dist",
|
||||
"cli": "$SHELL ./script/run",
|
||||
"lint": "rome format src tests && rome check src tests",
|
||||
"lint:fix": "rome format src tests --write && rome check src tests --apply",
|
||||
"lint": "rome format src tests docs && rome check src tests docs",
|
||||
"lint:fix": "rome format src tests docs --write && rome check src tests docs --apply",
|
||||
"types:check": "yarn tsc --project . --noEmit",
|
||||
"test": "$SHELL ./script/test-suite",
|
||||
"test:integration": "$SHELL ./script/integration-tests",
|
||||
"build:ts": "tsc --project .",
|
||||
"build": "$SHELL ./script/build"
|
||||
"build": "$SHELL ./script/build",
|
||||
"docs": "yarn dlx ts-node ./docs/generate.ts ./src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.10",
|
||||
|
|
|
@ -3,13 +3,28 @@
|
|||
*
|
||||
* 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.
|
||||
*/
|
||||
import assert from 'assert'
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
*
|
||||
|
@ -18,6 +9,28 @@ function formatMessage(results: string, failed: boolean): string {
|
|||
* decides to assign it and files assigned to the worker are only
|
||||
* 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() {
|
||||
if (process?.send === undefined) throw Error('No process global found')
|
||||
const tsMode = Boolean(process.env.TS === '1')
|
||||
|
|
Reference in a new issue