diff --git a/README.md b/README.md index ce3f66d..95fe855 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ it.layer(ApiLive)("api", (it) => { }) ``` -Property tests accept Effect `Schema` and FastCheck `Arbitrary` values. They shrink callbacks that return `false`, throw, or fail with a non-interruption cause. +Property tests accept Effect `Schema` values and `Arbitrary` values from `effect/unstable/arbitrary`. They shrink callbacks that return `false`, throw, or fail with a non-interruption cause. ```ts import { Effect, Schema } from "effect" @@ -66,7 +66,9 @@ it.effect.prop( ) ``` -All three helpers accept both tuple and record inputs, mixing schemas and FastCheck arbitraries. Schemas are converted with `Schema.toArbitrary(schema)(FastCheck)`; FastCheck arbitraries are used directly. For example, a synchronous property can use `[Schema.Literal("schema"), FastCheck.integer()]` or `{ label: Schema.Literal("schema"), count: FastCheck.integer() }`. A schema must support arbitrary generation; this does not make every possible schema generatable. +All three helpers accept tuple and record inputs, mixing schemas and arbitraries. Schemas are converted with `Arbitrary.schema(schema)`; `Arbitrary` values are used directly. For example, a synchronous property can use `[Schema.Literal("schema"), Arbitrary.schema(Schema.Int)]`. A schema must support arbitrary generation. Pass check options as `{ arbitrary: { runs: 200, seed: "repro" } }` (see `Arbitrary.CheckOptions`). + +Requires `effect` `4.0.0-rc.113` or later, which removed `effect/testing/FastCheck`. To migrate from 0.1.x, replace `FastCheck.*` inputs with schemas or `Arbitrary` values, and `{ fastCheck: { numRuns } }` with `{ arbitrary: { runs } }`. Rstest modifiers remain available. Use `it.effect.concurrent`, `it.effect.sequential`, `it.effect.skip`, `it.effect.only`, `it.effect.fails`, `it.effect.skipIf`, and `it.effect.runIf` with Effect tests. diff --git a/package.json b/package.json index 7285ecd..62351e7 100644 --- a/package.json +++ b/package.json @@ -50,13 +50,13 @@ }, "peerDependencies": { "@rstest/core": ">=0.11.12 <0.12.0", - "effect": ">=4.0.0-rc.112 <5.0.0" + "effect": ">=4.0.0-rc.113 <5.0.0" }, "devDependencies": { "@rslib/core": "1.0.0", "@rstest/core": "0.11.12", "@types/node": "^26.4.1", - "effect": "4.0.0-rc.112", + "effect": "4.0.0-rc.117", "tstyche": "^7.2.4", "typescript": "^5.9.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1f20ea..5e43982 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^26.4.1 version: 26.4.1 effect: - specifier: 4.0.0-rc.112 - version: 4.0.0-rc.112 + specifier: 4.0.0-rc.117 + version: 4.0.0-rc.117 tstyche: specifier: ^7.2.4 version: 7.2.4(typescript@5.9.3) @@ -100,36 +100,6 @@ packages: '@emnapi/wasi-threads@1.2.3': resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} - cpu: [x64] - os: [win32] - '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: @@ -283,30 +253,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - effect@4.0.0-rc.112: - resolution: {integrity: sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A==} - - fast-check@4.9.0: - resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} - engines: {node: '>=12.17.0'} - - msgpackr-extract@3.0.4: - resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} - hasBin: true - - msgpackr@2.1.0: - resolution: {integrity: sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ==} - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - pure-rand@8.4.2: - resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + effect@4.0.0-rc.117: + resolution: {integrity: sha512-UUyi9QiOW4nySFIBM3KnYbNMd9EZliOdKfBcyLD0mPMPheO2fkuXS5y0opjWc9oqzlJxPQICEGm8gGw3kh9j/w==} rsbuild-plugin-dts@1.0.0: resolution: {integrity: sha512-FVoTsiTfSc0LPeSjrVhpvFOBRig0YXJq3Hp+bVxzh9/bzFvODzLRWOEux8q1Phe1oVCIIVSc2+Nb7kDcxIhosw==} @@ -399,24 +347,6 @@ snapshots: tslib: 2.8.1 optional: true - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.11.3 @@ -540,40 +470,7 @@ snapshots: assertion-error@2.0.1: {} - detect-libc@2.1.2: - optional: true - - effect@4.0.0-rc.112: - dependencies: - fast-check: 4.9.0 - msgpackr: 2.1.0 - - fast-check@4.9.0: - dependencies: - pure-rand: 8.4.2 - - msgpackr-extract@3.0.4: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 - optional: true - - msgpackr@2.1.0: - optionalDependencies: - msgpackr-extract: 3.0.4 - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - pure-rand@8.4.2: {} + effect@4.0.0-rc.117: {} rsbuild-plugin-dts@1.0.0(@rsbuild/core@2.2.3)(typescript@5.9.3): dependencies: diff --git a/src/index.ts b/src/index.ts index 96c8e00..ac7208f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import type * as Effect from "effect/Effect" import type * as Layer from "effect/Layer" import type * as Schema from "effect/Schema" import type * as Scope from "effect/Scope" -import type { FastCheck as FC } from "effect/testing" +import type * as Arbitrary from "effect/unstable/arbitrary/Arbitrary" import * as internal from "./internal/internal.ts" /** @@ -46,11 +46,11 @@ export namespace Rstest { * @since 0.1.0 */ export type Arbitraries = - | ReadonlyArray | FC.Arbitrary> - | { [K in string]: Schema.Schema | FC.Arbitrary } + | ReadonlyArray | Arbitrary.Arbitrary> + | { [K in string]: Schema.Schema | Arbitrary.Arbitrary } type ArbitraryValue = A extends Schema.Schema ? T - : A extends FC.Arbitrary ? T + : A extends Arbitrary.Arbitrary ? T : never /** @@ -102,7 +102,7 @@ export namespace Rstest { timeout?: | number | R.TestOptions & { - fastCheck?: FC.Parameters + arbitrary?: Arbitrary.CheckOptions fails?: boolean } ) => void @@ -156,7 +156,7 @@ export namespace Rstest { timeout?: | number | R.TestOptions & { - fastCheck?: FC.Parameters + arbitrary?: Arbitrary.CheckOptions fails?: boolean } ) => void diff --git a/src/internal/internal.ts b/src/internal/internal.ts index d22b7c6..5f08712 100644 --- a/src/internal/internal.ts +++ b/src/internal/internal.ts @@ -13,9 +13,10 @@ import { flow, pipe } from "effect/Function" import * as Layer from "effect/Layer" import * as Predicate from "effect/Predicate" import * as Schedule from "effect/Schedule" -import * as Schema from "effect/Schema" +import type * as Schema from "effect/Schema" import * as Scope from "effect/Scope" -import { FastCheck as FC, TestClock, TestConsole } from "effect/testing" +import { TestClock, TestConsole } from "effect/testing" +import * as Arbitrary from "effect/unstable/arbitrary/Arbitrary" import type * as Rstest from "../index.ts" const runPromise: ( @@ -61,7 +62,7 @@ const hookTimeout = (timeout?: Duration.Input) => timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(timeout)) type PropertyOptions = R.TestOptions & { - readonly fastCheck?: FC.Parameters | undefined + readonly arbitrary?: Arbitrary.CheckOptions | undefined readonly fails?: boolean | undefined } @@ -75,11 +76,11 @@ const testOptions = (timeout?: number | R.TestOptions | PropertyOptions): R.Test if (timeout === undefined) { return {} } - const { fails: _, fastCheck: __, ...options } = timeout as PropertyOptions + const { arbitrary: __, fails: _, ...options } = timeout as PropertyOptions return options } -type ArbitraryInput = Schema.Schema | FC.Arbitrary +type ArbitraryInput = Schema.Schema | Arbitrary.Arbitrary type Arbitraries = ReadonlyArray | { [K in string]: ArbitraryInput } @@ -87,18 +88,18 @@ const propertyTestOptions = ( timeout: PropertyTimeout | undefined ): Exclude | undefined => typeof timeout === "number" ? undefined : timeout -const checkOptions = (timeout: PropertyTimeout | undefined): FC.Parameters | undefined => - propertyTestOptions(timeout)?.fastCheck +const checkOptions = (timeout: PropertyTimeout | undefined): Arbitrary.CheckOptions | undefined => + propertyTestOptions(timeout)?.arbitrary -const compileArbitraryInput = (input: ArbitraryInput): FC.Arbitrary => - Schema.isSchema(input) ? Schema.toArbitrary(input)(FC) : input as FC.Arbitrary +const compileArbitraryInput = (input: ArbitraryInput): Arbitrary.Arbitrary => + Arbitrary.isArbitrary(input) ? input : Arbitrary.schema(input) -const makeArbitrary = (arbitraries: Arbitraries): FC.Arbitrary => - Array.isArray(arbitraries) - ? FC.tuple(...arbitraries.map(compileArbitraryInput)) - : FC.record(Object.fromEntries( - Object.entries(arbitraries).map(([key, input]) => [key, compileArbitraryInput(input)]) - )) +const makeArbitrary = (arbitraries: Arbitraries): Arbitrary.Arbitrary => + Arbitrary.all( + Array.isArray(arbitraries) + ? arbitraries.map(compileArbitraryInput) + : Object.fromEntries(Object.entries(arbitraries).map(([key, input]) => [key, compileArbitraryInput(input)])) + ) const normalizeProperty = ( property: (value: A) => boolean | Effect.Effect, @@ -130,13 +131,18 @@ const normalizeSyncProperty = ( const runCheck = ( ctx: R.TestContext, - arbitrary: FC.Arbitrary, + arbitrary: Arbitrary.Arbitrary, property: (value: A) => boolean | Effect.Effect, - options: FC.Parameters | undefined + options: Arbitrary.CheckOptions | undefined ): Promise => - FC.assert( - FC.asyncProperty(arbitrary, (value) => runTest(ctx)(normalizeProperty(property, value))), - options + runTest(ctx)( + Effect.flatMapEager( + Arbitrary.checkEffect(arbitrary, (value) => normalizeProperty(property, value), options), + (result) => { + const failure = Arbitrary.formatCheckFailure(result) + return failure === undefined ? Effect.void : Effect.die(new Error(failure)) + } + ) ) type TestAPI = R.TestAPIs | R.TestAPIs["skip"] diff --git a/test/index.test.ts b/test/index.test.ts index f4f5697..af3e04a 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,8 @@ import { Clock, Context, Duration, Effect, Equal, Fiber, Hash, Layer, Schema } from "effect" import { addEqualityTesters, afterAll, assert, describe, expect, it, layer, makeMethods } from "effect-rstest" import * as testAssert from "effect-rstest/utils" -import { FastCheck, TestClock } from "effect/testing" +import { TestClock } from "effect/testing" +import { Arbitrary } from "effect/unstable/arbitrary" it.effect( "effect", @@ -185,7 +186,7 @@ describe("layer", () => { expect(foo).toEqual("foo") return num === num }), - { fastCheck: { numRuns: 200 } } + { arbitrary: { runs: 200 } } ) it.effect.prop( @@ -197,7 +198,7 @@ describe("layer", () => { assert.strictEqual(foo, "foo") assert.isTrue(Number.isInteger(value)) }), - { fastCheck: { numRuns: 5, seed: 1001 } } + { arbitrary: { runs: 5, seed: 1001 } } ) }) }) @@ -299,7 +300,7 @@ describe("anonymous parent layer lifetime", () => { }) const realNumber = Schema.Finite -const textArbitrary = FastCheck.constantFrom("a" as const, "b" as const) +const textArbitrary = Arbitrary.schema(Schema.Literals(["a", "b"])) it.prop( "schema with array", @@ -317,7 +318,7 @@ it.prop( "rejects Promise-returning synchronous properties", [Schema.Int], (() => Promise.resolve(true)) as unknown as () => boolean, - { fails: true, fastCheck: { numRuns: 1 } } + { fails: true, arbitrary: { runs: 1 } } ) let mixedTupleRuns = 0 @@ -335,7 +336,7 @@ it.prop( assert.isTrue(Number.isInteger(count)) assert.include(["a", "b"], text) }, - { fastCheck: { numRuns: 5, seed: 1002 } } + { arbitrary: { runs: 5, seed: 1002 } } ) it.effect.prop( @@ -347,7 +348,7 @@ it.effect.prop( assert.isTrue(Number.isInteger(count)) assert.include(["a", "b"], text) }), - { fastCheck: { numRuns: 5, seed: 1003 } } + { arbitrary: { runs: 5, seed: 1003 } } ) it.prop("symmetry", [realNumber, Schema.Int], ([a, b]) => a + b === b + a) @@ -376,7 +377,7 @@ it.effect.prop( assert.strictEqual(typeof text, "string") assert.isTrue(Number.isInteger(count)) }), - { fastCheck: { numRuns: 5, seed: 1004 } } + { arbitrary: { runs: 5, seed: 1004 } } ) it.effect.prop("symmetry", [realNumber, Schema.Int], ([a, b]) => @@ -417,6 +418,11 @@ describe("property failures", () => { assert.isTrue(timeoutPropertyReleased) }) + it.prop("falsifies properties that return false", [Input], () => false, { + fails: true, + arbitrary: { runs: 1, seed: "assertion-shrink" } + }) + it.prop( "shrinks synchronous defects", [Input], @@ -424,7 +430,7 @@ describe("property failures", () => { pureDefectValues.push(value) throw new Error("property defect") }, - { fails: true, fastCheck: { numRuns: 1, seed: 1005 } } + { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } } ) it.effect.prop( @@ -435,7 +441,7 @@ describe("property failures", () => { effectDefectValues.push(value) assert.strictEqual(value, 0) }), - { fails: true, fastCheck: { numRuns: 1, seed: 1005 } } + { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } } ) it.effect.prop( @@ -445,7 +451,7 @@ describe("property failures", () => { interruptedRuns++ return Effect.interrupt }, - { fails: true, fastCheck: { numRuns: 1, seed: 1005 } } + { fails: true, arbitrary: { runs: 1, seed: "assertion-shrink" } } ) it.effect.prop( @@ -462,6 +468,6 @@ describe("property failures", () => { timeoutPropertyReleased = true }) ), - { fails: true, timeout: 10, fastCheck: { numRuns: 1, seed: 1006 } } + { fails: true, timeout: 10, arbitrary: { runs: 1, seed: 1006 } } ) }) diff --git a/typetest/index.tst.ts b/typetest/index.tst.ts index dbcd647..112dc63 100644 --- a/typetest/index.tst.ts +++ b/typetest/index.tst.ts @@ -1,6 +1,6 @@ import { Context, Effect, Layer, Schema } from "effect" import { it, layer } from "effect-rstest" -import { FastCheck } from "effect/testing" +import { Arbitrary } from "effect/unstable/arbitrary" import { describe, expect, test } from "tstyche" class Foo extends Context.Service()("Foo") {} @@ -93,7 +93,7 @@ describe("property testing", () => { expect(count).type.toBe() return Effect.void }, - { fastCheck: { numRuns: 10, seed: 1001 } } + { arbitrary: { runs: 10, seed: 1001 } } ) }) @@ -109,17 +109,17 @@ describe("property testing", () => { test("infers Schema record values for the pure property helper", () => { it.prop( "schema record", - { text: Schema.String, count: Schema.Int }, + { text: Schema.String, count: Arbitrary.schema(Schema.Int) }, ({ text, count }) => { expect(text).type.toBe() expect(count).type.toBe() }, - { fastCheck: { numRuns: 10 } } + { arbitrary: { runs: 10 } } ) }) test("infers mixed Schema and Arbitrary values", () => { - const text = FastCheck.constantFrom("a" as const, "b" as const) + const text = Arbitrary.schema(Schema.Literals(["a", "b"])) it.effect.prop( "mixed tuple", diff --git a/typetest/prop-schema.tst.ts b/typetest/prop-schema.tst.ts index aecba17..f3ed1f3 100644 --- a/typetest/prop-schema.tst.ts +++ b/typetest/prop-schema.tst.ts @@ -1,18 +1,18 @@ import { it } from "effect-rstest" import { Effect, Schema } from "effect" -import { FastCheck } from "effect/testing" +import { Arbitrary } from "effect/unstable/arbitrary" import { describe, expect, test } from "tstyche" describe("schema properties", () => { test("plain tuple inputs infer schema and arbitrary values", () => { - it.prop("tuple", [Schema.Literal("schema"), FastCheck.integer()], ([label, count]) => { + it.prop("tuple", [Schema.Literal("schema"), Arbitrary.schema(Schema.Int)], ([label, count]) => { expect(label).type.toBe<"schema">() expect(count).type.toBe() }) }) test("plain record inputs infer schema and arbitrary values", () => { - it.prop("record", { label: Schema.Literal("schema"), count: FastCheck.integer() }, ({ label, count }) => { + it.prop("record", { label: Schema.Literal("schema"), count: Arbitrary.schema(Schema.Int) }, ({ label, count }) => { expect(label).type.toBe<"schema">() expect(count).type.toBe() })