Skip to content

Commit

Permalink
feat(attest): inline instantiations (#938)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShawnMorreau authored May 1, 2024
1 parent 764aa53 commit 2c99448
Show file tree
Hide file tree
Showing 23 changed files with 826 additions and 614 deletions.
2 changes: 1 addition & 1 deletion ark/attest/__tests__/assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as assert from "node:assert/strict"

const o = { ark: "type" }

specify(() => {
specify("type assertions", () => {
it("type parameter", () => {
attest<{ ark: string }>(o)
assert.throws(
Expand Down
4 changes: 2 additions & 2 deletions ark/attest/__tests__/benchExpectedOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type makeComplexType<S extends string> =

bench("bench type", () => {
return {} as makeComplexType<"defenestration">
}).types([177, "instantiations"])
}).types([176, "instantiations"])

bench("bench type from external module", () => {
return {} as externalmakeComplexType<"defenestration">
Expand All @@ -51,6 +51,6 @@ bench(
fakeCallOptions
)
.mean([2, "ms"])
.types([345, "instantiations"])
.types([344, "instantiations"])

bench("empty", () => {}).types([0, "instantiations"])
24 changes: 12 additions & 12 deletions ark/attest/__tests__/demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,17 @@ contextualize(() => {
// works for keys or index access as well (may need prettier-ignore to
// avoid removing quotes)
// prettier-ignore
attest({ f: "🐐" } as Legends).completions({ f: ["faker"] })
})

it("integrate runtime logic with type assertions", () => {
const arrayOf = type("<t>", "t[]")
const numericArray = arrayOf("number | bigint")
// flexibly combine runtime logic with type assertions to customize your
// tests beyond what is possible from pure static-analysis based type testing tools
if (getPrimaryTsVersionUnderTest().startsWith("5")) {
// this assertion will only occur when testing TypeScript 5+!
attest<(number | bigint)[]>(numericArray.infer)
}
attest({ "f": "🐐" } as Legends).completions({ f: ["faker"] })
})
// TODO: reenable once generics are finished
// it("integrate runtime logic with type assertions", () => {
// const arrayOf = type("<t>", "t[]")
// const numericArray = arrayOf("number | bigint")
// // flexibly combine runtime logic with type assertions to customize your
// // tests beyond what is possible from pure static-analysis based type testing tools
// if (getPrimaryTsVersionUnderTest().startsWith("5")) {
// // this assertion will only occur when testing TypeScript 5+!
// attest<(number | bigint)[]>(numericArray.infer)
// }
// })
})
10 changes: 10 additions & 0 deletions ark/attest/__tests__/instantiations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { attest, contextualize } from "@arktype/attest"
import { type } from "arktype"
import { it } from "mocha"

contextualize(() => {
it("Inline instantiations", () => {
type("string")
attest.instantiations([1968, "instantiations"])
})
})
14 changes: 7 additions & 7 deletions ark/attest/__tests__/snapExpectedOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import { attest, cleanup, setup } from "@arktype/attest"

setup()

attest({ re: "do" }).equals({ re: "do" }).type.toString.snap(`{ re: string; }`)
attest({ re: "do" }).equals({ re: "do" }).type.toString.snap("{ re: string; }")

attest(5).snap(5)

attest({ re: "do" }).snap({ re: `do` })
attest({ re: "do" }).snap({ re: "do" })

// @ts-expect-error (using internal updateSnapshots hook)
attest({ re: "dew" }, { updateSnapshots: true }).snap({ re: `dew` })
attest({ re: "dew" }, { cfg: { updateSnapshots: true } }).snap({ re: "dew" })

// @ts-expect-error (using internal updateSnapshots hook)
attest(5, { updateSnapshots: true }).snap(5)
attest(5, { cfg: { updateSnapshots: true } }).snap(5)

attest(undefined).snap(`(undefined)`)
attest(undefined).snap("(undefined)")

attest({ a: undefined }).snap({ a: `(undefined)` })
attest({ a: undefined }).snap({ a: "(undefined)" })

attest("multiline\nmultiline").snap(`multiline
multiline`)

attest("with `quotes`").snap(`with \`quotes\``)
attest("with `quotes`").snap("with `quotes`")

cleanup()
4 changes: 2 additions & 2 deletions ark/attest/__tests__/snapPopulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ contextualize(() => {
fromHere("benchExpectedOutput.ts")
).replaceAll("\r\n", "\n")
equal(actual, expectedOutput)
})
}).timeout(10000)

it("snap populates file", () => {
const actual = runThenGetContents(fromHere("snapTemplate.ts"))
const expectedOutput = readFile(
fromHere("snapExpectedOutput.ts")
).replaceAll("\r\n", "\n")
equal(actual, expectedOutput)
})
}).timeout(10000)
})
4 changes: 2 additions & 2 deletions ark/attest/__tests__/snapTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ attest(5).snap()
attest({ re: "do" }).snap()

