Skip to content

Commit

Permalink
feat: vitest equality tests using equivalences
Browse files Browse the repository at this point in the history
  • Loading branch information
sukovanej committed Apr 27, 2024
1 parent 3144f83 commit 12a22ec
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 29 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@typescript-eslint/parser": "^7.7.0",
"@vitest/browser": "^1.5.0",
"@vitest/coverage-v8": "^1.5.0",
"@vitest/expect": "^1.5.0",
"@vitest/web-worker": "^1.5.0",
"babel-plugin-annotate-pure-calls": "^0.4.0",
"eslint": "^8.57.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/effect/src/HashMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type { Equal } from "./Equal.js"
import type { Equivalence } from "./Equivalence.js"
import type { HashSet } from "./HashSet.js"
import type { Inspectable } from "./Inspectable.js"
import * as HM from "./internal/hashMap.js"
Expand Down Expand Up @@ -419,3 +420,11 @@ export const findFirst: {
<K, A, B extends A>(self: HashMap<K, A>, predicate: (a: A, k: K) => a is B): Option<[K, B]>
<K, A>(self: HashMap<K, A>, predicate: (a: A, k: K) => boolean): Option<[K, A]>
} = HM.findFirst

/**
* @category elements
* @since 3.1.0
*/
export const getEquivalence: <K, V>(
equivalences: { key: Equivalence<K>; value: Equivalence<V> }
) => Equivalence<HashMap<K, V>> = HM.getEquivalence
9 changes: 9 additions & 0 deletions packages/effect/src/HashSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type { Equal } from "./Equal.js"
import type { Equivalence } from "./Equivalence.js"
import type { Inspectable } from "./Inspectable.js"
import * as HS from "./internal/hashSet.js"
import type { Pipeable } from "./Pipeable.js"
Expand Down Expand Up @@ -298,3 +299,11 @@ export const partition: {
): [excluded: HashSet<Exclude<A, B>>, satisfying: HashSet<B>]
<A>(self: HashSet<A>, predicate: Predicate<A>): [excluded: HashSet<A>, satisfying: HashSet<A>]
} = HS.partition

/**
* @category elements
* @since 3.1.0
*/
export const getEquivalence: <A>(
equivalence: Equivalence<A>
) => Equivalence<HashSet<A>> = HS.getEquivalence
31 changes: 31 additions & 0 deletions packages/effect/src/internal/hashMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Equal from "../Equal.js"
import * as Equivalence from "../Equivalence.js"
import * as Dual from "../Function.js"
import { identity, pipe } from "../Function.js"
import * as Hash from "../Hash.js"
Expand Down Expand Up @@ -538,3 +539,33 @@ export const findFirst: {
return Option.none()
}
)

/** @internal */
export const getEquivalence = <K, V>(
{ key, value }: { key: Equivalence.Equivalence<K>; value: Equivalence.Equivalence<V> }
): Equivalence.Equivalence<HM.HashMap<K, V>> => {
// TODO:
return Equivalence.make((self, that) => {
if (size(that) !== size(self)) {
return false
}

for (const [k1, v1] of self) {
let found = false
for (const [k2, v2] of that) {
if (key(k1, k2)) {
if (!value(v1, v2)) {
return false
}
found = true
break
}
}
if (!found) {
return false
}
}

return true
})
}
10 changes: 10 additions & 0 deletions packages/effect/src/internal/hashSet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Equal from "../Equal.js"
import * as Equivalence from "../Equivalence.js"
import { dual } from "../Function.js"
import * as Hash from "../Hash.js"
import type { HashMap } from "../HashMap.js"
Expand Down Expand Up @@ -320,3 +321,12 @@ export const partition: {
}
return [endMutation(left), endMutation(right)]
})

/** @internal */
export const getEquivalence = <A>(
equivalence: Equivalence.Equivalence<A>
): Equivalence.Equivalence<HS.HashSet<A>> => {
const hmEq = HM.getEquivalence({ key: equivalence, value: () => true })

return Equivalence.make((self, that) => hmEq((self as HashSetImpl<A>)._keyMap, (that as HashSetImpl<A>)._keyMap))
}
25 changes: 25 additions & 0 deletions packages/effect/test/HashMap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,29 @@ describe("HashMap", () => {
expect(HM.findFirst(map1, (v, _k) => v.s === "bb")).toStrictEqual(Option.some([key(1), value("bb")]))
expect(HM.findFirst(map1, (v, k) => k.n === 0 && v.s === "bb")).toStrictEqual(Option.none())
})

it("equivalence", () => {
const eq = HM.getEquivalence({ key: Equal.equals, value: Equal.equals })

expect(eq(HM.empty(), HM.empty())).toBe(true)
expect(eq(HM.empty(), HM.make([1, 1]))).toBe(false)
expect(eq(HM.make([1, 1]), HM.make([1, 1]))).toBe(true)
expect(eq(HM.make([1, 1], [2, 2]), HM.make([2, 2], [1, 1]))).toBe(true)
expect(eq(HM.make([1, 1]), HM.make([1, 2]))).toBe(false)
expect(eq(HM.make([1, 1]), HM.make([1, 1], [2, 2]))).toBe(false)
})

