mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
33 Commits
claude/fix
...
pfg/add-sn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b556b9af | ||
|
|
d78d473e0f | ||
|
|
0c14fc2ba2 | ||
|
|
58e4488a58 | ||
|
|
bbc263d488 | ||
|
|
20fd605fbb | ||
|
|
63d16cee82 | ||
|
|
11ccf7e545 | ||
|
|
dc3a410963 | ||
|
|
efdb2e8769 | ||
|
|
3bfdc4b077 | ||
|
|
6e02340883 | ||
|
|
8b583c8b5f | ||
|
|
5ec3734d14 | ||
|
|
e55ebfdbd0 | ||
|
|
52fbeee121 | ||
|
|
4ee10fd1b5 | ||
|
|
f127a74aa5 | ||
|
|
b9fc1261f8 | ||
|
|
a6e55ccd55 | ||
|
|
f381f94ca5 | ||
|
|
62c78b46f2 | ||
|
|
2a8b5739ff | ||
|
|
b4e7d5ee69 | ||
|
|
0cfd29fcf3 | ||
|
|
2b81198922 | ||
|
|
198f21aab6 | ||
|
|
86436f51de | ||
|
|
ed877e38e1 | ||
|
|
b980e26fe7 | ||
|
|
5556d2781e | ||
|
|
67bdd10889 | ||
|
|
ba2c6ca29d |
48
packages/bun-types/test.d.ts
vendored
48
packages/bun-types/test.d.ts
vendored
@@ -697,8 +697,56 @@ declare module "bun:test" {
|
||||
* Ensures that a specific number of assertions are made
|
||||
*/
|
||||
assertions(neededAssertions: number): void;
|
||||
|
||||
/**
|
||||
* Add a custom snapshot serializer to customize how values are formatted in snapshots.
|
||||
*
|
||||
* @example
|
||||
* class Point {
|
||||
* constructor(public x: number, public y: number) {}
|
||||
* }
|
||||
*
|
||||
* expect.addSnapshotSerializer({
|
||||
* test: (val) => val instanceof Point,
|
||||
* serialize: (val) => `Point(${val.x}, ${val.y})`,
|
||||
* });
|
||||
*
|
||||
* expect(new Point(1, 2)).toMatchInlineSnapshot(`Point(1, 2)`);
|
||||
*
|
||||
* @param serializer The snapshot serializer configuration
|
||||
*/
|
||||
addSnapshotSerializer<T>(serializer: SnapshotSerializer<T>): void;
|
||||
}
|
||||
|
||||
export type SnapshotSerializer<T> =
|
||||
| {
|
||||
/**
|
||||
* Test function to determine if this serializer should be used for a value
|
||||
*/
|
||||
test(val: unknown): val is T;
|
||||
/**
|
||||
* Serialize function to convert the value to a string.
|
||||
*/
|
||||
serialize: (val: T) => string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Test function to determine if this serializer should be used for a value
|
||||
*/
|
||||
test: (val: unknown) => boolean;
|
||||
/**
|
||||
* Serialize function to convert the value to a string.
|
||||
*/
|
||||
serialize: (val: T) => string;
|
||||
}
|
||||
| {
|
||||
test: (val: unknown) => boolean;
|
||||
/**
|
||||
* @deprecated Pass `serialize` instead of `print`.
|
||||
*/
|
||||
print: (val: T) => string;
|
||||
};
|
||||
|
||||
/**
|
||||
* You can extend this interface with declaration merging, in order to add type support for custom matchers.
|
||||
* @template T Type of the actual value
|
||||
|
||||
226
src/bun.js/bindings/SnapshotSerializers.cpp
Normal file
226
src/bun.js/bindings/SnapshotSerializers.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
#include "root.h"
|
||||
#include "SnapshotSerializers.h"
|
||||
|
||||
#include <JavaScriptCore/JSArray.h>
|
||||
#include <JavaScriptCore/JSCJSValueInlines.h>
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include <JavaScriptCore/JSCInlines.h>
|
||||
#include <JavaScriptCore/Exception.h>
|
||||
#include <JavaScriptCore/JSFunction.h>
|
||||
#include "ErrorCode.h"
|
||||
#include "WebCoreJSBuiltins.h"
|
||||
|
||||
namespace Bun {
|
||||
|
||||
using namespace JSC;
|
||||
using namespace WebCore;
|
||||
|
||||
const ClassInfo SnapshotSerializers::s_info = { "SnapshotSerializers"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(SnapshotSerializers) };
|
||||
|
||||
SnapshotSerializers::SnapshotSerializers(VM& vm, Structure* structure)
|
||||
: Base(vm, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void SnapshotSerializers::finishCreation(VM& vm)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
}
|
||||
|
||||
JSArray* SnapshotSerializers::getTestCallbacks(JSGlobalObject* globalObject) const
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
JSArray* val = m_testCallbacks.get();
|
||||
if (!val) {
|
||||
val = JSC::constructEmptyArray(globalObject, nullptr, 0);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
m_testCallbacks.set(vm, this, val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
JSArray* SnapshotSerializers::getSerializeCallbacks(JSGlobalObject* globalObject) const
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
JSArray* val = m_serializeCallbacks.get();
|
||||
if (!val) {
|
||||
val = JSC::constructEmptyArray(globalObject, nullptr, 0);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
m_serializeCallbacks.set(vm, this, val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
template<typename Visitor>
|
||||
void SnapshotSerializers::visitChildrenImpl(JSCell* cell, Visitor& visitor)
|
||||
{
|
||||
SnapshotSerializers* thisObject = jsCast<SnapshotSerializers*>(cell);
|
||||
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
|
||||
Base::visitChildren(thisObject, visitor);
|
||||
|
||||
visitor.append(thisObject->m_testCallbacks);
|
||||
visitor.append(thisObject->m_serializeCallbacks);
|
||||
}
|
||||
|
||||
DEFINE_VISIT_CHILDREN(SnapshotSerializers);
|
||||
|
||||
SnapshotSerializers* SnapshotSerializers::create(VM& vm, Structure* structure)
|
||||
{
|
||||
SnapshotSerializers* serializers = new (NotNull, allocateCell<SnapshotSerializers>(vm)) SnapshotSerializers(vm, structure);
|
||||
serializers->finishCreation(vm);
|
||||
return serializers;
|
||||
}
|
||||
|
||||
void SnapshotSerializers::addSerializer(JSGlobalObject* globalObject, JSValue testCallback, JSValue serializeCallback)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
// Check for re-entrancy
|
||||
if (m_isExecuting) {
|
||||
throwTypeError(globalObject, scope, "Cannot add snapshot serializer from within a test or serialize callback"_s);
|
||||
RELEASE_AND_RETURN(scope, );
|
||||
}
|
||||
|
||||
// Validate that both callbacks are callable
|
||||
if (!testCallback.isCallable()) {
|
||||
throwTypeError(globalObject, scope, "Snapshot serializer test callback must be a function"_s);
|
||||
RELEASE_AND_RETURN(scope, );
|
||||
}
|
||||
|
||||
if (!serializeCallback.isCallable()) {
|
||||
throwTypeError(globalObject, scope, "Snapshot serializer serialize callback must be a function"_s);
|
||||
RELEASE_AND_RETURN(scope, );
|
||||
}
|
||||
|
||||
// Get the arrays (lazily initialized)
|
||||
JSArray* testCallbacks = getTestCallbacks(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
JSArray* serializeCallbacks = getSerializeCallbacks(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
|
||||
// Add to the end of the arrays (most recent last, we'll iterate in reverse)
|
||||
testCallbacks->push(globalObject, testCallback);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
|
||||
serializeCallbacks->push(globalObject, serializeCallback);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
}
|
||||
|
||||
JSValue SnapshotSerializers::serialize(JSGlobalObject* globalObject, JSValue value)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
// If no serializers are registered, return undefined
|
||||
if (!m_testCallbacks.get() || m_testCallbacks.get()->length() == 0) {
|
||||
return jsUndefined();
|
||||
}
|
||||
|
||||
// Check for re-entrancy
|
||||
if (m_isExecuting) {
|
||||
throwTypeError(globalObject, scope, "Cannot serialize from within a test or serialize callback"_s);
|
||||
RELEASE_AND_RETURN(scope, {});
|
||||
}
|
||||
|
||||
// RAII guard to manage m_isExecuting flag
|
||||
class ExecutionGuard {
|
||||
public:
|
||||
ExecutionGuard(bool& flag)
|
||||
: m_flag(flag)
|
||||
{
|
||||
m_flag = true;
|
||||
}
|
||||
~ExecutionGuard() { m_flag = false; }
|
||||
|
||||
private:
|
||||
bool& m_flag;
|
||||
};
|
||||
ExecutionGuard guard(m_isExecuting);
|
||||
|
||||
JSArray* testCallbacks = getTestCallbacks(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
JSArray* serializeCallbacks = getSerializeCallbacks(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
// Use JavaScript builtin for iteration to avoid deoptimization at boundaries
|
||||
// Get the cached function from the global object
|
||||
JSFunction* serializeBuiltin = jsCast<Zig::GlobalObject*>(globalObject)->snapshotSerializersSerializeFunction();
|
||||
|
||||
MarkedArgumentBuffer args;
|
||||
args.append(testCallbacks);
|
||||
args.append(serializeCallbacks);
|
||||
args.append(value);
|
||||
ASSERT(!args.hasOverflowed());
|
||||
|
||||
JSValue result = call(globalObject, serializeBuiltin, args, "snapshotSerializersSerialize"_s);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
using namespace Bun;
|
||||
using namespace JSC;
|
||||
|
||||
// Zig-exported functions
|
||||
|
||||
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__create(Zig::GlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto* structure = globalObject->SnapshotSerializersStructure();
|
||||
auto* serializers = SnapshotSerializers::create(vm, structure);
|
||||
return JSValue::encode(serializers);
|
||||
}
|
||||
|
||||
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__add(
|
||||
Zig::GlobalObject* globalObject,
|
||||
JSC::EncodedJSValue encodedSerializers,
|
||||
JSC::EncodedJSValue encodedTestCallback,
|
||||
JSC::EncodedJSValue encodedSerializeCallback)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSValue serializersValue = JSValue::decode(encodedSerializers);
|
||||
SnapshotSerializers* serializers = jsDynamicCast<SnapshotSerializers*>(serializersValue);
|
||||
|
||||
if (!serializers) {
|
||||
throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s);
|
||||
RELEASE_AND_RETURN(scope, {});
|
||||
}
|
||||
|
||||
JSValue testCallback = JSValue::decode(encodedTestCallback);
|
||||
JSValue serializeCallback = JSValue::decode(encodedSerializeCallback);
|
||||
|
||||
serializers->addSerializer(globalObject, testCallback, serializeCallback);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__serialize(
|
||||
Zig::GlobalObject* globalObject,
|
||||
JSC::EncodedJSValue encodedSerializers,
|
||||
JSC::EncodedJSValue encodedValue)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSValue serializersValue = JSValue::decode(encodedSerializers);
|
||||
SnapshotSerializers* serializers = jsDynamicCast<SnapshotSerializers*>(serializersValue);
|
||||
|
||||
if (!serializers) {
|
||||
throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s);
|
||||
RELEASE_AND_RETURN(scope, {});
|
||||
}
|
||||
|
||||
JSValue value = JSValue::decode(encodedValue);
|
||||
JSValue result = serializers->serialize(globalObject, value);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
return JSValue::encode(result);
|
||||
}
|
||||
64
src/bun.js/bindings/SnapshotSerializers.h
Normal file
64
src/bun.js/bindings/SnapshotSerializers.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include <JavaScriptCore/JSDestructibleObject.h>
|
||||
#include <JavaScriptCore/JSArray.h>
|
||||
#include <JavaScriptCore/WriteBarrier.h>
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
namespace Bun {
|
||||
|
||||
class SnapshotSerializers final : public JSC::JSDestructibleObject {
|
||||
public:
|
||||
using Base = JSC::JSDestructibleObject;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static SnapshotSerializers* create(JSC::VM& vm, JSC::Structure* structure);
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
}
|
||||
|
||||
template<typename MyClassT, JSC::SubspaceAccess mode>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
|
||||
return nullptr;
|
||||
return WebCore::subspaceForImpl<MyClassT, WebCore::UseCustomHeapCellType::No>(
|
||||
vm,
|
||||
[](auto& spaces) { return spaces.m_clientSubspaceForSnapshotSerializers.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForSnapshotSerializers = std::forward<decltype(space)>(space); },
|
||||
[](auto& spaces) { return spaces.m_subspaceForSnapshotSerializers.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_subspaceForSnapshotSerializers = std::forward<decltype(space)>(space); });
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
DECLARE_VISIT_CHILDREN;
|
||||
|
||||
// Add a new snapshot serializer
|
||||
// Throws TypeError if callbacks are not callable or if called re-entrantly
|
||||
void addSerializer(JSC::JSGlobalObject* globalObject, JSC::JSValue testCallback, JSC::JSValue serializeCallback);
|
||||
|
||||
// Test a value and serialize if a matching serializer is found
|
||||
// Returns the serialized string or null
|
||||
JSC::JSValue serialize(JSC::JSGlobalObject* globalObject, JSC::JSValue value);
|
||||
|
||||
private:
|
||||
SnapshotSerializers(JSC::VM& vm, JSC::Structure* structure);
|
||||
|
||||
void finishCreation(JSC::VM& vm);
|
||||
|
||||
// Lazy getters for arrays (following JSMockFunction pattern)
|
||||
JSC::JSArray* getTestCallbacks(JSC::JSGlobalObject* globalObject) const;
|
||||
JSC::JSArray* getSerializeCallbacks(JSC::JSGlobalObject* globalObject) const;
|
||||
|
||||
// Arrays store serializers with most recent last; iterated in reverse order
|
||||
mutable JSC::WriteBarrier<JSC::JSArray> m_testCallbacks;
|
||||
mutable JSC::WriteBarrier<JSC::JSArray> m_serializeCallbacks;
|
||||
|
||||
// Re-entrancy guard
|
||||
bool m_isExecuting { false };
|
||||
};
|
||||
|
||||
} // namespace Bun
|
||||
@@ -124,6 +124,7 @@
|
||||
#include "JSSink.h"
|
||||
#include "JSSocketAddressDTO.h"
|
||||
#include "JSSQLStatement.h"
|
||||
#include "SnapshotSerializers.h"
|
||||
#include "JSStringDecoder.h"
|
||||
#include "JSTextEncoder.h"
|
||||
#include "JSTextEncoderStream.h"
|
||||
@@ -2288,6 +2289,10 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(JSC::JSFunction::create(init.vm, init.owner, WebCore::ipcSerializeCodeGenerator(init.vm), init.owner));
|
||||
});
|
||||
|
||||
m_snapshotSerializersSerializeFunction.initLater([](const LazyProperty<JSC::JSGlobalObject, JSC::JSFunction>::Initializer& init) {
|
||||
init.set(JSC::JSFunction::create(init.vm, init.owner, WebCore::snapshotSerializersSerializeCodeGenerator(init.vm), init.owner));
|
||||
});
|
||||
|
||||
m_JSFileSinkClassStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::FileSink);
|
||||
@@ -2366,6 +2371,12 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.setConstructor(constructor);
|
||||
});
|
||||
|
||||
m_SnapshotSerializersStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
auto* structure = Bun::SnapshotSerializers::createStructure(init.vm, init.global, jsNull());
|
||||
init.setStructure(structure);
|
||||
});
|
||||
|
||||
m_JSBufferListClassStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
auto* prototype = JSBufferListPrototype::create(
|
||||
|
||||
@@ -201,6 +201,7 @@ public:
|
||||
WebCore::JSBuiltinInternalFunctions& builtinInternalFunctions() { return m_builtinInternalFunctions; }
|
||||
JSC::Structure* FFIFunctionStructure() const { return m_JSFFIFunctionStructure.getInitializedOnMainThread(this); }
|
||||
JSC::Structure* NapiClassStructure() const { return m_NapiClassStructure.getInitializedOnMainThread(this); }
|
||||
JSC::Structure* SnapshotSerializersStructure() const { return m_SnapshotSerializersStructure.getInitializedOnMainThread(this); }
|
||||
|
||||
JSC::Structure* FileSinkStructure() const { return m_JSFileSinkClassStructure.getInitializedOnMainThread(this); }
|
||||
JSC::JSObject* FileSink() const { return m_JSFileSinkClassStructure.constructorInitializedOnMainThread(this); }
|
||||
@@ -277,6 +278,8 @@ public:
|
||||
|
||||
JSC::JSFunction* wasmStreamingConsumeStreamFunction() const { return m_wasmStreamingConsumeStreamFunction.getInitializedOnMainThread(this); }
|
||||
|
||||
JSC::JSFunction* snapshotSerializersSerializeFunction() const { return m_snapshotSerializersSerializeFunction.getInitializedOnMainThread(this); }
|
||||
|
||||
JSObject* requireFunctionUnbound() const { return m_requireFunctionUnbound.getInitializedOnMainThread(this); }
|
||||
JSObject* requireResolveFunctionUnbound() const { return m_requireResolveFunctionUnbound.getInitializedOnMainThread(this); }
|
||||
Bun::InternalModuleRegistry* internalModuleRegistry() const { return m_internalModuleRegistry.getInitializedOnMainThread(this); }
|
||||
@@ -528,6 +531,7 @@ public:
|
||||
V(private, LazyClassStructure, m_JSBufferListClassStructure) \
|
||||
V(private, LazyClassStructure, m_JSFFIFunctionStructure) \
|
||||
V(private, LazyClassStructure, m_JSFileSinkClassStructure) \
|
||||
V(private, LazyClassStructure, m_SnapshotSerializersStructure) \
|
||||
V(private, LazyClassStructure, m_JSHTTPResponseSinkClassStructure) \
|
||||
V(private, LazyClassStructure, m_JSHTTPSResponseSinkClassStructure) \
|
||||
V(private, LazyClassStructure, m_JSNetworkSinkClassStructure) \
|
||||
@@ -631,7 +635,8 @@ public:
|
||||
V(public, LazyPropertyOfGlobalObject<Symbol>, m_nodeVMDontContextify) \
|
||||
V(public, LazyPropertyOfGlobalObject<Symbol>, m_nodeVMUseMainContextDefaultLoader) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_ipcSerializeFunction) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_ipcParseHandleFunction)
|
||||
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_ipcParseHandleFunction) \
|
||||
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_snapshotSerializersSerializeFunction)
|
||||
|
||||
#define DECLARE_GLOBALOBJECT_GC_MEMBER(visibility, T, name) \
|
||||
visibility: \
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSMockFunction;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForAsyncContextFrame;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMockWithImplementationCleanupData;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSnapshotSerializers;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForProcessObject;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForInternalModuleRegistry;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForErrorCodeCache;
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSMockFunction;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForAsyncContextFrame;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForMockWithImplementationCleanupData;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForSnapshotSerializers;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForProcessObject;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForInternalModuleRegistry;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForErrorCodeCache;
|
||||
|
||||
22
src/bun.js/test/SnapshotSerializerOptions.bindv2.ts
Normal file
22
src/bun.js/test/SnapshotSerializerOptions.bindv2.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as b from "bindgenv2";
|
||||
|
||||
export const SnapshotSerializerOptions = b.dictionary(
|
||||
{
|
||||
name: "SnapshotSerializerOptions",
|
||||
generateConversionFunction: true,
|
||||
},
|
||||
{
|
||||
test: {
|
||||
type: b.RawAny,
|
||||
internalName: "test_fn",
|
||||
},
|
||||
serialize: {
|
||||
type: b.RawAny,
|
||||
internalName: "serialize_fn",
|
||||
},
|
||||
print: {
|
||||
type: b.RawAny,
|
||||
internalName: "print_fn",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -710,7 +710,10 @@ pub const Expect = struct {
|
||||
|
||||
var pretty_value = std.Io.Writer.Allocating.init(default_allocator);
|
||||
defer pretty_value.deinit();
|
||||
try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value.writer, fn_name);
|
||||
this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value.writer, fn_name) catch |err| return switch (err) {
|
||||
error.WriteFailed => error.OutOfMemory,
|
||||
else => |e| e,
|
||||
};
|
||||
|
||||
var start_indent: ?[]const u8 = null;
|
||||
var end_indent: ?[]const u8 = null;
|
||||
@@ -795,7 +798,7 @@ pub const Expect = struct {
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
pub fn matchAndFmtSnapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, pretty_value: *std.Io.Writer, comptime fn_name: []const u8) bun.JSError!void {
|
||||
pub fn matchAndFmtSnapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, pretty_value: *std.Io.Writer, comptime fn_name: []const u8) (bun.JSError || std.Io.Writer.Error)!void {
|
||||
if (property_matchers) |_prop_matchers| {
|
||||
if (!value.isObject()) {
|
||||
const signature = comptime getSignature(fn_name, "<green>properties<r><d>, <r>hint", false);
|
||||
@@ -816,16 +819,15 @@ pub const Expect = struct {
|
||||
}
|
||||
}
|
||||
|
||||
value.jestSnapshotPrettyFormat(pretty_value, globalThis) catch {
|
||||
var formatter = jsc.ConsoleObject.Formatter{ .globalThis = globalThis };
|
||||
defer formatter.deinit();
|
||||
return globalThis.throw("Failed to pretty format value: {f}", .{value.toFmt(&formatter)});
|
||||
};
|
||||
try value.jestSnapshotPrettyFormat(pretty_value, globalThis);
|
||||
}
|
||||
pub fn snapshot(this: *Expect, globalThis: *JSGlobalObject, value: JSValue, property_matchers: ?JSValue, hint: []const u8, comptime fn_name: []const u8) bun.JSError!JSValue {
|
||||
var pretty_value = std.Io.Writer.Allocating.init(default_allocator);
|
||||
defer pretty_value.deinit();
|
||||
try this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value.writer, fn_name);
|
||||
this.matchAndFmtSnapshot(globalThis, value, property_matchers, &pretty_value.writer, fn_name) catch |err| return switch (err) {
|
||||
error.WriteFailed => error.OutOfMemory,
|
||||
else => |e| e,
|
||||
};
|
||||
|
||||
const existing_value = Jest.runner.?.snapshots.getOrPut(this, pretty_value.written(), hint) catch |err| {
|
||||
var buntest_strong = this.bunTest() orelse return globalThis.throw("Snapshot matchers cannot be used outside of a test", .{});
|
||||
@@ -968,6 +970,50 @@ pub const Expect = struct {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
/// Implements `expect.addSnapshotSerializer({ test, serialize })`
|
||||
pub fn addSnapshotSerializer(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
const arg = callFrame.argumentsAsArray(1)[0];
|
||||
|
||||
const runner = Jest.runner orelse {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nSnapshot serializers can only be added during test execution\n", .{});
|
||||
};
|
||||
|
||||
// Parse options using bindv2
|
||||
const options = try SnapshotSerializerOptions.fromJS(globalThis, arg);
|
||||
|
||||
// Validate test function
|
||||
if (!options.test_fn.jsType().isFunction()) {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected 'test' to be a function\n", .{});
|
||||
}
|
||||
|
||||
// Get serialize or print function
|
||||
var serialize_fn_value = options.serialize_fn;
|
||||
if (serialize_fn_value.isUndefinedOrNull()) {
|
||||
serialize_fn_value = options.print_fn;
|
||||
}
|
||||
|
||||
if (serialize_fn_value.isUndefinedOrNull()) {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected serializer object to have a 'serialize' or 'print' function\n", .{});
|
||||
}
|
||||
|
||||
if (!serialize_fn_value.jsType().isFunction()) {
|
||||
return globalThis.throwPretty("<d>expect.<r>addSnapshotSerializer<d>(<r>serializer<d>)<r>\n\nExpected 'serialize' or 'print' to be a function\n", .{});
|
||||
}
|
||||
|
||||
// Get or create the SnapshotSerializers object
|
||||
const serializers = runner.snapshots.serializers.get() orelse blk: {
|
||||
// Create a new SnapshotSerializers object
|
||||
const new_serializers = try bun.cpp.SnapshotSerializers__create(globalThis);
|
||||
runner.snapshots.serializers.set(globalThis, new_serializers);
|
||||
break :blk new_serializers;
|
||||
};
|
||||
|
||||
// Add the serializer
|
||||
_ = try bun.cpp.SnapshotSerializers__add(globalThis, serializers, options.test_fn, serialize_fn_value);
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
const CustomMatcherParamsFormatter = struct {
|
||||
colors: bool,
|
||||
globalThis: *JSGlobalObject,
|
||||
@@ -1172,8 +1218,6 @@ pub const Expect = struct {
|
||||
return thisValue;
|
||||
}
|
||||
|
||||
pub const addSnapshotSerializer = notImplementedStaticFn;
|
||||
|
||||
pub fn hasAssertions(globalThis: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
_ = callFrame;
|
||||
defer globalThis.bunVM().autoGarbageCollect();
|
||||
@@ -2249,8 +2293,12 @@ test "fuzz Expect.trimLeadingWhitespaceForInlineSnapshot" {
|
||||
|
||||
const string = []const u8;
|
||||
|
||||
pub const Jest = jest.Jest;
|
||||
|
||||
const bindgen_generated = @import("bindgen_generated");
|
||||
const std = @import("std");
|
||||
const DiffFormatter = @import("./diff_format.zig").DiffFormatter;
|
||||
const SnapshotSerializerOptions = bindgen_generated.snapshot_serializer_options.SnapshotSerializerOptions;
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
@@ -2269,5 +2317,4 @@ const ZigString = jsc.ZigString;
|
||||
|
||||
const jest = bun.jsc.Jest;
|
||||
const DescribeScope = jest.DescribeScope;
|
||||
const Jest = jest.Jest;
|
||||
const TestRunner = jest.TestRunner;
|
||||
|
||||
@@ -869,12 +869,32 @@ pub const JestPrettyFormat = struct {
|
||||
) bun.JSError!void {
|
||||
if (this.failed)
|
||||
return;
|
||||
|
||||
var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length };
|
||||
defer {
|
||||
if (writer.failed) {
|
||||
this.failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try user-defined snapshot serializers first
|
||||
if (expect.Jest.runner) |runner| {
|
||||
if (runner.snapshots.serializers.get()) |serializers| {
|
||||
const result = try bun.cpp.SnapshotSerializers__serialize(this.globalThis, serializers, value);
|
||||
if (!result.isUndefinedOrNull()) {
|
||||
if (bun.Environment.ci_assert) bun.assert(result.isString()); // should have thrown in SnapshotSerializers__serialize()
|
||||
|
||||
var str = ZigString.Empty;
|
||||
try result.toZigString(&str, this.globalThis);
|
||||
|
||||
this.addForNewLine(str.len);
|
||||
writer.writeString(str);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime Format.canHaveCircularReferences()) {
|
||||
if (this.map_node == null) {
|
||||
this.map_node = Visited.Pool.get(default_allocator);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const Snapshots = struct {
|
||||
snapshot_dir_path: ?string = null,
|
||||
inline_snapshots_to_write: *std.AutoArrayHashMap(TestRunner.File.ID, std.array_list.Managed(InlineSnapshotToWrite)),
|
||||
last_error_snapshot_name: ?[]const u8 = null,
|
||||
serializers: jsc.Strong.Optional = .empty,
|
||||
|
||||
pub const InlineSnapshotToWrite = struct {
|
||||
line: c_ulong,
|
||||
|
||||
31
src/js/builtins/SnapshotSerializers.ts
Normal file
31
src/js/builtins/SnapshotSerializers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Iterates through snapshot serializers and returns the serialized value or null
|
||||
// This is implemented in JavaScript to avoid deoptimization around JS/C++ boundaries
|
||||
export function serialize(
|
||||
testCallbacks: Function[],
|
||||
serializeCallbacks: Function[],
|
||||
value: unknown,
|
||||
): string | undefined {
|
||||
// Iterate through serializers in reverse order (most recent to least recent)
|
||||
for (let i = testCallbacks.length - 1; i >= 0; i--) {
|
||||
const testCallback = testCallbacks[i];
|
||||
|
||||
// Call the test function with the value
|
||||
if (!testCallback(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use this serializer
|
||||
const serializeCallback = serializeCallbacks[i];
|
||||
const result = serializeCallback(value);
|
||||
|
||||
// Error if the result is not a string
|
||||
if (typeof result !== "string") {
|
||||
throw new TypeError("Snapshot serializer serialize callback must return a string");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// No matching serializer found
|
||||
return undefined;
|
||||
}
|
||||
@@ -400,3 +400,64 @@ declare const setOfStrings: Set<string>;
|
||||
/** 1. **/ expect(setOfStrings).toBe(new Set()); // this is inferrable to Set<string> so this should pass
|
||||
/** 2. **/ expect(setOfStrings).toBe(new Set<string>()); // exact, so we are happy!
|
||||
/** 3. **/ expect(setOfStrings).toBe<Set<string>>(new Set()); // happy! We opted out of type safety for this expectation
|
||||
|
||||
class Point {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
) {}
|
||||
}
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Point,
|
||||
serialize: val => {
|
||||
expectType<Point>(val);
|
||||
return `Point(${val.x}, ${val.y})`;
|
||||
},
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => typeof val === "object" && val !== null && "qwerty" in val,
|
||||
serialize: val => {
|
||||
expectType<object & Record<"qwerty", unknown>>(val);
|
||||
return `{qwerty}`;
|
||||
},
|
||||
});
|
||||
|
||||
function returnsBoolean(): boolean {
|
||||
return false;
|
||||
}
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => returnsBoolean(),
|
||||
serialize: val => {
|
||||
expectType<unknown>(val);
|
||||
return `boolean`;
|
||||
},
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => false,
|
||||
print: val => {
|
||||
expectType<unknown>(val);
|
||||
return `false`;
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => true,
|
||||
// @ts-expect-error
|
||||
serialize: val => {
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
// @ts-expect-error
|
||||
test: () => 25,
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
expect.addSnapshotSerializer({
|
||||
test: () => false,
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
141
test/js/bun/test/snapshot-serializers.test.ts
Normal file
141
test/js/bun/test/snapshot-serializers.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
class Point {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
class Color {
|
||||
constructor(public name: string) {}
|
||||
}
|
||||
|
||||
class Size {
|
||||
constructor(
|
||||
public width: number,
|
||||
public height: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
class CustomSerializer {
|
||||
constructor(public opts: { test: (val: unknown) => boolean; serialize: (val: unknown) => string }) {}
|
||||
}
|
||||
|
||||
// Add serializers at the top level
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Point,
|
||||
serialize: val => `Point(${val.x}, ${val.y})`,
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Color,
|
||||
serialize: val => `Color[${val.name}]`,
|
||||
});
|
||||
|
||||
// Add a second Point serializer to test that most recent wins
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Point,
|
||||
serialize: val => `OVERRIDE: Point(${val.x}, ${val.y})`,
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => val instanceof Size,
|
||||
print: val => `Size{${val.width}x${val.height}}`,
|
||||
});
|
||||
|
||||
expect.addSnapshotSerializer({
|
||||
test: val => (val instanceof CustomSerializer ? val.opts.test(val) : false),
|
||||
serialize: val => val.opts.serialize(val),
|
||||
});
|
||||
|
||||
test("snapshot serializers work for custom formatting", () => {
|
||||
const color = new Color("red");
|
||||
expect(color).toMatchInlineSnapshot(`Color[red]`);
|
||||
});
|
||||
|
||||
test("most recently added serializer is used when multiple match", () => {
|
||||
// The second Point serializer should be used (most recent wins)
|
||||
const point = new Point(10, 20);
|
||||
expect(point).toMatchInlineSnapshot(`OVERRIDE: Point(10, 20)`);
|
||||
});
|
||||
|
||||
test("snapshot serializer with 'print' instead of 'serialize'", () => {
|
||||
const size = new Size(100, 200);
|
||||
expect(size).toMatchInlineSnapshot(`Size{100x200}`);
|
||||
});
|
||||
|
||||
test("snapshot serializers apply to object fields", () => {
|
||||
const obj = {
|
||||
color: new Color("blue"),
|
||||
size: new Size(640, 480),
|
||||
};
|
||||
expect(obj).toMatchInlineSnapshot(`
|
||||
{
|
||||
"color": Color[blue],
|
||||
"size": Size{640x480},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("test function throwing error propagates to expect()", () => {
|
||||
const obj = new CustomSerializer({
|
||||
test: () => {
|
||||
throw new Error("Test function error");
|
||||
},
|
||||
serialize: () => "test",
|
||||
});
|
||||
expect(() => {
|
||||
expect(obj).toMatchInlineSnapshot();
|
||||
}).toThrow("Test function error");
|
||||
});
|
||||
|
||||
test("serialize function throwing error propagates to expect()", () => {
|
||||
const obj = new CustomSerializer({
|
||||
test: () => true,
|
||||
serialize: () => {
|
||||
throw new Error("Serialize function error");
|
||||
},
|
||||
});
|
||||
expect(() => {
|
||||
expect(obj).toMatchInlineSnapshot();
|
||||
}).toThrow("Serialize function error");
|
||||
});
|
||||
|
||||
test("serialize function returning non-string throws error", () => {
|
||||
const obj = new CustomSerializer({
|
||||
test: () => true,
|
||||
serialize: () => 123 as unknown as string,
|
||||
});
|
||||
expect(() => {
|
||||
expect(obj).toMatchInlineSnapshot();
|
||||
}).toThrow("Snapshot serializer serialize callback must return a string");
|
||||
});
|
||||
|
||||
test("cannot add snapshot serializer from within a test callback", () => {
|
||||
expect(() => {
|
||||
expect(
|
||||
new CustomSerializer({
|
||||
test: () => {
|
||||
expect.addSnapshotSerializer({ test: () => true, serialize: () => "test" });
|
||||
return true;
|
||||
},
|
||||
serialize: () => "test",
|
||||
}),
|
||||
).toMatchInlineSnapshot();
|
||||
}).toThrow("Cannot add snapshot serializer from within a test or serialize callback");
|
||||
});
|
||||
|
||||
test("cannot add snapshot serializer from within a serialize callback", () => {
|
||||
expect(() => {
|
||||
expect(
|
||||
new CustomSerializer({
|
||||
test: () => true,
|
||||
serialize: () => {
|
||||
expect.addSnapshotSerializer({ test: () => true, serialize: () => "test" });
|
||||
return "test";
|
||||
},
|
||||
}),
|
||||
).toMatchInlineSnapshot();
|
||||
}).toThrow("Cannot add snapshot serializer from within a test or serialize callback");
|
||||
});
|
||||
Reference in New Issue
Block a user