// @ts-expect-error (using internal updateSnapshots hook)
attest({ re: "dew" }, { updateSnapshots: true }).snap()
attest({ re: "dew" }, { cfg: { updateSnapshots: true } }).snap({ re: "do" })

// @ts-expect-error (using internal updateSnapshots hook)
attest(5, { updateSnapshots: true }).snap(6)
attest(5, { cfg: { updateSnapshots: true } }).snap(6)

attest(undefined).snap()

Expand Down
8 changes: 4 additions & 4 deletions ark/attest/assert/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { printable, throwInternalError } from "@arktype/util"
import { AssertionError } from "node:assert"
import * as assert from "node:assert/strict"
import type { TypeAssertionData } from "../cache/writeAssertionCache.js"
import type { TypeRelationshipAssertionData } from "../cache/writeAssertionCache.js"
import type { AssertionContext } from "./attest.js"

export type ThrowAssertionErrorContext = {
Expand Down Expand Up @@ -34,7 +34,7 @@ export type MappedTypeAssertionResult = {
export class TypeAssertionMapping {
constructor(
public fn: (
data: TypeAssertionData,
data: TypeRelationshipAssertionData,
ctx: AssertionContext
) => MappedTypeAssertionResult
) {}
Expand All @@ -44,12 +44,12 @@ export const versionableAssertion =
(fn: AssertFn): AssertFn =>
(expected, actual, ctx) => {
if (actual instanceof TypeAssertionMapping) {
if (!ctx.typeAssertionEntries) {
if (!ctx.typeRelationshipAssertionEntries) {
throwInternalError(
`Unexpected missing typeAssertionEntries when passed a TypeAssertionMapper`
)
}
for (const [version, data] of ctx.typeAssertionEntries!) {
for (const [version, data] of ctx.typeRelationshipAssertionEntries) {
let errorMessage = ""
try {
const mapped = actual.fn(data, ctx)
Expand Down
34 changes: 29 additions & 5 deletions ark/attest/assert/attest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { caller, getCallStack, type SourcePosition } from "@arktype/fs"
import type { inferTypeRoot, validateTypeRoot } from "arktype"
import { getBenchCtx } from "../bench/bench.js"
import type { Measure } from "../bench/measure.js"
import { instantiationDataHandler } from "../bench/type.js"
import {
getTypeAssertionsAtPosition,
getTypeRelationshipAssertionsAtPosition,
type VersionedTypeAssertion
} from "../cache/getCachedAssertions.js"
import type {
TypeBenchmarkingAssertionData,
TypeRelationshipAssertionData
} from "../cache/writeAssertionCache.js"
import { getConfig, type AttestConfig } from "../config.js"
import { assertEquals, typeEqualityMapping } from "./assertions.js"
import {
Expand All @@ -22,6 +29,8 @@ export type AttestFn = {
def: validateTypeRoot<def>
): asserts actual is unknown extends actual ? inferTypeRoot<def> & actual
: Extract<actual, inferTypeRoot<def>>

instantiations: (count?: Measure<"instantiations"> | undefined) => void
}

export type AssertionContext = {
Expand All @@ -32,7 +41,8 @@ export type AssertionContext = {
position: SourcePosition
defaultExpected?: unknown
assertionStack: string
typeAssertionEntries?: VersionedTypeAssertion[]
typeRelationshipAssertionEntries?: VersionedTypeAssertion<TypeRelationshipAssertionData>[]
typeBenchmarkingAssertionEntries?: VersionedTypeAssertion<TypeBenchmarkingAssertionData>[]
lastSnapName?: string
}

Expand All @@ -57,13 +67,27 @@ export const attestInternal = (
...ctxHooks
}
if (!cfg.skipTypes) {
ctx.typeAssertionEntries = getTypeAssertionsAtPosition(position)
if (ctx.typeAssertionEntries[0]?.[1].typeArgs[0]) {
ctx.typeRelationshipAssertionEntries =
getTypeRelationshipAssertionsAtPosition(position)
if (ctx.typeRelationshipAssertionEntries[0][1].typeArgs[0]) {
// if there is an expected type arg, check it immediately
assertEquals(undefined, typeEqualityMapping, ctx)
}
}
return new ChainableAssertions(ctx)
}

export const attest: AttestFn = attestInternal as never
attestInternal.instantiations = (
args: Measure<"instantiations"> | undefined
) => {
const attestConfig = getConfig()
if (attestConfig.skipInlineInstantiations) return

const calledFrom = caller()
const ctx = getBenchCtx([calledFrom.file])
ctx.benchCallPosition = calledFrom
ctx.lastSnapCallPosition = calledFrom
instantiationDataHandler({ ...ctx, kind: "instantiations" }, args, false)
}

export const attest: AttestFn = attestInternal as AttestFn
15 changes: 10 additions & 5 deletions ark/attest/assert/chainableAssertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ export class ChainableAssertions implements AssertionRecord {
}

private get actual() {
return this.ctx.actual instanceof TypeAssertionMapping ?
this.ctx.actual.fn(this.ctx.typeAssertionEntries![0][1], this.ctx)!
.actual
: this.ctx.actual
if (this.ctx.actual instanceof TypeAssertionMapping) {
return this.ctx.actual.fn(
this.ctx.typeRelationshipAssertionEntries![0][1],
this.ctx
)!.actual
}
return this.ctx.actual
}

private get serializedActual() {
Expand All @@ -65,7 +68,9 @@ export class ChainableAssertions implements AssertionRecord {
ctx: this.ctx,
message:
messageOnError ??
`${this.serializedActual} failed to satisfy predicate${predicate.name ? ` ${predicate.name}` : ""}`
`${this.serializedActual} failed to satisfy predicate${
predicate.name ? ` ${predicate.name}` : ""
}`
})
}
return this.actual as never
Expand Down
14 changes: 1 addition & 13 deletions ark/attest/bench/baseline.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { ensureDir } from "@arktype/fs"
import { snapshot } from "@arktype/util"
import { rmSync } from "node:fs"
import process from "node:process"
import { queueSnapshotUpdate } from "../cache/snapshots.js"
import { getConfig } from "../config.js"
import type { BenchAssertionContext, BenchContext } from "./bench.js"
import {
stringifyMeasure,
Expand All @@ -12,8 +9,6 @@ import {
type MeasureComparison
} from "./measure.js"

let isFirstQueuedUpdate = true

export const queueBaselineUpdateIfNeeded = (
updated: Measure | MarkMeasure,
baseline: Measure | MarkMeasure | undefined,
Expand All @@ -28,13 +23,6 @@ export const queueBaselineUpdateIfNeeded = (
`Unable to update baseline for ${ctx.qualifiedName} ('lastSnapCallPosition' was unset).`
)
}
if (isFirstQueuedUpdate) {
// remove any leftover cached snaps before the first is written
const { benchSnapCacheDir } = getConfig()
rmSync(benchSnapCacheDir, { recursive: true, force: true })
ensureDir(benchSnapCacheDir)
isFirstQueuedUpdate = false
}
queueSnapshotUpdate({
position: ctx.lastSnapCallPosition,
serializedValue,
Expand Down Expand Up @@ -81,4 +69,4 @@ const handleNegativeDelta = (formattedDelta: string, ctx: BenchContext) => {
1
)}! Consider setting a new baseline.`
)
}
}
Loading

0 comments on commit 2c99448

Please sign in to comment.