it("vitest equality", () => {
expect(HM.empty()).toStrictEqual(HM.empty())
expect(HM.make([1, 1])).toStrictEqual(HM.make([1, 1]))
expect(HM.make([[1, 2], 2])).toStrictEqual(HM.make([[1, 2], 2]))
expect(HM.make([1, [1, 2]])).toStrictEqual(HM.make([1, [1, 2]]))

expect(HM.empty()).not.toStrictEqual(HM.make([1, 1]))
expect(HM.make([1, 1])).not.toStrictEqual(HM.make([2, 2]))
expect(HM.make([1, 1])).not.toStrictEqual(HM.make([1, 1], [2, 2]))
expect(HM.make([1, 1])).not.toStrictEqual(HM.make([1, 2]))
expect(HM.make([[1, 1], 2])).not.toStrictEqual(HM.make([[1, 2], 2]))
expect(HM.make([1, [1, 1]])).not.toStrictEqual(HM.make([1, [1, 2]]))
})
})
17 changes: 17 additions & 0 deletions packages/effect/test/HashSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,21 @@ describe("HashSet", () => {
expect(HashSet.isHashSet(null)).toBe(false)
expect(HashSet.isHashSet({})).toBe(false)
})

it("equivalence", () => {
const eq = HashSet.getEquivalence(Equal.equals)
expect(eq(HashSet.empty(), HashSet.empty())).toBe(true)
expect(eq(HashSet.empty(), HashSet.make(1))).toBe(false)
expect(eq(HashSet.make(1), HashSet.make(1))).toBe(true)
expect(eq(HashSet.make(2, 1), HashSet.make(1, 2))).toBe(true)
expect(eq(HashSet.make(1, 2), HashSet.make(1, 2, 3))).toBe(false)
})

it("vitest equality", () => {
expect(HashSet.empty()).toStrictEqual(HashSet.empty())
expect(HashSet.make(1)).toStrictEqual(HashSet.make(1))

expect(HashSet.empty()).not.toStrictEqual(HashSet.make(1))
expect(HashSet.make(1)).not.toStrictEqual(HashSet.make(1, 2))
})
})
55 changes: 55 additions & 0 deletions packages/vitest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
/**
* @since 1.0.0
*/
import type { Tester, TesterContext } from "@vitest/expect"
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Either from "effect/Either"
import type * as Equivalence from "effect/Equivalence"
import { pipe } from "effect/Function"
import * as HashMap from "effect/HashMap"
import * as HashSet from "effect/HashSet"
import * as Layer from "effect/Layer"
import * as Logger from "effect/Logger"
import * as Option from "effect/Option"
import * as Schedule from "effect/Schedule"
import type * as Scope from "effect/Scope"
import * as TestEnvironment from "effect/TestContext"
Expand All @@ -22,6 +28,55 @@ const TestEnv = TestEnvironment.TestContext.pipe(
Layer.provide(Logger.remove(Logger.defaultLogger))
)

/** @internal */
const makeCustomTesters = (equivalence: Equivalence.Equivalence<unknown>): ReadonlyArray<
{ is: (u: unknown) => boolean; equals: (a: any, b: any) => boolean }
> => [
{
is: Option.isOption,
equals: Option.getEquivalence(equivalence)
},
{
is: Either.isEither,
equals: Either.getEquivalence({ left: equivalence, right: equivalence })
},
{
is: HashSet.isHashSet,
equals: HashSet.getEquivalence(equivalence)
},
{
is: HashMap.isHashMap,
equals: HashMap.getEquivalence({ key: equivalence, value: equivalence })
}
]

/** @internal */
function customTester(this: TesterContext, a: unknown, b: unknown, customTesters: Array<Tester>) {
const testers = makeCustomTesters((a, b) => this.equals(a, b, customTesters))

for (const { equals, is } of testers) {
const isA = is(a)
const isB = is(b)

if (isA && isB) {
return equals(a, b)
}

if (isA || isB) {
return false
}
}

return undefined
}

/**
* @since 1.0.0
*/
export const addEqualityTesters = () => {
V.expect.addEqualityTesters([customTester])
}

/**
* @since 1.0.0
*/
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 2 additions & 29 deletions setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@
import { equals } from "@vitest/expect"
import { expect } from "vitest"
import * as effectVitest from "@effect/vitest"

// workaround for https://github.com/vitest-dev/vitest/issues/5620

function hasIterator(object: any) {
return !!(object !== null && object[Symbol.iterator])
}

expect.addEqualityTesters([(a: unknown, b: unknown) => {
if (
typeof a !== "object" ||
typeof b !== "object" ||
Array.isArray(a) ||
Array.isArray(b) ||
!hasIterator(a) ||
!hasIterator(b) ||
a === null ||
b === null
) {
return undefined
}

const aEntries = Object.entries(a)
const bEntries = Object.entries(b)

if (!equals(aEntries, bEntries)) return false

return undefined
}])
effectVitest.addEqualityTesters()

0 comments on commit 12a22ec

Please sign in to comment.