diff --git a/ark/fast-check/__tests__/arktypeFastCheck.test.ts b/ark/fast-check/__tests__/arktypeFastCheck.test.ts index 4d70f9c79..f3799e4c7 100644 --- a/ark/fast-check/__tests__/arktypeFastCheck.test.ts +++ b/ark/fast-check/__tests__/arktypeFastCheck.test.ts @@ -2,16 +2,15 @@ import { attest } from "@ark/attest" import { arkToArbitrary } from "@ark/fast-check/internal/arktypeFastCheck.ts" import { ArkErrors } from "@ark/schema" import { scope, type } from "arktype" -import { cyclic10 } from "arktype/internal/__tests__/generated/cyclic.ts" import { assert, property, type Arbitrary } from "fast-check" import { describe, it } from "mocha" const assertProperty = (arbitrary: Arbitrary, schema: type.Any) => assert( property(arbitrary, value => { - const result = schema(value) console.log(value) - return !(result instanceof ArkErrors) + schema.assert(value) + return true }) ) @@ -53,6 +52,11 @@ describe("Arbitrary Generation", () => { const arbitrary = arkToArbitrary(t) assertProperty(arbitrary, t) }) + it("large divisor", () => { + const t = type("number%7654321001>1") + const arbitrary = arkToArbitrary(t) + assertProperty(arbitrary, t) + }) it("divisible within range", () => { const t = type("15 { const arbitrary = arkToArbitrary(t) assertProperty(arbitrary, t) }) - it("multiple aliases", () => { - const t = scope(cyclic10).type("user&user2") - const arbitrary = arkToArbitrary(t) - assertProperty(arbitrary, t) - }) it("cyclic throws", () => { const $ = scope({ arf2: { diff --git a/ark/fast-check/arbitraryBuilders.ts b/ark/fast-check/arbitraryBuilders.ts index b1915bc5b..314eb2e5a 100644 --- a/ark/fast-check/arbitraryBuilders.ts +++ b/ark/fast-check/arbitraryBuilders.ts @@ -86,12 +86,10 @@ export const arbitraryBuilders: ArbitraryBuilders = { const handleAlias = (fastCheckContext: FastCheckContext) => { const tieStack = fastCheckContext.fastCheckTies - if (tieStack.length) { - const tie = tieStack.at(-1)! - const id = (fastCheckContext.currentNodeContext as StructureNodeContext).id - return tie(id) - } - throwInternalError("Tie has not been initialized") + if (!(tieStack.length > 0)) throwInternalError("Tie has not been initialized") + const tie = tieStack[tieStack.length - 1] + const id = (fastCheckContext.currentNodeContext as StructureNodeContext).id + return tie(id) } export const getKey = (nodeContext: NodeContext): keyof ArbitraryBuilders => { diff --git a/ark/fast-check/terminalArbitraries.ts b/ark/fast-check/terminalArbitraries.ts index d5691a0ca..ab2ddc8f9 100644 --- a/ark/fast-check/terminalArbitraries.ts +++ b/ark/fast-check/terminalArbitraries.ts @@ -1,4 +1,4 @@ -import { hasKey } from "@ark/util" +import { hasKey, throwInternalError } from "@ark/util" import { date, double, @@ -66,42 +66,53 @@ export const generateNumberArbitrary = ( const hasMax = condensedNodeContext.max !== undefined const hasMin = condensedNodeContext.min !== undefined - if ("divisor" in condensedNodeContext) { - const constraints = getNumericBoundConstraints( - fastCheckContext, - ["min", "max"], - { - min: (value: number) => Math.ceil(value), - max: (value: number) => Math.floor(value) - } - ) - if (hasMin && hasMax && constraints.min > constraints.max) { - throw new Error( - `No integer value satisfies >${constraints.min} & <${constraints.max}` - ) - } - - const firstDivisibleInRange = - Math.ceil( - (constraints.min ?? Number.MIN_SAFE_INTEGER) / - condensedNodeContext.divisor - ) * condensedNodeContext.divisor - - if (firstDivisibleInRange > (constraints.max ?? Number.MAX_SAFE_INTEGER)) { - throw new Error( - `No values within range ${constraints.min} - ${constraints.max} are divisible by ${condensedNodeContext.divisor}.` - ) + if (condensedNodeContext.divisor === undefined) { + const constraints = getNumericBoundConstraints(fastCheckContext, [ + "min", + "max" + ]) + return double(constraints) + } + const divisor = condensedNodeContext.divisor + const constraints = getNumericBoundConstraints( + fastCheckContext, + ["min", "max"], + { + min: (value: number) => Math.ceil(value), + max: (value: number) => Math.floor(value) } + ) + if (hasMin && hasMax && constraints.min > constraints.max) { + throw new Error( + `No integer value satisfies >${constraints.min} & <${constraints.max}` + ) + } + const min = constraints.min ?? Number.MIN_SAFE_INTEGER + const max = constraints.max ?? Number.MAX_SAFE_INTEGER + const firstDivisibleInRange = Math.ceil(min / divisor) * divisor - return integer(constraints).filter( - num => num % condensedNodeContext.divisor! === 0 + if (firstDivisibleInRange > max || firstDivisibleInRange < min) { + throw new Error( + `No values within range ${constraints.min} - ${constraints.max} are divisible by ${divisor}.` ) } - const constraints = getNumericBoundConstraints(fastCheckContext, [ - "min", - "max" - ]) - return double(constraints) + constraints.min = firstDivisibleInRange + //fast-check defaults max to 0x7fffffff which prevents larger divisible numbers from being produced + constraints.max = max + const integerArbitrary = integer(constraints) + const integersDivisibleByDivisor = integerArbitrary.map(value => { + const remainder = value % divisor + if (remainder === 0) return value + + const lowerPossibleValue = value - remainder + if ( + lowerPossibleValue >= firstDivisibleInRange && + lowerPossibleValue % divisor === 0 + ) + return lowerPossibleValue + return value + remainder + }) + return integersDivisibleByDivisor } const getNumericBoundConstraints: ConstraintContext = (