diff --git a/documentation/desired-concept/architecture/configuration.md b/documentation/desired-concept/architecture/configuration.md index 9d8c130d..f6b7dbaf 100644 --- a/documentation/desired-concept/architecture/configuration.md +++ b/documentation/desired-concept/architecture/configuration.md @@ -98,10 +98,10 @@ The first-party shape is: ```ts import { defineConfig } from '@overkill-dev/test/config'; -import { lineReporter } from '@overkill-dev/test/reporters'; +import { createLineReporter } from '@overkill-dev/test/reporters'; export const config = defineConfig({ - reporters: [ lineReporter() ], + reporters: [ createLineReporter() ], profiles: { unit: { testFamily: 'microtest', @@ -242,7 +242,7 @@ Use profiles for ordinary and optional runtime families instead: ```ts export const config = defineConfig({ - reporters: [ lineReporter() ], + reporters: [ createLineReporter() ], profiles: { 'unit-fast': { testFamily: 'microtest', diff --git a/documentation/desired-concept/architecture/package-architecture.md b/documentation/desired-concept/architecture/package-architecture.md index 17c379d2..4174610b 100644 --- a/documentation/desired-concept/architecture/package-architecture.md +++ b/documentation/desired-concept/architecture/package-architecture.md @@ -699,13 +699,14 @@ Everything outside the authoring hot path uses an explicit subpath: ```ts import { defineConfig } from '@overkill-dev/test/config'; -import { lineReporter } from '@overkill-dev/test/reporters'; -import { benchmark, workload } from '@overkill-dev/test/bench'; +import { createLineReporter } from '@overkill-dev/test/reporters'; import { defineCompositeAssertion } from '@overkill-dev/test/assert'; ``` Subpaths may re-export standard-stack packages for user ergonomics. They do not transfer semantic ownership away from those packages. +Current reserved standard subpaths expose only `unavailable()` until their +leaf package exists. ### Load Boundaries diff --git a/documentation/desired-concept/architecture/reporters.md b/documentation/desired-concept/architecture/reporters.md index abb0ad45..158ba2cc 100644 --- a/documentation/desired-concept/architecture/reporters.md +++ b/documentation/desired-concept/architecture/reporters.md @@ -224,17 +224,13 @@ Config should prefer explicit imported values, for example: ```ts import { defineConfig } from '@overkill-dev/test/config'; import { - benchmarkHtmlReporter, - lineReporter + createGithubActionsOutputRenderer, + createLineReporter } from '@overkill-dev/test/reporters'; export const config = defineConfig({ - reporters: [ - lineReporter(), - benchmarkHtmlReporter({ - outputDir: '.overkill/bench-report' - }) - ] + outputRenderer: createGithubActionsOutputRenderer(), + reporters: [ createLineReporter() ] }); ``` @@ -243,7 +239,10 @@ convention and no package-name lookup magic in the settled concept. `@overkill-dev/test/reporters` may re-export built-in reporter factories so standard users do not need to import each built-in reporter from its leaf -package by default. +package by default. The current standard distribution exports +`createLineReporter`, `createBriefReporter`, `createDotReporter`, and +`createGithubActionsOutputRenderer`; benchmark-specific and artifact reporters +arrive with their leaf packages. ## Multi-Reporter Composition diff --git a/documentation/desired-concept/overview.md b/documentation/desired-concept/overview.md index 0728ba32..0e593da1 100644 --- a/documentation/desired-concept/overview.md +++ b/documentation/desired-concept/overview.md @@ -273,8 +273,9 @@ The standard distribution is a packaging convenience, not the architectural source of truth. The root `@overkill-dev/test` import is reserved for the authoring hot path and lightweight doubles. Other standard features are available through explicit subpaths such as `@overkill-dev/test/config`, -`@overkill-dev/test/reporters`, `@overkill-dev/test/bench`, and -`@overkill-dev/test/assert`. +`@overkill-dev/test/reporters`, `@overkill-dev/test/assert`, +`@overkill-dev/test/bench`, `@overkill-dev/test/resources`, and +`@overkill-dev/test/baselines`. Optional packages stay separate installs when they are domain-specific, heavyweight, or aimed at a narrower audience. That includes browser and diff --git a/justfile b/justfile index 3ff8691b..cb31e942 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ export PATH := './node_modules/.bin:' + env_var('PATH') -package-smoke-packages := '@overkill-dev/engine,@overkill-dev/assert,@overkill-dev/doubles,@overkill-dev/run,@overkill-dev/test,@overkill-dev/reporter-line' +package-smoke-packages := '@overkill-dev/engine,@overkill-dev/assert,@overkill-dev/doubles,@overkill-dev/run,@overkill-dev/test,@overkill-dev/reporter-line,@overkill-dev/reporter-brief,@overkill-dev/reporter-dot,@overkill-dev/output-renderer-github-actions' default: @just --list diff --git a/packtory.config.js b/packtory.config.js index 1f8620df..7f150b49 100644 --- a/packtory.config.js +++ b/packtory.config.js @@ -144,6 +144,10 @@ export const config = { js: 'packages/run/command-line.entry-point.js', declarationFile: 'packages/run/command-line.entry-point.d.ts' }, + config: { + js: 'packages/run/config.entry-point.js', + declarationFile: 'packages/run/config.entry-point.d.ts' + }, filters: { js: 'packages/run/filters.entry-point.js', declarationFile: 'packages/run/filters.entry-point.d.ts' @@ -163,6 +167,10 @@ export const config = { export: './command-line', root: 'commandLine' }, + { + export: './config', + root: 'config' + }, { export: './filters', root: 'filters' @@ -182,14 +190,46 @@ export const config = { }, { name: '@overkill-dev/test', - bundleDependencies: [ '@overkill-dev/engine', '@overkill-dev/reporter-line', '@overkill-dev/run' ], + bundleDependencies: [ + '@overkill-dev/assert', + '@overkill-dev/engine', + '@overkill-dev/output-renderer-github-actions', + '@overkill-dev/reporter-brief', + '@overkill-dev/reporter-dot', + '@overkill-dev/reporter-line', + '@overkill-dev/run' + ], roots: { + assert: { + js: 'packages/test/assert.entry-point.js', + declarationFile: 'packages/test/assert.entry-point.d.ts' + }, + baselines: { + js: 'packages/test/baselines.entry-point.js', + declarationFile: 'packages/test/baselines.entry-point.d.ts' + }, + bench: { + js: 'packages/test/bench.entry-point.js', + declarationFile: 'packages/test/bench.entry-point.d.ts' + }, + config: { + js: 'packages/test/config.entry-point.js', + declarationFile: 'packages/test/config.entry-point.d.ts' + }, main: { js: 'packages/test/test.entry-point.js', declarationFile: 'packages/test/test.entry-point.d.ts' }, overkill: { js: 'packages/test/overkill.entry-point.js' + }, + reporters: { + js: 'packages/test/reporters.entry-point.js', + declarationFile: 'packages/test/reporters.entry-point.d.ts' + }, + resources: { + js: 'packages/test/resources.entry-point.js', + declarationFile: 'packages/test/resources.entry-point.d.ts' } }, packageInterface: { @@ -197,6 +237,30 @@ export const config = { { export: '.', root: 'main' + }, + { + export: './assert', + root: 'assert' + }, + { + export: './baselines', + root: 'baselines' + }, + { + export: './bench', + root: 'bench' + }, + { + export: './config', + root: 'config' + }, + { + export: './reporters', + root: 'reporters' + }, + { + export: './resources', + root: 'resources' } ], bins: [ diff --git a/source/integration-tests/package-smoke/test-binary.test.ts b/source/integration-tests/package-smoke/test-binary.test.ts index cec2bf37..c967d36f 100644 --- a/source/integration-tests/package-smoke/test-binary.test.ts +++ b/source/integration-tests/package-smoke/test-binary.test.ts @@ -76,6 +76,50 @@ const rootImportScript = [ '}' ] .join('\n'); +const standardSubpathImportScript = [ + "const configModule = await import('@overkill-dev/test/config');", + "const reportersModule = await import('@overkill-dev/test/reporters');", + "const assertModule = await import('@overkill-dev/test/assert');", + "const benchModule = await import('@overkill-dev/test/bench');", + "const resourcesModule = await import('@overkill-dev/test/resources');", + "const baselinesModule = await import('@overkill-dev/test/baselines');", + 'console.log(JSON.stringify(Object.keys(configModule)));', + 'console.log(JSON.stringify(Object.keys(reportersModule)));', + 'console.log(JSON.stringify(Object.keys(assertModule)));', + 'console.log(configModule.defineConfig({ profiles: {} }).profiles === undefined);', + 'console.log(reportersModule.createLineReporter().name);', + 'console.log(reportersModule.createBriefReporter().name);', + 'const dotReporter = reportersModule.createDotReporter();', + 'console.log(dotReporter.name);', + 'if (dotReporter.dispose !== null) {', + ' dotReporter.dispose();', + '}', + 'console.log(reportersModule.createGithubActionsOutputRenderer().render({', + " annotation: null, kind: 'stdout-line', role: 'primary', text: 'hello'", + '}));', + 'console.log(typeof assertModule.defineCompositeAssertion);', + 'for (const [name, module] of [', + " [ 'bench', benchModule ],", + " [ 'resources', resourcesModule ],", + " [ 'baselines', baselinesModule ]", + ']) {', + ' console.log(JSON.stringify(Object.keys(module)));', + ' try {', + ' module.unavailable();', + ' } catch (error) {', + ' console.log(error instanceof Error ? error.message : String(error));', + ' }', + '}' +] + .join('\n'); +const runConfigImportScript = [ + "const configModule = await import('@overkill-dev/run/config');", + 'console.log(JSON.stringify(Object.keys(configModule)));', + 'console.log(configModule.defineConfig({ profiles: {} }).profiles === undefined);', + 'console.log(typeof configModule.loadRunConfig);', + "console.log(new configModule.RunConfigError('Invalid config.').name);" +] + .join('\n'); const expectedRootImportOutput = [ '["createTestFacade","defineMacro","runIfMain","suite","table","test"]', 'undefined', @@ -90,6 +134,33 @@ const expectedRootImportOutput = [ '' ] .join('\n'); +const expectedStandardSubpathImportOutput = [ + '["defineConfig"]', + '["createBriefReporter","createDotReporter","createGithubActionsOutputRenderer","createLineReporter"]', + '["defineCompositeAssertion","defineNarrowingCompositeAssertion"]', + 'false', + 'line', + 'brief', + 'dot', + 'hello', + 'function', + '["unavailable"]', + 'The @overkill-dev/test/bench subpath is reserved until its leaf package exists.', + '["unavailable"]', + 'The @overkill-dev/test/resources subpath is reserved until its leaf package exists.', + '["unavailable"]', + 'The @overkill-dev/test/baselines subpath is reserved until its leaf package exists.', + '' +] + .join('\n'); +const expectedRunConfigImportOutput = [ + '["RunConfigError","defineConfig","loadRunConfig"]', + 'false', + 'function', + 'RunConfigError', + '' +] + .join('\n'); async function readPackageJson(packageFolder: string): Promise { return JSON.parse(await fs.readFile(path.join(packageFolder, 'package.json'), 'utf8')) as PackageJson; @@ -103,6 +174,20 @@ function isPackageExportsMap(value: unknown): value is Readonly>> { + const packageJson = await readPackageJson(packageFolder); + const packageExports = packageJson.exports; + + if (!isPackageExportsMap(packageExports)) { + throw new Error(`Expected ${packageName} exports map.`); + } + + return packageExports; +} + async function collectStream(stream: Readable): Promise { return await new Promise(function collect(resolve, reject) { const chunks: Buffer[] = []; @@ -142,6 +227,65 @@ async function importPackagedFilters(): Promise { return await import(pathToFileURL(modulePath).href) as FiltersModule; } +function assertTestStandardSubpathExports(scope: TestScope, packageExports: Readonly>): void { + scope.assert.deepEqual(packageExports['./config'], { + import: './packages/test/config.entry-point.js', + types: './packages/test/config.entry-point.d.ts' + }); + scope.assert.deepEqual(packageExports['./reporters'], { + import: './packages/test/reporters.entry-point.js', + types: './packages/test/reporters.entry-point.d.ts' + }); + scope.assert.deepEqual(packageExports['./assert'], { + import: './packages/test/assert.entry-point.js', + types: './packages/test/assert.entry-point.d.ts' + }); + scope.assert.deepEqual(packageExports['./bench'], { + import: './packages/test/bench.entry-point.js', + types: './packages/test/bench.entry-point.d.ts' + }); + scope.assert.deepEqual(packageExports['./resources'], { + import: './packages/test/resources.entry-point.js', + types: './packages/test/resources.entry-point.d.ts' + }); + scope.assert.deepEqual(packageExports['./baselines'], { + import: './packages/test/baselines.entry-point.js', + types: './packages/test/baselines.entry-point.d.ts' + }); +} + +function assertRunConfigSubpathExport(scope: TestScope, packageExports: Readonly>): void { + scope.assert.deepEqual(packageExports['./config'], { + import: './packages/run/config.entry-point.js', + types: './packages/run/config.entry-point.d.ts' + }); +} + +function assertPackagedFilters(scope: TestScope, filters: FiltersModule): void { + const { all, file, not, parseRunFilterExpression, tag } = filters; + + scope.assert.deepEqual(all([ tag('fast'), not(file('source/**')) ]), { + filters: [ + { field: 'tag', kind: 'equals', value: 'fast' }, + { + filter: { field: 'file', kind: 'glob', pattern: 'source/**' }, + kind: 'not' + } + ], + kind: 'all' + }); + scope.assert.deepEqual(parseRunFilterExpression('tag=fast !tag=flaky'), { + filters: [ + { field: 'tag', kind: 'equals', value: 'fast' }, + { + filter: { field: 'tag', kind: 'equals', value: 'flaky' }, + kind: 'not' + } + ], + kind: 'all' + }); +} + async function writeAuthoringSmokeFile(): Promise { await fs.writeFile(path.join(packageSmokeFolder, authoringSmokeFile), authoringSmokeScript); } @@ -197,6 +341,30 @@ export const testSuite = createSuite({ return scope.assert.collect(); } }), + createTestCase({ + name: 'consumer imports packaged @overkill-dev/test standard subpaths', + metadata: {}, + async body(scope: TestScope) { + const testPackageJson = await readPackageJson(testPackageFolder); + const packageExports = testPackageJson.exports; + const result = await spawnNode([ + '--input-type=module', + '--eval', + standardSubpathImportScript + ]); + + if (!isPackageExportsMap(packageExports)) { + throw new Error('Expected @overkill-dev/test exports map.'); + } + + assertTestStandardSubpathExports(scope, packageExports); + scope.assert.equal(result.code, 0); + scope.assert.equal(result.stderr, ''); + scope.assert.equal(result.stdout, expectedStandardSubpathImportOutput); + + return scope.assert.collect(); + } + }), createTestCase({ name: 'packaged overkill binary prints command help', metadata: {}, @@ -258,28 +426,19 @@ export const testSuite = createSuite({ name: 'consumer imports packaged @overkill-dev/run/filters helpers', metadata: {}, async body(scope: TestScope) { - const { all, file, not, parseRunFilterExpression, tag } = await importPackagedFilters(); - - scope.assert.deepEqual(all([ tag('fast'), not(file('source/**')) ]), { - filters: [ - { field: 'tag', kind: 'equals', value: 'fast' }, - { - filter: { field: 'file', kind: 'glob', pattern: 'source/**' }, - kind: 'not' - } - ], - kind: 'all' - }); - scope.assert.deepEqual(parseRunFilterExpression('tag=fast !tag=flaky'), { - filters: [ - { field: 'tag', kind: 'equals', value: 'fast' }, - { - filter: { field: 'tag', kind: 'equals', value: 'flaky' }, - kind: 'not' - } - ], - kind: 'all' - }); + const packageExports = await readPackageExports(runPackageFolder, '@overkill-dev/run'); + const result = await spawnNode([ + '--input-type=module', + '--eval', + runConfigImportScript + ]); + const filters = await importPackagedFilters(); + + assertRunConfigSubpathExport(scope, packageExports); + scope.assert.equal(result.code, 0); + scope.assert.equal(result.stderr, ''); + scope.assert.equal(result.stdout, expectedRunConfigImportOutput); + assertPackagedFilters(scope, filters); return scope.assert.collect(); } diff --git a/source/packages/run/config.entry-point.ts b/source/packages/run/config.entry-point.ts new file mode 100644 index 00000000..bae8264e --- /dev/null +++ b/source/packages/run/config.entry-point.ts @@ -0,0 +1,20 @@ +export { + defineConfig, + loadRunConfig, + RunConfigError +} from '../../run/run-config.ts'; +export type { + LoadedRunConfig, + RunConfigLoadRequest, + RunProjectConfig, + RunProjectMeasuredResourceUsage, + RunProjectMicrotestExecution, + RunProjectMicrotestProfileConfig, + RunProjectProfileConfig, + RunProjectProfileFiles, + RunProjectProfilesConfig, + RunProjectResourceBudgets, + RunProjectResourceUsageConfig, + RunProjectTimeoutConfig, + RunProjectUnmeasuredResourceUsage +} from '../../run/run-config.ts'; diff --git a/source/packages/run/config.type-test.ts b/source/packages/run/config.type-test.ts new file mode 100644 index 00000000..7a97c614 --- /dev/null +++ b/source/packages/run/config.type-test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'tstyche'; +import type { DefinedReporter } from '../engine/engine.entry-point.ts'; +import { + RunConfigError, + type defineConfig, + type loadRunConfig, + type LoadedRunConfig, + type RunConfigLoadRequest, + type RunProjectConfig +} from './config.entry-point.ts'; + +describe('@overkill-dev/run/config', function () { + test('exposes configuration loading and authoring types', function () { + expect().type.toBe<(config: RunProjectConfig) => RunProjectConfig>(); + expect().type.toBe< + (request: RunConfigLoadRequest) => Promise + >(); + expect().type.toBe< + readonly [DefinedReporter, ...DefinedReporter[]] | undefined + >(); + expect(new RunConfigError('Invalid config.')).type.toBe(); + }); +}); diff --git a/source/packages/run/readme.md b/source/packages/run/readme.md index d39dc0bd..c992c812 100644 --- a/source/packages/run/readme.md +++ b/source/packages/run/readme.md @@ -16,6 +16,10 @@ Top-level API: - `orchestrator.run(command)` - `orchestrator.runWithReporterDelivery(command)` +Configuration loading and authoring are also exposed through +`@overkill-dev/run/config` for packages that need the config-owned surface +without importing orchestration. + Command-line business logic is exposed through `@overkill-dev/run/command-line`: - `commandLineRunner.runTests({ cwd, configPath, runRequest })` diff --git a/source/packages/test/assert.entry-point.ts b/source/packages/test/assert.entry-point.ts new file mode 100644 index 00000000..1226643f --- /dev/null +++ b/source/packages/test/assert.entry-point.ts @@ -0,0 +1,23 @@ +export { + defineCompositeAssertion, + defineNarrowingCompositeAssertion +} from '../assert/assert.entry-point.ts'; +export type { + AssertReferenceArguments, + AssertReferenceReturn, + AssertionSource, + CompositeAssertionDefinition, + CompositeAssertionGroup, + CompositeAssertionReference, + CompositeAssertionReturn, + CompositeAssertionSummaryContext, + CompositeCheckBuilder, + DeepComparable, + ErrorMatcher, + ExactThrownMatcher, + NarrowingCompositeAssertionDefinition, + NarrowingCompositeAssertionReference, + NonEmptyReadonlyArray, + SynchronousCallback, + ThrownMatcher +} from '../assert/assert.entry-point.ts'; diff --git a/source/packages/test/baselines.entry-point.ts b/source/packages/test/baselines.entry-point.ts new file mode 100644 index 00000000..328f106e --- /dev/null +++ b/source/packages/test/baselines.entry-point.ts @@ -0,0 +1,3 @@ +import { createUnavailableStandardSubpathApi } from './reserved-standard-subpath.ts'; + +export const unavailable = createUnavailableStandardSubpathApi('baselines'); diff --git a/source/packages/test/bench.entry-point.ts b/source/packages/test/bench.entry-point.ts new file mode 100644 index 00000000..07b0822e --- /dev/null +++ b/source/packages/test/bench.entry-point.ts @@ -0,0 +1,3 @@ +import { createUnavailableStandardSubpathApi } from './reserved-standard-subpath.ts'; + +export const unavailable = createUnavailableStandardSubpathApi('bench'); diff --git a/source/packages/test/config.entry-point.ts b/source/packages/test/config.entry-point.ts new file mode 100644 index 00000000..a7976e21 --- /dev/null +++ b/source/packages/test/config.entry-point.ts @@ -0,0 +1,14 @@ +export { defineConfig } from '../run/config.entry-point.ts'; +export type { + RunProjectConfig, + RunProjectMeasuredResourceUsage, + RunProjectMicrotestExecution, + RunProjectMicrotestProfileConfig, + RunProjectProfileConfig, + RunProjectProfileFiles, + RunProjectProfilesConfig, + RunProjectResourceBudgets, + RunProjectResourceUsageConfig, + RunProjectTimeoutConfig, + RunProjectUnmeasuredResourceUsage +} from '../run/config.entry-point.ts'; diff --git a/source/packages/test/readme.md b/source/packages/test/readme.md index e78bc749..e4b85a36 100644 --- a/source/packages/test/readme.md +++ b/source/packages/test/readme.md @@ -15,6 +15,19 @@ Current root runtime exports: - `createTestFacade` - `runIfMain` +Standard subpaths: + +- `@overkill-dev/test/config` exports `defineConfig` and run project config + types. +- `@overkill-dev/test/reporters` exports `createLineReporter`, + `createBriefReporter`, `createDotReporter`, and + `createGithubActionsOutputRenderer`. +- `@overkill-dev/test/assert` re-exports assertion-extension helpers from + `@overkill-dev/assert`. +- `@overkill-dev/test/bench`, `@overkill-dev/test/resources`, and + `@overkill-dev/test/baselines` are reserved. They currently export only + `unavailable()`. + Implemented root authoring forms: ```ts @@ -80,5 +93,3 @@ When no paths are supplied, `run` and `list` discover files from the selected profile's `files.include` and `files.exclude` policy. Explicit file paths run directly. Directory paths filter the selected profile's discovered files and require that profile policy. - -Standard subpaths are later milestones. diff --git a/source/packages/test/reporters.entry-point.ts b/source/packages/test/reporters.entry-point.ts new file mode 100644 index 00000000..83effb03 --- /dev/null +++ b/source/packages/test/reporters.entry-point.ts @@ -0,0 +1,7 @@ +export { + createGithubActionsOutputRenderer +} from '../output-renderer-github-actions/output-renderer-github-actions.entry-point.ts'; +export { createBriefReporter } from '../reporter-brief/reporter-brief.entry-point.ts'; +export type { BriefReporterSinks } from '../reporter-brief/reporter-brief.entry-point.ts'; +export { createDotReporter } from '../reporter-dot/reporter-dot.entry-point.ts'; +export { createLineReporter } from '../reporter-line/reporter-line.entry-point.ts'; diff --git a/source/packages/test/reserved-standard-subpath.ts b/source/packages/test/reserved-standard-subpath.ts new file mode 100644 index 00000000..278671c0 --- /dev/null +++ b/source/packages/test/reserved-standard-subpath.ts @@ -0,0 +1,7 @@ +export type UnavailableStandardSubpathApi = (...parameters: readonly unknown[]) => never; + +export function createUnavailableStandardSubpathApi(subpath: string): UnavailableStandardSubpathApi { + return function unavailableStandardSubpathApi(): never { + throw new Error(`The @overkill-dev/test/${subpath} subpath is reserved until its leaf package exists.`); + }; +} diff --git a/source/packages/test/resources.entry-point.ts b/source/packages/test/resources.entry-point.ts new file mode 100644 index 00000000..aef99e14 --- /dev/null +++ b/source/packages/test/resources.entry-point.ts @@ -0,0 +1,3 @@ +import { createUnavailableStandardSubpathApi } from './reserved-standard-subpath.ts'; + +export const unavailable = createUnavailableStandardSubpathApi('resources'); diff --git a/source/packages/test/standard-subpaths.test.ts b/source/packages/test/standard-subpaths.test.ts new file mode 100644 index 00000000..4f800984 --- /dev/null +++ b/source/packages/test/standard-subpaths.test.ts @@ -0,0 +1,140 @@ +import { createSuite, createTestCase, runIfMain, type TestScope } from '@overkill-dev/engine'; +import { createLineReporter } from '@overkill-dev/reporter-line'; +import { defineCompositeAssertion } from './assert.entry-point.ts'; +import * as assertSubpath from './assert.entry-point.ts'; +import * as baselinesSubpath from './baselines.entry-point.ts'; +import * as benchSubpath from './bench.entry-point.ts'; +import { defineConfig } from './config.entry-point.ts'; +import * as configSubpath from './config.entry-point.ts'; +import * as reportersSubpath from './reporters.entry-point.ts'; +import * as resourcesSubpath from './resources.entry-point.ts'; + +type ReservedSubpathModule = { + readonly name: string; + readonly module: { + readonly unavailable: (...parameters: readonly unknown[]) => never; + }; +}; + +const reservedSubpathModules: readonly ReservedSubpathModule[] = [ + { module: baselinesSubpath, name: 'baselines' }, + { module: benchSubpath, name: 'bench' }, + { module: resourcesSubpath, name: 'resources' } +]; + +function sortedKeys(value: Readonly>): readonly string[] { + return Object.keys(value).toSorted(function compareExportNames(left, right) { + return left.localeCompare(right); + }); +} + +function assertConfigSubpath(scope: TestScope): void { + const config = { + profiles: { + unit: { + files: { + include: [ 'source/**/*.test.ts' ] + }, + testFamily: 'microtest' + } + } + } as const; + + scope.assert.deepEqual(sortedKeys(configSubpath), [ 'defineConfig' ]); + scope.assert.equal(defineConfig(config), config); + scope.assert.equal(Object.hasOwn(configSubpath, 'orchestrator'), false); + scope.assert.equal(Object.hasOwn(configSubpath, 'loadRunConfig'), false); +} + +async function assertReporterSubpath(scope: TestScope): Promise { + const line = reportersSubpath.createLineReporter(); + const brief = reportersSubpath.createBriefReporter(); + const dot = reportersSubpath.createDotReporter(); + const githubActions = reportersSubpath.createGithubActionsOutputRenderer(); + + scope.assert.deepEqual(sortedKeys(reportersSubpath), [ + 'createBriefReporter', + 'createDotReporter', + 'createGithubActionsOutputRenderer', + 'createLineReporter' + ]); + scope.assert.deepEqual([ + line.name, + brief.name, + dot.name, + githubActions.render({ + annotation: null, + kind: 'stdout-line', + role: 'primary', + text: 'hello' + }) + ], [ 'line', 'brief', 'dot', 'hello' ]); + + if (dot.dispose !== null) { + await dot.dispose(); + } +} + +function assertAssertSubpath(scope: TestScope): void { + scope.assert.deepEqual(sortedKeys(assertSubpath), [ + 'defineCompositeAssertion', + 'defineNarrowingCompositeAssertion' + ]); + scope.assert.equal(typeof defineCompositeAssertion, 'function'); +} + +function assertReservedSubpath(scope: TestScope, subpath: ReservedSubpathModule): void { + scope.assert.deepEqual(sortedKeys(subpath.module), [ 'unavailable' ]); + scope.assert.throws(function invokeUnavailableSubpath() { + subpath.module.unavailable('ignored'); + }, { + message: `The @overkill-dev/test/${subpath.name} subpath is reserved until its leaf package exists.` + }); +} + +export const testSuite = createSuite({ + name: 'source/packages/test/standard-subpaths.test.ts', + metadata: {}, + children: [ + createTestCase({ + name: '@overkill-dev/test/config exposes config authoring only', + metadata: {}, + body(scope: TestScope) { + assertConfigSubpath(scope); + + return scope.assert.collect(); + } + }), + createTestCase({ + name: '@overkill-dev/test/reporters exposes current built-in factories', + metadata: {}, + async body(scope: TestScope) { + await assertReporterSubpath(scope); + + return scope.assert.collect(); + } + }), + createTestCase({ + name: '@overkill-dev/test/assert re-exports assertion extension ownership', + metadata: {}, + body(scope: TestScope) { + assertAssertSubpath(scope); + + return scope.assert.collect(); + } + }), + createTestCase({ + name: '@overkill-dev/test reserved subpaths expose sentinel only', + metadata: {}, + body(scope: TestScope) { + for (const subpath of reservedSubpathModules) { + assertReservedSubpath(scope, subpath); + } + + return scope.assert.collect(); + } + }) + ] +}); + +await runIfMain(import.meta, testSuite, { reporters: [ createLineReporter() ] }); diff --git a/source/packages/test/standard-subpaths.type-test.ts b/source/packages/test/standard-subpaths.type-test.ts new file mode 100644 index 00000000..a543b8b1 --- /dev/null +++ b/source/packages/test/standard-subpaths.type-test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from 'tstyche'; +import type { + DefinedOutputRenderer, + DefinedReporter, + RealTimeReporter +} from '../engine/engine.entry-point.ts'; +import type { + CompositeAssertionDefinition, + CompositeCheckBuilder, + NarrowingCompositeAssertionDefinition +} from './assert.entry-point.ts'; +import type { unavailable as baselinesUnavailable } from './baselines.entry-point.ts'; +import type { unavailable as benchUnavailable } from './bench.entry-point.ts'; +import type { + defineConfig, + RunProjectConfig, + RunProjectMicrotestProfileConfig, + RunProjectProfileConfig, + RunProjectProfileFiles, + RunProjectResourceBudgets +} from './config.entry-point.ts'; +import type { + BriefReporterSinks, + createBriefReporter, + createDotReporter, + createGithubActionsOutputRenderer, + createLineReporter +} from './reporters.entry-point.ts'; +import type { unavailable as resourcesUnavailable } from './resources.entry-point.ts'; + +type UnavailableStandardSubpathApi = (...parameters: readonly unknown[]) => never; +type CompositeBooleanDefinition = CompositeAssertionDefinition< + [value: boolean], + ReturnType['true']> +>; +type NarrowingStringDefinition = NarrowingCompositeAssertionDefinition; + +describe('@overkill-dev/test standard subpaths', function () { + test('exposes config authoring types', function () { + expect().type.toBe<(config: RunProjectConfig) => RunProjectConfig>(); + expect().type.toBe(); + expect().type.toBe(); + expect().type.toBe< + 'activeResourceCount' | 'javaScriptEngineHeapBytes' | 'residentSetBytes' | 'residentSetGrowthBytesPerSecond' + >(); + }); + + test('exposes reporter factories through the standard distribution', function () { + expect().type.toBe<() => DefinedReporter>(); + expect().type.toBe< + () => DefinedReporter> + >(); + expect().type.toBe<() => DefinedReporter>(); + expect().type.toBe<() => DefinedOutputRenderer>(); + }); + + test('exposes assertion extension types from the assert package', function () { + expect().type.toBe(); + expect().type.toBe<(actual: unknown) => actual is string>(); + expect['true']>().type.toBe< + (actual: unknown) => ReturnType['true']> + >(); + }); + + test('exposes only unavailable sentinel types for reserved subpaths', function () { + expect().type.toBe(); + expect().type.toBe(); + expect().type.toBe(); + }); +}); diff --git a/source/packages/test/test-suite.test.ts b/source/packages/test/test-suite.test.ts index a0414d8c..f57372b8 100644 --- a/source/packages/test/test-suite.test.ts +++ b/source/packages/test/test-suite.test.ts @@ -1,5 +1,6 @@ import { createSuite } from '@overkill-dev/engine'; import { testSuite as commandLineRunnerTestSuite } from './command-line-runner.test.ts'; +import { testSuite as standardSubpathsTestSuite } from './standard-subpaths.test.ts'; import { testSuite as entryPointTestSuite } from './test-entry-point.test.ts'; export const testSuite = createSuite({ @@ -7,6 +8,7 @@ export const testSuite = createSuite({ metadata: {}, children: [ commandLineRunnerTestSuite, + standardSubpathsTestSuite, entryPointTestSuite ] });