From 9537dfddba882bd93d9a429697fd44bc72428426 Mon Sep 17 00:00:00 2001 From: DivisionByZero Date: Thu, 26 Sep 2024 17:50:05 +0200 Subject: infra: update file structure for util/internal (#3141) --- src/faker.ts | 4 +- src/index.ts | 8 +- src/internal/locale-proxy.ts | 115 +++++++++++++ src/internal/mersenne.ts | 48 ------ src/internal/types.ts | 39 +++++ src/locale-proxy.ts | 115 ------------- src/modules/date/index.ts | 2 +- src/modules/string/index.ts | 2 +- src/simple-faker.ts | 2 +- src/utils/mersenne.ts | 48 ++++++ src/utils/types.ts | 39 ----- test/internal/__snapshots__/mersenne.spec.ts.snap | 25 --- test/internal/locale-proxy.spec.ts | 197 ++++++++++++++++++++++ test/internal/mersenne-test-utils.ts | 17 -- test/internal/mersenne.spec.ts | 140 --------------- test/locale-proxy.spec.ts | 197 ---------------------- test/modules/number.spec.ts | 2 +- test/scripts/apidocs/method.example.ts | 2 +- test/scripts/apidocs/verify-jsdoc-tags.spec.ts | 2 +- test/support/seeded-runs.ts | 2 +- test/utils/__snapshots__/mersenne.spec.ts.snap | 25 +++ test/utils/mersenne-test-utils.ts | 17 ++ test/utils/mersenne.spec.ts | 140 +++++++++++++++ 23 files changed, 594 insertions(+), 594 deletions(-) create mode 100644 src/internal/locale-proxy.ts create mode 100644 src/internal/types.ts delete mode 100644 src/locale-proxy.ts create mode 100644 src/utils/mersenne.ts delete mode 100644 src/utils/types.ts delete mode 100644 test/internal/__snapshots__/mersenne.spec.ts.snap create mode 100644 test/internal/locale-proxy.spec.ts delete mode 100644 test/internal/mersenne-test-utils.ts delete mode 100644 test/internal/mersenne.spec.ts delete mode 100644 test/locale-proxy.spec.ts create mode 100644 test/utils/__snapshots__/mersenne.spec.ts.snap create mode 100644 test/utils/mersenne-test-utils.ts create mode 100644 test/utils/mersenne.spec.ts diff --git a/src/faker.ts b/src/faker.ts index 5f5e14ce..7b7f5e16 100644 --- a/src/faker.ts +++ b/src/faker.ts @@ -1,8 +1,8 @@ import type { LocaleDefinition, MetadataDefinition } from './definitions'; import { FakerError } from './errors/faker-error'; import { deprecated } from './internal/deprecated'; -import type { LocaleProxy } from './locale-proxy'; -import { createLocaleProxy } from './locale-proxy'; +import type { LocaleProxy } from './internal/locale-proxy'; +import { createLocaleProxy } from './internal/locale-proxy'; import { AirlineModule } from './modules/airline'; import { AnimalModule } from './modules/animal'; import { ColorModule } from './modules/color'; diff --git a/src/index.ts b/src/index.ts index 0b590340..f94b3aa0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,10 +30,6 @@ export type { export { FakerError } from './errors/faker-error'; export { Faker } from './faker'; export type { FakerOptions } from './faker'; -export { - generateMersenne32Randomizer, - generateMersenne53Randomizer, -} from './internal/mersenne'; export * from './locale'; export { fakerEN as faker } from './locale'; export * from './locales'; @@ -85,3 +81,7 @@ export type { WordModule } from './modules/word'; export type { Randomizer } from './randomizer'; export { SimpleFaker, simpleFaker } from './simple-faker'; export { mergeLocales } from './utils/merge-locales'; +export { + generateMersenne32Randomizer, + generateMersenne53Randomizer, +} from './utils/mersenne'; diff --git a/src/internal/locale-proxy.ts b/src/internal/locale-proxy.ts new file mode 100644 index 00000000..dbb9f277 --- /dev/null +++ b/src/internal/locale-proxy.ts @@ -0,0 +1,115 @@ +import type { LocaleDefinition } from '../definitions'; +import { FakerError } from '../errors/faker-error'; + +/** + * A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined. + */ +export type LocaleProxy = Readonly<{ + [key in keyof LocaleDefinition]-?: LocaleProxyCategory; +}>; + +type LocaleProxyCategory = Readonly<{ + [key in keyof T]-?: LocaleProxyEntry; +}>; + +type LocaleProxyEntry = unknown extends T ? T : Readonly>; + +const throwReadOnlyError: () => never = () => { + throw new FakerError('You cannot edit the locale data on the faker instance'); +}; + +/** + * Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed. + * + * @param locale The locale definition to create the proxy for. + */ +export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy { + const proxies = {} as LocaleDefinition; + return new Proxy(locale, { + has(): true { + // Categories are always present (proxied), that's why we return true. + return true; + }, + + get( + target: LocaleDefinition, + categoryName: keyof LocaleDefinition + ): LocaleDefinition[keyof LocaleDefinition] { + if (typeof categoryName === 'symbol' || categoryName === 'nodeType') { + return target[categoryName]; + } + + if (categoryName in proxies) { + return proxies[categoryName]; + } + + return (proxies[categoryName] = createCategoryProxy( + categoryName, + target[categoryName] + )); + }, + + set: throwReadOnlyError, + deleteProperty: throwReadOnlyError, + }) as LocaleProxy; +} + +/** + * Checks that the value is not null or undefined and throws an error if it is. + * + * @param value The value to check. + * @param path The path to the locale data. + */ +export function assertLocaleData( + value: T, + ...path: string[] +): asserts value is NonNullable { + if (value === null) { + throw new FakerError( + `The locale data for '${path.join('.')}' aren't applicable to this locale. + If you think this is a bug, please report it at: https://github.com/faker-js/faker` + ); + } else if (value === undefined) { + throw new FakerError( + `The locale data for '${path.join('.')}' are missing in this locale. + Please contribute the missing data to the project or use a locale/Faker instance that has these data. + For more information see https://fakerjs.dev/guide/localization.html` + ); + } +} + +/** + * Creates a proxy for a category that throws an error when accessing an undefined property. + * + * @param categoryName The name of the category. + * @param categoryData The module to create the proxy for. + */ +function createCategoryProxy< + TCategoryData extends Record, +>( + categoryName: string, + categoryData: TCategoryData = {} as TCategoryData +): Required { + return new Proxy(categoryData, { + has(target: TCategoryData, entryName: keyof TCategoryData): boolean { + const value = target[entryName]; + return value != null; + }, + + get( + target: TCategoryData, + entryName: keyof TCategoryData + ): TCategoryData[keyof TCategoryData] { + const value = target[entryName]; + if (typeof entryName === 'symbol' || entryName === 'nodeType') { + return value; + } + + assertLocaleData(value, categoryName, entryName.toString()); + return value; + }, + + set: throwReadOnlyError, + deleteProperty: throwReadOnlyError, + }) as Required; +} diff --git a/src/internal/mersenne.ts b/src/internal/mersenne.ts index d01b5ee9..351faa4e 100644 --- a/src/internal/mersenne.ts +++ b/src/internal/mersenne.ts @@ -1,5 +1,3 @@ -import type { Randomizer } from '../randomizer'; - /** * Copyright (c) 2022-2023 Faker * @@ -325,49 +323,3 @@ export class MersenneTwister19937 { } // These real versions are due to Isaku Wada, 2002/01/09 } - -/** - * Generates a MersenneTwister19937 randomizer with 32 bits of precision. - * This is the default randomizer used by faker prior to v9.0. - */ -export function generateMersenne32Randomizer(): Randomizer { - const twister = new MersenneTwister19937(); - - twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); - - return { - next(): number { - return twister.genrandReal2(); - }, - seed(seed: number | number[]): void { - if (typeof seed === 'number') { - twister.initGenrand(seed); - } else if (Array.isArray(seed)) { - twister.initByArray(seed, seed.length); - } - }, - }; -} - -/** - * Generates a MersenneTwister19937 randomizer with 53 bits of precision. - * This is the default randomizer used by faker starting with v9.0. - */ -export function generateMersenne53Randomizer(): Randomizer { - const twister = new MersenneTwister19937(); - - twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); - - return { - next(): number { - return twister.genrandRes53(); - }, - seed(seed: number | number[]): void { - if (typeof seed === 'number') { - twister.initGenrand(seed); - } else if (Array.isArray(seed)) { - twister.initByArray(seed, seed.length); - } - }, - }; -} diff --git a/src/internal/types.ts b/src/internal/types.ts new file mode 100644 index 00000000..affdda7e --- /dev/null +++ b/src/internal/types.ts @@ -0,0 +1,39 @@ +/** + * Type that provides auto-suggestions but also any string. + * + * @see https://github.com/microsoft/TypeScript/issues/29729#issuecomment-471566609 + */ +export type LiteralUnion = + | TSuggested + | (TBase & { zz_IGNORE_ME?: never }); + +/** + * A function that returns a value. + * + * `Function` cannot be used instead because it doesn't accept class declarations. + * These would fail when invoked since they are invoked without the `new` keyword. + */ +export type Callable = ( + // TODO @Shinigami92 2023-02-14: This `any` type can be fixed by anyone if they want to. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...args: any[] +) => unknown; + +/** + * Type that represents a single method/function name of the given type. + */ +export type MethodOf = { + [Key in keyof TObjectType]: TObjectType[Key] extends TSignature + ? Key extends string + ? Key + : never + : never; +}[keyof TObjectType]; + +/** + * Type that represents all method/function names of the given type. + */ +export type MethodsOf< + TObjectType, + TSignature extends Callable = Callable, +> = ReadonlyArray>; diff --git a/src/locale-proxy.ts b/src/locale-proxy.ts deleted file mode 100644 index e99ba655..00000000 --- a/src/locale-proxy.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { LocaleDefinition } from './definitions'; -import { FakerError } from './errors/faker-error'; - -/** - * A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined. - */ -export type LocaleProxy = Readonly<{ - [key in keyof LocaleDefinition]-?: LocaleProxyCategory; -}>; - -type LocaleProxyCategory = Readonly<{ - [key in keyof T]-?: LocaleProxyEntry; -}>; - -type LocaleProxyEntry = unknown extends T ? T : Readonly>; - -const throwReadOnlyError: () => never = () => { - throw new FakerError('You cannot edit the locale data on the faker instance'); -}; - -/** - * Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed. - * - * @param locale The locale definition to create the proxy for. - */ -export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy { - const proxies = {} as LocaleDefinition; - return new Proxy(locale, { - has(): true { - // Categories are always present (proxied), that's why we return true. - return true; - }, - - get( - target: LocaleDefinition, - categoryName: keyof LocaleDefinition - ): LocaleDefinition[keyof LocaleDefinition] { - if (typeof categoryName === 'symbol' || categoryName === 'nodeType') { - return target[categoryName]; - } - - if (categoryName in proxies) { - return proxies[categoryName]; - } - - return (proxies[categoryName] = createCategoryProxy( - categoryName, - target[categoryName] - )); - }, - - set: throwReadOnlyError, - deleteProperty: throwReadOnlyError, - }) as LocaleProxy; -} - -/** - * Checks that the value is not null or undefined and throws an error if it is. - * - * @param value The value to check. - * @param path The path to the locale data. - */ -export function assertLocaleData( - value: T, - ...path: string[] -): asserts value is NonNullable { - if (value === null) { - throw new FakerError( - `The locale data for '${path.join('.')}' aren't applicable to this locale. - If you think this is a bug, please report it at: https://github.com/faker-js/faker` - ); - } else if (value === undefined) { - throw new FakerError( - `The locale data for '${path.join('.')}' are missing in this locale. - Please contribute the missing data to the project or use a locale/Faker instance that has these data. - For more information see https://fakerjs.dev/guide/localization.html` - ); - } -} - -/** - * Creates a proxy for a category that throws an error when accessing an undefined property. - * - * @param categoryName The name of the category. - * @param categoryData The module to create the proxy for. - */ -function createCategoryProxy< - TCategoryData extends Record, ->( - categoryName: string, - categoryData: TCategoryData = {} as TCategoryData -): Required { - return new Proxy(categoryData, { - has(target: TCategoryData, entryName: keyof TCategoryData): boolean { - const value = target[entryName]; - return value != null; - }, - - get( - target: TCategoryData, - entryName: keyof TCategoryData - ): TCategoryData[keyof TCategoryData] { - const value = target[entryName]; - if (typeof entryName === 'symbol' || entryName === 'nodeType') { - return value; - } - - assertLocaleData(value, categoryName, entryName.toString()); - return value; - }, - - set: throwReadOnlyError, - deleteProperty: throwReadOnlyError, - }) as Required; -} diff --git a/src/modules/date/index.ts b/src/modules/date/index.ts index f2eac1cf..53204a03 100644 --- a/src/modules/date/index.ts +++ b/src/modules/date/index.ts @@ -1,8 +1,8 @@ import type { Faker } from '../..'; import type { DateEntryDefinition } from '../../definitions'; import { FakerError } from '../../errors/faker-error'; +import { assertLocaleData } from '../../internal/locale-proxy'; import { SimpleModuleBase } from '../../internal/module-base'; -import { assertLocaleData } from '../../locale-proxy'; /** * Converts a date passed as a `string`, `number` or `Date` to a valid `Date` object. diff --git a/src/modules/string/index.ts b/src/modules/string/index.ts index cfee0dc1..7df31fc7 100644 --- a/src/modules/string/index.ts +++ b/src/modules/string/index.ts @@ -1,6 +1,6 @@ import { FakerError } from '../../errors/faker-error'; import { SimpleModuleBase } from '../../internal/module-base'; -import type { LiteralUnion } from '../../utils/types'; +import type { LiteralUnion } from '../../internal/types'; export type Casing = 'upper' | 'lower' | 'mixed'; diff --git a/src/simple-faker.ts b/src/simple-faker.ts index e775cbc7..743df923 100644 --- a/src/simple-faker.ts +++ b/src/simple-faker.ts @@ -1,10 +1,10 @@ -import { generateMersenne53Randomizer } from './internal/mersenne'; import { DatatypeModule } from './modules/datatype'; import { SimpleDateModule } from './modules/date'; import { SimpleHelpersModule } from './modules/helpers'; import { NumberModule } from './modules/number'; import { StringModule } from './modules/string'; import type { Randomizer } from './randomizer'; +import { generateMersenne53Randomizer } from './utils/mersenne'; /** * This is a simplified Faker class that doesn't need any localized data to generate its output. diff --git a/src/utils/mersenne.ts b/src/utils/mersenne.ts new file mode 100644 index 00000000..de415f91 --- /dev/null +++ b/src/utils/mersenne.ts @@ -0,0 +1,48 @@ +import { MersenneTwister19937 } from '../internal/mersenne'; +import type { Randomizer } from '../randomizer'; + +/** + * Generates a MersenneTwister19937 randomizer with 32 bits of precision. + * This is the default randomizer used by faker prior to v9.0. + */ +export function generateMersenne32Randomizer(): Randomizer { + const twister = new MersenneTwister19937(); + + twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); + + return { + next(): number { + return twister.genrandReal2(); + }, + seed(seed: number | number[]): void { + if (typeof seed === 'number') { + twister.initGenrand(seed); + } else if (Array.isArray(seed)) { + twister.initByArray(seed, seed.length); + } + }, + }; +} + +/** + * Generates a MersenneTwister19937 randomizer with 53 bits of precision. + * This is the default randomizer used by faker starting with v9.0. + */ +export function generateMersenne53Randomizer(): Randomizer { + const twister = new MersenneTwister19937(); + + twister.initGenrand(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); + + return { + next(): number { + return twister.genrandRes53(); + }, + seed(seed: number | number[]): void { + if (typeof seed === 'number') { + twister.initGenrand(seed); + } else if (Array.isArray(seed)) { + twister.initByArray(seed, seed.length); + } + }, + }; +} diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index affdda7e..00000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Type that provides auto-suggestions but also any string. - * - * @see https://github.com/microsoft/TypeScript/issues/29729#issuecomment-471566609 - */ -export type LiteralUnion = - | TSuggested - | (TBase & { zz_IGNORE_ME?: never }); - -/** - * A function that returns a value. - * - * `Function` cannot be used instead because it doesn't accept class declarations. - * These would fail when invoked since they are invoked without the `new` keyword. - */ -export type Callable = ( - // TODO @Shinigami92 2023-02-14: This `any` type can be fixed by anyone if they want to. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...args: any[] -) => unknown; - -/** - * Type that represents a single method/function name of the given type. - */ -export type MethodOf = { - [Key in keyof TObjectType]: TObjectType[Key] extends TSignature - ? Key extends string - ? Key - : never - : never; -}[keyof TObjectType]; - -/** - * Type that represents all method/function names of the given type. - */ -export type MethodsOf< - TObjectType, - TSignature extends Callable = Callable, -> = ReadonlyArray>; diff --git a/test/internal/__snapshots__/mersenne.spec.ts.snap b/test/internal/__snapshots__/mersenne.spec.ts.snap deleted file mode 100644 index c045e2e7..00000000 --- a/test/internal/__snapshots__/mersenne.spec.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`generateMersenne32Randomizer() > seed: [42,1,2] > should return deterministic value for next() 1`] = `0.8562037434894592`; - -exports[`generateMersenne32Randomizer() > seed: [1211,1,2] > should return deterministic value for next() 1`] = `0.8916433283593506`; - -exports[`generateMersenne32Randomizer() > seed: [1337,1,2] > should return deterministic value for next() 1`] = `0.17990487208589911`; - -exports[`generateMersenne32Randomizer() > seed: 42 > should return deterministic value for next() 1`] = `0.37454011430963874`; - -exports[`generateMersenne32Randomizer() > seed: 1211 > should return deterministic value for next() 1`] = `0.9285201537422836`; - -exports[`generateMersenne32Randomizer() > seed: 1337 > should return deterministic value for next() 1`] = `0.2620246761944145`; - -exports[`generateMersenne53Randomizer() > seed: [42,1,2] > should return deterministic value for next() 1`] = `0.8562037477947296`; - -exports[`generateMersenne53Randomizer() > seed: [1211,1,2] > should return deterministic value for next() 1`] = `0.8916433279801969`; - -exports[`generateMersenne53Randomizer() > seed: [1337,1,2] > should return deterministic value for next() 1`] = `0.17990487224060836`; - -exports[`generateMersenne53Randomizer() > seed: 42 > should return deterministic value for next() 1`] = `0.3745401188473625`; - -exports[`generateMersenne53Randomizer() > seed: 1211 > should return deterministic value for next() 1`] = `0.9285201539025842`; - -exports[`generateMersenne53Randomizer() > seed: 1337 > should return deterministic value for next() 1`] = `0.2620246750155817`; diff --git a/test/internal/locale-proxy.spec.ts b/test/internal/locale-proxy.spec.ts new file mode 100644 index 00000000..1b6e060b --- /dev/null +++ b/test/internal/locale-proxy.spec.ts @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; +import { FakerError, en } from '../../src'; +import { createLocaleProxy } from '../../src/internal/locale-proxy'; + +describe('LocaleProxy', () => { + const locale = createLocaleProxy(en); + const enAirline = en.airline ?? { never: 'missing' }; + + describe('locale', () => { + it('should be possible to use equals on locale', () => { + expect(locale).toEqual(createLocaleProxy(en)); + }); + + it('should be possible to use not equals on locale', () => { + expect(locale).not.toEqual(createLocaleProxy({})); + }); + }); + + describe('category', () => { + it('should be possible to check for a missing category', () => { + expect('category' in locale).toBe(true); + }); + + it('should be possible to check for an existing category', () => { + expect('airline' in locale).toBe(true); + }); + + it('should be possible to access the title', () => { + expect(locale.metadata.title).toBe('English'); + }); + + it('should be possible to access a missing category', () => { + expect(locale.category).toBeDefined(); + }); + + it('should not be possible to add a new category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + locale.category = {}; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to replace a category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + locale.airline = {}; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to delete a missing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + delete locale.category; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to delete an existing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + delete locale.airline; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should be possible to get all categories keys on empty locale', () => { + const empty = createLocaleProxy({}); + + expect(Object.keys(empty)).toEqual([]); + }); + + it('should be possible to get all categories keys on actual locale', () => { + expect(Object.keys(locale).sort()).toEqual(Object.keys(en).sort()); + }); + }); + + describe('entry', () => { + it('should be possible to check for a missing entry in a missing category', () => { + expect('missing' in locale.category).toBe(false); + }); + + it('should be possible to check for a missing entry in a present category', () => { + expect('missing' in locale.airline).toBe(false); + }); + + it('should be possible to check for a present entry', () => { + expect('airline' in locale.airline).toBe(true); + }); + + it('should not be possible to access a missing entry in a missing category', () => { + expect(() => locale.category.missing).toThrow( + new FakerError( + `The locale data for 'category.missing' are missing in this locale. + Please contribute the missing data to the project or use a locale/Faker instance that has these data. + For more information see https://fakerjs.dev/guide/localization.html` + ) + ); + }); + + it('should not be possible to access a missing entry in a present category', () => { + expect(() => locale.airline.missing).toThrow( + new FakerError( + `The locale data for 'airline.missing' are missing in this locale. + Please contribute the missing data to the project or use a locale/Faker instance that has these data. + For more information see https://fakerjs.dev/guide/localization.html` + ) + ); + }); + + it('should be possible to access a present entry', () => { + expect(locale.airline.airline).toBeDefined(); + }); + + it('should not be possible to access an unavailable entry in a present category', () => { + const unavailable = createLocaleProxy({ + airline: { airline: null }, + }); + + expect(() => unavailable.airline.airline).toThrow( + new FakerError( + `The locale data for 'airline.airline' aren't applicable to this locale. + If you think this is a bug, please report it at: https://github.com/faker-js/faker` + ) + ); + }); + + it('should not be possible to add a new entry in a missing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + locale.category.missing = {}; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to add a new entry in an existing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + locale.airline.missing = {}; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to replace an entry in an existing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + locale.airline.airline = ['dummy']; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to delete a missing entry in a missing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + delete locale.category.missing; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to delete a missing entry in an existing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + delete locale.airline.missing; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should not be possible to delete an existing entry in an existing category', () => { + expect(() => { + // @ts-expect-error: LocaleProxy is read-only. + delete locale.airline.airline; + }).toThrow( + new FakerError('You cannot edit the locale data on the faker instance') + ); + }); + + it('should be possible to get all keys from missing category', () => { + expect(Object.keys(locale.missing)).toEqual([]); + }); + + it('should be possible to get all keys from existing category', () => { + expect(Object.keys(locale.airline).sort()).toEqual( + Object.keys(enAirline).sort() + ); + }); + }); +}); diff --git a/test/internal/mersenne-test-utils.ts b/test/internal/mersenne-test-utils.ts deleted file mode 100644 index ee7a43da..00000000 --- a/test/internal/mersenne-test-utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Moved to a separate file to avoid importing the tests - -/** - * The maximum value that can be returned by `MersenneTwister19937.genrandReal2()`. - * This is the max possible value with 32 bits of precision that is less than 1. - */ -export const TWISTER_32CO_MAX_VALUE = 0.9999999997671694; -/** - * The maximum value that can be returned by `MersenneTwister19937.genrandRes53()`. - * This is the max possible value with 53 bits of precision that is less than 1. - */ -export const TWISTER_53CO_MAX_VALUE = 0.9999999999999999; -// Re-exported because the value might change in the future -/** - * The maximum value that can be returned by `next()`. - */ -export const MERSENNE_MAX_VALUE = TWISTER_32CO_MAX_VALUE; diff --git a/test/internal/mersenne.spec.ts b/test/internal/mersenne.spec.ts deleted file mode 100644 index f0e48bd0..00000000 --- a/test/internal/mersenne.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { - MersenneTwister19937, - generateMersenne32Randomizer, - generateMersenne53Randomizer, -} from '../../src/internal/mersenne'; -import type { Randomizer } from '../../src/randomizer'; -import { seededRuns } from '../support/seeded-runs'; -import { times } from '../support/times'; -import { - MERSENNE_MAX_VALUE, - TWISTER_32CO_MAX_VALUE, - TWISTER_53CO_MAX_VALUE, -} from './mersenne-test-utils'; - -const NON_SEEDED_BASED_RUN = 25; - -function newTwister( - seed: number = Math.random() * Number.MAX_SAFE_INTEGER -): MersenneTwister19937 { - const twister = new MersenneTwister19937(); - twister.initGenrand(seed); - return twister; -} - -describe('MersenneTwister19937', () => { - describe('genrandInt32()', () => { - it('should be able to return 0', () => { - const twister = newTwister(257678572); - - // There is no single value seed that can produce 0 in the first call - for (let i = 0; i < 5; i++) { - twister.genrandInt32(); - } - - const actual = twister.genrandInt32(); - expect(actual).toBe(0); - }); - - it('should be able to return 2^32-1', () => { - const twister = newTwister(2855577693); - const actual = twister.genrandInt32(); - expect(actual).toBe(2 ** 32 - 1); - }); - }); - - describe('genrandReal2()', () => { - it('should be able to return 0', () => { - const twister = newTwister(); - // shortcut to return minimal value - // the test above shows that it is possible to return 0 - twister.genrandInt32 = () => 0; - const actual = twister.genrandReal2(); - expect(actual).toBe(0); - }); - - it('should be able to return almost 1', () => { - const twister = newTwister(); - // shortcut to return maximal value - // the test above shows that it is possible to return 2^32-1 - twister.genrandInt32 = () => 2 ** 32 - 1; - const actual = twister.genrandReal2(); - expect(actual).toBe(TWISTER_32CO_MAX_VALUE); - }); - }); - - describe('genrandRes53()', () => { - it('should be able to return 0', () => { - const twister = newTwister(); - // shortcut to return minimal value - // the test above shows that it is possible to return 0 - twister.genrandInt32 = () => 0; - const actual = twister.genrandRes53(); - expect(actual).toBe(0); - }); - - it('should be able to return almost 1', () => { - const twister = newTwister(); - // shortcut to return maximal value - // the test above shows that it is possible to return 2^32-1 - twister.genrandInt32 = () => 2 ** 32 - 1; - const actual = twister.genrandRes53(); - expect(actual).toBe(TWISTER_53CO_MAX_VALUE); - }); - }); -}); - -describe.each([ - ['generateMersenne32Randomizer()', generateMersenne32Randomizer], - ['generateMersenne53Randomizer()', generateMersenne53Randomizer], -])('%s', (_, factory) => { - const randomizer: Randomizer = factory(); - - it('should return a result matching the interface', () => { - expect(randomizer).toBeDefined(); - expect(randomizer).toBeTypeOf('object'); - expect(randomizer.next).toBeTypeOf('function'); - expect(randomizer.seed).toBeTypeOf('function'); - }); - - describe.each( - [...seededRuns, ...seededRuns.map((v) => [v, 1, 2])].map((v) => [v]) - )('seed: %j', (seed) => { - beforeEach(() => { - randomizer.seed(seed); - }); - - it('should return deterministic value for next()', () => { - const actual = randomizer.next(); - - expect(actual).toMatchSnapshot(); - }); - }); - - function randomSeed(): number { - return Math.ceil(Math.random() * 1_000_000_000); - } - - // Create and log-back the seed for debug purposes - describe.each( - times(NON_SEEDED_BASED_RUN).flatMap(() => [ - [randomSeed()], - [[randomSeed(), randomSeed()]], - ]) - )('random seeded tests %j', (seed) => { - beforeAll(() => { - randomizer.seed(seed); - }); - - describe('next', () => { - it('should return random number from interval [0, 1)', () => { - const actual = randomizer.next(); - - expect(actual).toBeGreaterThanOrEqual(0); - expect(actual).toBeLessThanOrEqual(MERSENNE_MAX_VALUE); - expect(actual).toBeLessThan(1); - }); - }); - }); -}); diff --git a/test/locale-proxy.spec.ts b/test/locale-proxy.spec.ts deleted file mode 100644 index c9c8928c..00000000 --- a/test/locale-proxy.spec.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { FakerError, en } from '../src'; -import { createLocaleProxy } from '../src/locale-proxy'; - -describe('LocaleProxy', () => { - const locale = createLocaleProxy(en); - const enAirline = en.airline ?? { never: 'missing' }; - - describe('locale', () => { - it('should be possible to use equals on locale', () => { - expect(locale).toEqual(createLocaleProxy(en)); - }); - - it('should be possible to use not equals on locale', () => { - expect(locale).not.toEqual(createLocaleProxy({})); - }); - }); - - describe('category', () => { - it('should be possible to check for a missing category', () => { - expect('category' in locale).toBe(true); - }); - - it('should be possible to check for an existing category', () => { - expect('airline' in locale).toBe(true); - }); - - it('should be possible to access the title', () => { - expect(locale.metadata.title).toBe('English'); - }); - - it('should be possible to access a missing category', () => { - expect(locale.category).toBeDefined(); - }); - - it('should not be possible to add a new category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - locale.category = {}; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to replace a category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - locale.airline = {}; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to delete a missing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - delete locale.category; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to delete an existing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - delete locale.airline; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should be possible to get all categories keys on empty locale', () => { - const empty = createLocaleProxy({}); - - expect(Object.keys(empty)).toEqual([]); - }); - - it('should be possible to get all categories keys on actual locale', () => { - expect(Object.keys(locale).sort()).toEqual(Object.keys(en).sort()); - }); - }); - - describe('entry', () => { - it('should be possible to check for a missing entry in a missing category', () => { - expect('missing' in locale.category).toBe(false); - }); - - it('should be possible to check for a missing entry in a present category', () => { - expect('missing' in locale.airline).toBe(false); - }); - - it('should be possible to check for a present entry', () => { - expect('airline' in locale.airline).toBe(true); - }); - - it('should not be possible to access a missing entry in a missing category', () => { - expect(() => locale.category.missing).toThrow( - new FakerError( - `The locale data for 'category.missing' are missing in this locale. - Please contribute the missing data to the project or use a locale/Faker instance that has these data. - For more information see https://fakerjs.dev/guide/localization.html` - ) - ); - }); - - it('should not be possible to access a missing entry in a present category', () => { - expect(() => locale.airline.missing).toThrow( - new FakerError( - `The locale data for 'airline.missing' are missing in this locale. - Please contribute the missing data to the project or use a locale/Faker instance that has these data. - For more information see https://fakerjs.dev/guide/localization.html` - ) - ); - }); - - it('should be possible to access a present entry', () => { - expect(locale.airline.airline).toBeDefined(); - }); - - it('should not be possible to access an unavailable entry in a present category', () => { - const unavailable = createLocaleProxy({ - airline: { airline: null }, - }); - - expect(() => unavailable.airline.airline).toThrow( - new FakerError( - `The locale data for 'airline.airline' aren't applicable to this locale. - If you think this is a bug, please report it at: https://github.com/faker-js/faker` - ) - ); - }); - - it('should not be possible to add a new entry in a missing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - locale.category.missing = {}; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to add a new entry in an existing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - locale.airline.missing = {}; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to replace an entry in an existing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - locale.airline.airline = ['dummy']; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to delete a missing entry in a missing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - delete locale.category.missing; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to delete a missing entry in an existing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - delete locale.airline.missing; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should not be possible to delete an existing entry in an existing category', () => { - expect(() => { - // @ts-expect-error: LocaleProxy is read-only. - delete locale.airline.airline; - }).toThrow( - new FakerError('You cannot edit the locale data on the faker instance') - ); - }); - - it('should be possible to get all keys from missing category', () => { - expect(Object.keys(locale.missing)).toEqual([]); - }); - - it('should be possible to get all keys from existing category', () => { - expect(Object.keys(locale.airline).sort()).toEqual( - Object.keys(enAirline).sort() - ); - }); - }); -}); diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index f318fc5b..f9adf9ff 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -1,8 +1,8 @@ import validator from 'validator'; import { describe, expect, it } from 'vitest'; import { FakerError, SimpleFaker, faker } from '../../src'; -import { MERSENNE_MAX_VALUE } from '../internal/mersenne-test-utils'; import { seededTests } from '../support/seeded-runs'; +import { MERSENNE_MAX_VALUE } from '../utils/mersenne-test-utils'; import { times } from './../support/times'; describe('number', () => { diff --git a/test/scripts/apidocs/method.example.ts b/test/scripts/apidocs/method.example.ts index e72f5f3f..4ecdaee1 100644 --- a/test/scripts/apidocs/method.example.ts +++ b/test/scripts/apidocs/method.example.ts @@ -1,7 +1,7 @@ import type { Casing, ColorFormat } from '../../../src'; import { FakerError } from '../../../src/errors/faker-error'; +import type { LiteralUnion } from '../../../src/internal/types'; import type { AlphaNumericChar } from '../../../src/modules/string'; -import type { LiteralUnion } from '../../../src/utils/types'; // explicitly export types so they show up in the docs as decomposed types export type { NumberColorFormat, StringColorFormat } from '../../../src'; export type { AlphaNumericChar, Casing, ColorFormat, LiteralUnion }; diff --git a/test/scripts/apidocs/verify-jsdoc-tags.spec.ts b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts index ea5ed8b9..753ef9b3 100644 --- a/test/scripts/apidocs/verify-jsdoc-tags.spec.ts +++ b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts @@ -119,7 +119,7 @@ describe('verify JSDoc tags', () => { ); if (moduleName === 'randomizer') { - examples = `import { generateMersenne32Randomizer } from '${relativeImportPath}/internal/mersenne'; + examples = `import { generateMersenne32Randomizer } from '${relativeImportPath}/utils/mersenne'; const randomizer = generateMersenne32Randomizer(); diff --git a/test/support/seeded-runs.ts b/test/support/seeded-runs.ts index a05f1854..dee0051c 100644 --- a/test/support/seeded-runs.ts +++ b/test/support/seeded-runs.ts @@ -1,6 +1,6 @@ import { describe, expect, describe as vi_describe, it as vi_it } from 'vitest'; import type { Faker } from '../../src/faker'; -import type { Callable, MethodOf } from '../../src/utils/types'; +import type { Callable, MethodOf } from '../../src/internal/types'; export const seededRuns = [42, 1337, 1211]; diff --git a/test/utils/__snapshots__/mersenne.spec.ts.snap b/test/utils/__snapshots__/mersenne.spec.ts.snap new file mode 100644 index 00000000..c045e2e7 --- /dev/null +++ b/test/utils/__snapshots__/mersenne.spec.ts.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateMersenne32Randomizer() > seed: [42,1,2] > should return deterministic value for next() 1`] = `0.8562037434894592`; + +exports[`generateMersenne32Randomizer() > seed: [1211,1,2] > should return deterministic value for next() 1`] = `0.8916433283593506`; + +exports[`generateMersenne32Randomizer() > seed: [1337,1,2] > should return deterministic value for next() 1`] = `0.17990487208589911`; + +exports[`generateMersenne32Randomizer() > seed: 42 > should return deterministic value for next() 1`] = `0.37454011430963874`; + +exports[`generateMersenne32Randomizer() > seed: 1211 > should return deterministic value for next() 1`] = `0.9285201537422836`; + +exports[`generateMersenne32Randomizer() > seed: 1337 > should return deterministic value for next() 1`] = `0.2620246761944145`; + +exports[`generateMersenne53Randomizer() > seed: [42,1,2] > should return deterministic value for next() 1`] = `0.8562037477947296`; + +exports[`generateMersenne53Randomizer() > seed: [1211,1,2] > should return deterministic value for next() 1`] = `0.8916433279801969`; + +exports[`generateMersenne53Randomizer() > seed: [1337,1,2] > should return deterministic value for next() 1`] = `0.17990487224060836`; + +exports[`generateMersenne53Randomizer() > seed: 42 > should return deterministic value for next() 1`] = `0.3745401188473625`; + +exports[`generateMersenne53Randomizer() > seed: 1211 > should return deterministic value for next() 1`] = `0.9285201539025842`; + +exports[`generateMersenne53Randomizer() > seed: 1337 > should return deterministic value for next() 1`] = `0.2620246750155817`; diff --git a/test/utils/mersenne-test-utils.ts b/test/utils/mersenne-test-utils.ts new file mode 100644 index 00000000..ee7a43da --- /dev/null +++ b/test/utils/mersenne-test-utils.ts @@ -0,0 +1,17 @@ +// Moved to a separate file to avoid importing the tests + +/** + * The maximum value that can be returned by `MersenneTwister19937.genrandReal2()`. + * This is the max possible value with 32 bits of precision that is less than 1. + */ +export const TWISTER_32CO_MAX_VALUE = 0.9999999997671694; +/** + * The maximum value that can be returned by `MersenneTwister19937.genrandRes53()`. + * This is the max possible value with 53 bits of precision that is less than 1. + */ +export const TWISTER_53CO_MAX_VALUE = 0.9999999999999999; +// Re-exported because the value might change in the future +/** + * The maximum value that can be returned by `next()`. + */ +export const MERSENNE_MAX_VALUE = TWISTER_32CO_MAX_VALUE; diff --git a/test/utils/mersenne.spec.ts b/test/utils/mersenne.spec.ts new file mode 100644 index 00000000..5e9e4c27 --- /dev/null +++ b/test/utils/mersenne.spec.ts @@ -0,0 +1,140 @@ +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { MersenneTwister19937 } from '../../src/internal/mersenne'; +import type { Randomizer } from '../../src/randomizer'; +import { + generateMersenne32Randomizer, + generateMersenne53Randomizer, +} from '../../src/utils/mersenne'; +import { seededRuns } from '../support/seeded-runs'; +import { times } from '../support/times'; +import { + MERSENNE_MAX_VALUE, + TWISTER_32CO_MAX_VALUE, + TWISTER_53CO_MAX_VALUE, +} from './mersenne-test-utils'; + +const NON_SEEDED_BASED_RUN = 25; + +function newTwister( + seed: number = Math.random() * Number.MAX_SAFE_INTEGER +): MersenneTwister19937 { + const twister = new MersenneTwister19937(); + twister.initGenrand(seed); + return twister; +} + +describe('MersenneTwister19937', () => { + describe('genrandInt32()', () => { + it('should be able to return 0', () => { + const twister = newTwister(257678572); + + // There is no single value seed that can produce 0 in the first call + for (let i = 0; i < 5; i++) { + twister.genrandInt32(); + } + + const actual = twister.genrandInt32(); + expect(actual).toBe(0); + }); + + it('should be able to return 2^32-1', () => { + const twister = newTwister(2855577693); + const actual = twister.genrandInt32(); + expect(actual).toBe(2 ** 32 - 1); + }); + }); + + describe('genrandReal2()', () => { + it('should be able to return 0', () => { + const twister = newTwister(); + // shortcut to return minimal value + // the test above shows that it is possible to return 0 + twister.genrandInt32 = () => 0; + const actual = twister.genrandReal2(); + expect(actual).toBe(0); + }); + + it('should be able to return almost 1', () => { + const twister = newTwister(); + // shortcut to return maximal value + // the test above shows that it is possible to return 2^32-1 + twister.genrandInt32 = () => 2 ** 32 - 1; + const actual = twister.genrandReal2(); + expect(actual).toBe(TWISTER_32CO_MAX_VALUE); + }); + }); + + describe('genrandRes53()', () => { + it('should be able to return 0', () => { + const twister = newTwister(); + // shortcut to return minimal value + // the test above shows that it is possible to return 0 + twister.genrandInt32 = () => 0; + const actual = twister.genrandRes53(); + expect(actual).toBe(0); + }); + + it('should be able to return almost 1', () => { + const twister = newTwister(); + // shortcut to return maximal value + // the test above shows that it is possible to return 2^32-1 + twister.genrandInt32 = () => 2 ** 32 - 1; + const actual = twister.genrandRes53(); + expect(actual).toBe(TWISTER_53CO_MAX_VALUE); + }); + }); +}); + +describe.each([ + ['generateMersenne32Randomizer()', generateMersenne32Randomizer], + ['generateMersenne53Randomizer()', generateMersenne53Randomizer], +])('%s', (_, factory) => { + const randomizer: Randomizer = factory(); + + it('should return a result matching the interface', () => { + expect(randomizer).toBeDefined(); + expect(randomizer).toBeTypeOf('object'); + expect(randomizer.next).toBeTypeOf('function'); + expect(randomizer.seed).toBeTypeOf('function'); + }); + + describe.each( + [...seededRuns, ...seededRuns.map((v) => [v, 1, 2])].map((v) => [v]) + )('seed: %j', (seed) => { + beforeEach(() => { + randomizer.seed(seed); + }); + + it('should return deterministic value for next()', () => { + const actual = randomizer.next(); + + expect(actual).toMatchSnapshot(); + }); + }); + + function randomSeed(): number { + return Math.ceil(Math.random() * 1_000_000_000); + } + + // Create and log-back the seed for debug purposes + describe.each( + times(NON_SEEDED_BASED_RUN).flatMap(() => [ + [randomSeed()], + [[randomSeed(), randomSeed()]], + ]) + )('random seeded tests %j', (seed) => { + beforeAll(() => { + randomizer.seed(seed); + }); + + describe('next', () => { + it('should return random number from interval [0, 1)', () => { + const actual = randomizer.next(); + + expect(actual).toBeGreaterThanOrEqual(0); + expect(actual).toBeLessThanOrEqual(MERSENNE_MAX_VALUE); + expect(actual).toBeLessThan(1); + }); + }); + }); +}); -- cgit v1.2.3