diff --git a/.yarn/versions/4c50fb71.yml b/.yarn/versions/4c50fb71.yml new file mode 100644 index 00000000000..a9650bcd747 --- /dev/null +++ b/.yarn/versions/4c50fb71.yml @@ -0,0 +1,8 @@ +releases: + fast-check: minor + +declined: + - "@fast-check/ava" + - "@fast-check/jest" + - "@fast-check/vitest" + - "@fast-check/worker" diff --git a/packages/fast-check/src/arbitrary/_internals/mappers/VersionsApplierForUuid.ts b/packages/fast-check/src/arbitrary/_internals/mappers/VersionsApplierForUuid.ts new file mode 100644 index 00000000000..528280ec3b6 --- /dev/null +++ b/packages/fast-check/src/arbitrary/_internals/mappers/VersionsApplierForUuid.ts @@ -0,0 +1,33 @@ +import { Error, safeSubstring } from '../../../utils/globals'; + +/** @internal */ +const quickNumberToHexaString = '0123456789abcdef'; + +/** @internal */ +export function buildVersionsAppliersForUuid(versions: number[]): { + versionsApplierMapper: (value: string) => string; + versionsApplierUnmapper: (value: unknown) => string; +} { + const mapping: Record = {}; + const reversedMapping: Record = {}; + for (let index = 0; index !== versions.length; ++index) { + const from = quickNumberToHexaString[index]; + const to = quickNumberToHexaString[versions[index]]; + mapping[from] = to; + reversedMapping[to] = from; + } + function versionsApplierMapper(value: string): string { + return mapping[value[0]] + safeSubstring(value, 1); + } + function versionsApplierUnmapper(value: unknown): string { + if (typeof value !== 'string') { + throw new Error('Cannot produce non-string values'); + } + const rev = reversedMapping[value[0]]; + if (rev === undefined) { + throw new Error('Cannot produce strings not starting by the version in hexa code'); + } + return rev + safeSubstring(value, 1); + } + return { versionsApplierMapper, versionsApplierUnmapper }; +} diff --git a/packages/fast-check/src/arbitrary/uuid.ts b/packages/fast-check/src/arbitrary/uuid.ts index c2f1db78aeb..6b3ee08a992 100644 --- a/packages/fast-check/src/arbitrary/uuid.ts +++ b/packages/fast-check/src/arbitrary/uuid.ts @@ -2,6 +2,46 @@ import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary'; import { tuple } from './tuple'; import { buildPaddedNumberArbitrary } from './_internals/builders/PaddedNumberArbitraryBuilder'; import { paddedEightsToUuidMapper, paddedEightsToUuidUnmapper } from './_internals/mappers/PaddedEightsToUuid'; +import { Error } from '../utils/globals'; +import { buildVersionsAppliersForUuid } from './_internals/mappers/VersionsApplierForUuid'; + +/** + * Constraints to be applied on {@link uuid} + * @remarks Since 3.21.0 + * @public + */ +export interface UuidConstraints { + /** + * Define accepted versions in the [1-15] according to {@link https://datatracker.ietf.org/doc/html/rfc9562#name-version-field | RFC 9562} + * @defaultValue [1,2,3,4,5] + * @remarks Since 3.21.0 + */ + version?: + | (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15) + | (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15)[]; +} + +/** @internal */ +function assertValidVersions(versions: number[]) { + const found: { [key: number]: true | undefined } = {}; + for (const version of versions) { + // Check no duplicates + if (found[version]) { + throw new Error(`Version ${version} has been requested at least twice for uuid`); + } + found[version] = true; + // Check version + if (version < 1 || version > 15) { + throw new Error(`Version must be a value in [1-15] for uuid, but received ${version}`); + } + if (~~version !== version) { + throw new Error(`Version must be an integer value for uuid, but received ${version}`); + } + } + if (versions.length === 0) { + throw new Error(`Must provide at least one version for uuid`); + } +} /** * For UUID from v1 to v5 @@ -13,13 +53,24 @@ import { paddedEightsToUuidMapper, paddedEightsToUuidUnmapper } from './_interna * @remarks Since 1.17.0 * @public */ -export function uuid(): Arbitrary { +export function uuid(constraints: UuidConstraints = {}): Arbitrary { // According to RFC 4122: Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively // ie.: ????????-????-X???-Y???-???????????? // with X in 1, 2, 3, 4, 5 // with Y in 8, 9, A, B const padded = buildPaddedNumberArbitrary(0, 0xffffffff); - const secondPadded = buildPaddedNumberArbitrary(0x10000000, 0x5fffffff); + const version = + constraints.version !== undefined + ? typeof constraints.version === 'number' + ? [constraints.version] + : constraints.version + : [1, 2, 3, 4, 5]; + assertValidVersions(version); + const { versionsApplierMapper, versionsApplierUnmapper } = buildVersionsAppliersForUuid(version); + const secondPadded = buildPaddedNumberArbitrary(0, 0x10000000 * version.length - 1).map( + versionsApplierMapper, + versionsApplierUnmapper, + ); const thirdPadded = buildPaddedNumberArbitrary(0x80000000, 0xbfffffff); return tuple(padded, secondPadded, thirdPadded, padded).map(paddedEightsToUuidMapper, paddedEightsToUuidUnmapper); } diff --git a/packages/fast-check/src/fast-check-default.ts b/packages/fast-check/src/fast-check-default.ts index 7dbdf4e4984..a9597ce52a8 100644 --- a/packages/fast-check/src/fast-check-default.ts +++ b/packages/fast-check/src/fast-check-default.ts @@ -126,6 +126,7 @@ import { shuffledSubarray } from './arbitrary/shuffledSubarray'; import { tuple } from './arbitrary/tuple'; import { ulid } from './arbitrary/ulid'; import { uuid } from './arbitrary/uuid'; +import type { UuidConstraints } from './arbitrary/uuid'; import { uuidV } from './arbitrary/uuidV'; import type { WebAuthorityConstraints } from './arbitrary/webAuthority'; import { webAuthority } from './arbitrary/webAuthority'; @@ -295,6 +296,7 @@ export type { UniqueArrayConstraintsRecommended, UniqueArrayConstraintsCustomCompare, UniqueArrayConstraintsCustomCompareSelect, + UuidConstraints, SparseArrayConstraints, StringMatchingConstraints, StringSharedConstraints, diff --git a/packages/fast-check/test/unit/arbitrary/_internals/mappers/VersionsApplierForUuid.spec.ts b/packages/fast-check/test/unit/arbitrary/_internals/mappers/VersionsApplierForUuid.spec.ts new file mode 100644 index 00000000000..43416a1b762 --- /dev/null +++ b/packages/fast-check/test/unit/arbitrary/_internals/mappers/VersionsApplierForUuid.spec.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest'; +import { buildVersionsAppliersForUuid } from '../../../../../src/arbitrary/_internals/mappers/VersionsApplierForUuid'; +import fc from 'fast-check'; + +describe('versionsApplierMapper', () => { + it('should transform the first hexa element to its proper version', () => { + // Arrange + const source = '301'; // 3 means 4th entry + const versions = [10, 3, 7, 9, 15]; + + // Act + const { versionsApplierMapper } = buildVersionsAppliersForUuid(versions); + const out = versionsApplierMapper(source); + + // Assert + expect(out).toBe('901'); + }); + + it('should correctly transform to versions strictly above 9', () => { + // Arrange + const source = '481'; // 4 means 5th entry + const versions = [10, 3, 7, 9, 15]; + + // Act + const { versionsApplierMapper } = buildVersionsAppliersForUuid(versions); + const out = versionsApplierMapper(source); + + // Assert + expect(out).toBe('f81'); + }); +}); + +describe('versionsApplierUnmapper', () => { + it('should correctly unmap from a known version', () => { + // Arrange + const source = '901'; // 9 is at index 3, so it should unmap to 3 + const versions = [10, 3, 7, 9, 15]; + + // Act + const { versionsApplierUnmapper } = buildVersionsAppliersForUuid(versions); + const out = versionsApplierUnmapper(source); + + // Assert + expect(out).toBe('301'); + }); + + it('should reject unknown versions', () => { + // Arrange + const source = '801'; // 8 is unknown + const versions = [10, 3, 7, 9, 15]; + + // Act + const { versionsApplierUnmapper } = buildVersionsAppliersForUuid(versions); + + // Assert + expect(() => versionsApplierUnmapper(source)).toThrowError(); + }); + + it('should reject versions with invalid case', () => { + // Arrange + const source = 'F01'; // F is unknown, but f would have be known + const versions = [10, 3, 7, 9, 15]; + + // Act + const { versionsApplierUnmapper } = buildVersionsAppliersForUuid(versions); + + // Assert + expect(() => versionsApplierUnmapper(source)).toThrowError(); + }); + + it('should be able to unmap any mapped value', () => + fc.assert( + fc.property( + fc.uniqueArray(fc.nat({ max: 15 }), { minLength: 1 }), + fc.nat(), + fc.hexaString(), + (versions, diceIndex, tail) => { + // Arrange + const index = diceIndex % versions.length; + const source = index.toString(16) + tail; + const { versionsApplierMapper, versionsApplierUnmapper } = buildVersionsAppliersForUuid(versions); + const mapped = versionsApplierMapper(source); + + // Act + const out = versionsApplierUnmapper(mapped); + + // Assert + expect(out).toEqual(source); + }, + ), + )); +}); diff --git a/packages/fast-check/test/unit/arbitrary/uuid.spec.ts b/packages/fast-check/test/unit/arbitrary/uuid.spec.ts index 7a1fe53baf1..65494b48b57 100644 --- a/packages/fast-check/test/unit/arbitrary/uuid.spec.ts +++ b/packages/fast-check/test/unit/arbitrary/uuid.spec.ts @@ -1,6 +1,8 @@ import { describe, it, expect, vi } from 'vitest'; +import type { UuidConstraints } from '../../../src/arbitrary/uuid'; import { uuid } from '../../../src/arbitrary/uuid'; import { fakeArbitraryStaticValue } from './__test-helpers__/ArbitraryHelpers'; +import fc from 'fast-check'; import * as _IntegerMock from '../../../src/arbitrary/integer'; import type { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary'; @@ -53,25 +55,46 @@ describe('uuid', () => { }); describe('uuid (integration)', () => { - const isCorrect = (u: string) => { - expect(u).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[12345][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + type Extra = UuidConstraints; + const extraParameters: fc.Arbitrary = fc.record( + { + version: fc.oneof( + fc.constantFrom(...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const)), + fc.uniqueArray(fc.constantFrom(...([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const)), { + minLength: 1, + }), + ), + }, + { requiredKeys: [] }, + ); + + const isCorrect = (u: string, extra: Extra) => { + expect(u).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-9a-f][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + const versions = + extra.version !== undefined + ? typeof extra.version === 'number' + ? [extra.version] + : extra.version + : [1, 2, 3, 4, 5]; + const versionInValue = u[14]; + expect(versions.map((v) => v.toString(16))).toContain(versionInValue); }; - const uuidVBuilder = () => uuid(); + const uuidBuilder = (extra: Extra) => uuid(extra); it('should produce the same values given the same seed', () => { - assertProduceSameValueGivenSameSeed(uuidVBuilder); + assertProduceSameValueGivenSameSeed(uuidBuilder, { extraParameters }); }); it('should only produce correct values', () => { - assertProduceCorrectValues(uuidVBuilder, isCorrect); + assertProduceCorrectValues(uuidBuilder, isCorrect, { extraParameters }); }); it('should produce values seen as shrinkable without any context', () => { - assertProduceValuesShrinkableWithoutContext(uuidVBuilder); + assertProduceValuesShrinkableWithoutContext(uuidBuilder, { extraParameters }); }); it('should be able to shrink to the same values without initial context', () => { - assertShrinkProducesSameValueWithoutInitialContext(uuidVBuilder); + assertShrinkProducesSameValueWithoutInitialContext(uuidBuilder, { extraParameters }); }); }); diff --git a/website/docs/core-blocks/arbitraries/fake-data/identifier.md b/website/docs/core-blocks/arbitraries/fake-data/identifier.md index 73e984da382..7d6d5cb3de2 100644 --- a/website/docs/core-blocks/arbitraries/fake-data/identifier.md +++ b/website/docs/core-blocks/arbitraries/fake-data/identifier.md @@ -32,11 +32,16 @@ Available since 3.11.0. ### uuid -UUID values including versions 1 to 5. +UUID values including versions 1 to 5 and going up to 15 when asked to. **Signatures:** - `fc.uuid()` +- `fc.uuid({version?})` + +**with:** + +- `version` — default: `[1,2,3,4,5]` — _version or versions of the uuid to produce: 1, 2, 3, 4, 5... or 15_ **Usages:** @@ -49,6 +54,24 @@ fc.uuid(); // • "17983d5d-001b-1000-98d3-6afba08e1e61" // • "7da15579-001d-1000-a6b3-4d71cf6e5de5" // • … + +fc.uuid({ version: 4 }); +// Examples of generated values: +// • "00000009-2401-464f-bd6c-b85100000018" +// • "ffffffea-ffe7-4fff-af56-be4ec6ccfa3c" +// • "00000013-6705-4bdd-bfe3-0669d6ee4e9a" +// • "ed7479b3-cef8-4562-bc9c-0b0d8b2be3ae" +// • "58dbd17a-7152-4770-8d89-9485fffffff6" +// • … + +fc.uuid({ version: [4, 7] }); +// Examples of generated values: +// • "ffffffe8-4e61-40c1-8000-001d7f621812" +// • "0000001f-b6dc-7d7d-b40c-08568ae90153" +// • "0000000b-0002-4000-9003-de96d8957794" +// • "8b8e8b89-251e-78e7-8000-000000000000" +// • "ffffffe5-000d-4000-bfff-fff496517cc4" +// • … ``` Resources: [API reference](https://fast-check.dev/api-reference/functions/uuid.html).