diff options
| author | 2023-05-18 18:32:31 +0000 | |
|---|---|---|
| committer | 2023-05-18 11:32:31 -0700 | |
| commit | 228ca3269a97d9beb72d2a959f65aea7fb0b3964 (patch) | |
| tree | c656e893e66c3a3b59db75cd40688f2d80b8c1d3 | |
| parent | 621232c197c0f6f562061e976ae39301cf85897f (diff) | |
| download | bun-228ca3269a97d9beb72d2a959f65aea7fb0b3964.tar.gz bun-228ca3269a97d9beb72d2a959f65aea7fb0b3964.tar.zst bun-228ca3269a97d9beb72d2a959f65aea7fb0b3964.zip | |
Implement `expect().toBeCloseTo()` (#2870)
| -rw-r--r-- | packages/bun-types/bun-test.d.ts | 20 | ||||
| -rw-r--r-- | src/bun.js/test/jest.zig | 99 | ||||
| -rw-r--r-- | test/js/bun/test/expect.test.ts | 49 | ||||
| -rw-r--r-- | test/js/bun/test/test-test.test.ts | 2 |
4 files changed, 167 insertions, 3 deletions
diff --git a/packages/bun-types/bun-test.d.ts b/packages/bun-types/bun-test.d.ts index d9bf36124..25997e2bb 100644 --- a/packages/bun-types/bun-test.d.ts +++ b/packages/bun-types/bun-test.d.ts @@ -33,7 +33,7 @@ declare module "bun:test" { */ export type Describe = { (label: string, fn: () => void): void; - skip: (label: string, fn: () => void) => void + skip: (label: string, fn: () => void) => void; }; /** * Describes a group of related tests. @@ -243,6 +243,24 @@ declare module "bun:test" { */ toBe(expected: T): void; /** + * Asserts that value is close to the expected by floating point precision. + * + * For example, the following fails because arithmetic on decimal (base 10) + * values often have rounding errors in limited precision binary (base 2) representation. + * + * @example + * expect(0.2 + 0.1).toBe(0.3); // fails + * + * Use `toBeCloseTo` to compare floating point numbers for approximate equality. + * + * @example + * expect(0.2 + 0.1).toBeCloseTo(0.3, 5); // passes + * + * @param expected the expected value + * @param numDigits the number of digits to check after the decimal point. Default is `2` + */ + toBeCloseTo(expected: number, numDigits?: number): void; + /** * Asserts that a value is deeply equal to what is expected. * * @example diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 5b0315f78..e7f83110c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -2169,6 +2169,104 @@ pub const Expect = struct { return .zero; } + pub fn toBeCloseTo(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { + defer this.postMatch(globalObject); + + const thisValue = callFrame.this(); + const thisArguments = callFrame.arguments(2); + const arguments = thisArguments.ptr[0..thisArguments.len]; + + if (arguments.len < 1) { + globalObject.throwInvalidArguments("toBeCloseTo() requires at least 1 argument. Expected value must be a number", .{}); + return .zero; + } + + const expected_ = arguments[0]; + if (!expected_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "expected", "number"); + return .zero; + } + + var precision: f64 = 2.0; + if (arguments.len > 1) { + const precision_ = arguments[1]; + if (!precision_.isNumber()) { + globalObject.throwInvalidArgumentType("toBeCloseTo", "precision", "number"); + return .zero; + } + + precision = precision_.asNumber(); + } + + const received_: JSC.JSValue = Expect.capturedValueGetCached(thisValue) orelse { + globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{}); + return .zero; + }; + + if (!received_.isNumber()) { + globalObject.throwInvalidArgumentType("expect", "received", "number"); + return .zero; + } + + var expected = expected_.asNumber(); + var received = received_.asNumber(); + + if (std.math.isNegativeInf(expected)) { + expected = -expected; + } + + if (std.math.isNegativeInf(received)) { + received = -received; + } + + if (std.math.isPositiveInf(expected) and std.math.isPositiveInf(received)) { + return thisValue; + } + + const expected_diff = std.math.pow(f64, 10, -precision) / 2; + const actual_diff = std.math.fabs(received - expected); + var pass = actual_diff < expected_diff; + + const not = this.op.contains(.not); + if (not) pass = !pass; + + if (pass) return thisValue; + + var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true }; + + const expected_fmt = expected_.toFmt(globalObject, &formatter); + const received_fmt = received_.toFmt(globalObject, &formatter); + + const expected_line = "Expected: <green>{any}<r>\n"; + const received_line = "Received: <red>{any}<r>\n"; + const expected_precision = "Expected precision: {d}\n"; + const expected_difference = "Expected difference: \\< <green>{d}<r>\n"; + const received_difference = "Received difference: <red>{d}<r>\n"; + + const suffix_fmt = "\n\n" ++ expected_line ++ received_line ++ "\n" ++ expected_precision ++ expected_difference ++ received_difference; + + if (not) { + const fmt = comptime getSignature("toBeCloseTo", "<green>expected<r>, precision", true) ++ suffix_fmt; + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + const fmt = comptime getSignature("toBeCloseTo", "<green>expected<r>, precision", false) ++ suffix_fmt; + + if (Output.enable_ansi_colors) { + globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + + globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt, precision, expected_diff, actual_diff }); + return .zero; + } + pub fn toBeOdd(this: *Expect, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { defer this.postMatch(globalObject); @@ -2948,7 +3046,6 @@ pub const Expect = struct { pub const toHaveReturnedWith = notImplementedJSCFn; pub const toHaveLastReturnedWith = notImplementedJSCFn; pub const toHaveNthReturnedWith = notImplementedJSCFn; - pub const toBeCloseTo = notImplementedJSCFn; pub const toContainEqual = notImplementedJSCFn; pub const toMatchObject = notImplementedJSCFn; pub const toMatchInlineSnapshot = notImplementedJSCFn; diff --git a/test/js/bun/test/expect.test.ts b/test/js/bun/test/expect.test.ts index ae535a6f7..2fabd6e66 100644 --- a/test/js/bun/test/expect.test.ts +++ b/test/js/bun/test/expect.test.ts @@ -78,4 +78,53 @@ describe("expect()", () => { test(label, () => expect(value).toMatch(matched)); } }); + + describe("toBeCloseTo()", () => { + const passTests = [ + [0, 0], + [0, 0.001], + [1.23, 1.229], + [1.23, 1.226], + [1.23, 1.225], + [1.23, 1.234], + [Infinity, Infinity], + [-Infinity, -Infinity], + [0, 0.1, 0], + [0, 0.0001, 3], + [0, 0.000004, 5], + [2.0000002, 2, 5], + ]; + for (const [actual, expected, precision] of passTests) { + if (precision === undefined) { + test(`actual = ${actual}, expected = ${expected}`, () => { + expect(actual).toBeCloseTo(expected); + }); + } else { + test(`actual = ${actual}, expected = ${expected}, precision = ${precision}`, () => { + expect(actual).toBeCloseTo(expected, precision); + }); + } + } + const failTests = [ + [0, 0.01], + [1, 1.23], + [1.23, 1.2249999], + [Infinity, -Infinity], + [Infinity, 1.23], + [-Infinity, -1.23], + [3.141592e-7, 3e-7, 8], + [56789, 51234, -4], + ]; + for (const [actual, expected, precision] of failTests) { + if (precision === undefined) { + test(`actual = ${actual}, expected != ${expected}`, () => { + expect(actual).not.toBeCloseTo(expected); + }); + } else { + test(`actual = ${actual}, expected != ${expected}, precision = ${precision}`, () => { + expect(actual).not.toBeCloseTo(expected, precision); + }); + } + } + }); }); diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index c739a8921..ed00c57ed 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { spawn, spawnSync } from "bun"; -import { describe, expect, it, test } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test } from "bun:test"; import { mkdirSync, realpathSync, rmSync, writeFileSync } from "fs"; import { mkdtemp, rm, writeFile } from "fs/promises"; import { bunEnv, bunExe } from "harness"; |
