aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bun.js/api/bun.zig2
-rw-r--r--src/bun.js/bindings/bindings.cpp96
-rw-r--r--src/bun.js/bindings/bindings.zig29
-rw-r--r--src/bun.js/bindings/exports.zig25
-rw-r--r--src/bun.js/bindings/headers.h3
-rw-r--r--src/bun.js/bindings/headers.zig1
-rw-r--r--src/bun.js/test/jest.zig979
-rw-r--r--src/deps/diffz/DiffMatchPatch.zig2247
-rw-r--r--src/output.zig5
-rw-r--r--test/bun.js/install/bun-link.test.ts4
-rw-r--r--test/bun.js/test-test.test.ts6
11 files changed, 3224 insertions, 173 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index da42e5feb..0e17d089f 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -246,6 +246,8 @@ pub fn inspect(
false,
false,
false,
+ false,
+ false,
);
buffered_writer.flush() catch {
return JSC.C.JSValueMakeUndefined(ctx);
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index bb01e4101..c093fad59 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -97,9 +97,7 @@ static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res)
auto& internalHeaders = headers->internalHeaders();
for (auto& value : internalHeaders.getSetCookieHeaders()) {
- res->writeHeader(std::string_view("set-cookie", 10), std::string_view(
- value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length()
- ));
+ res->writeHeader(std::string_view("set-cookie", 10), std::string_view(value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length()));
}
for (auto& header : internalHeaders.commonHeaders()) {
@@ -107,8 +105,7 @@ static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res)
auto& value = header.value;
res->writeHeader(
std::string_view(name.is8Bit() ? reinterpret_cast<const char*>(name.characters8()) : name.utf8().data(), name.length()),
- std::string_view(value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length())
- );
+ std::string_view(value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length()));
}
for (auto& header : internalHeaders.uncommonHeaders()) {
@@ -116,8 +113,7 @@ static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res)
auto& value = header.value;
res->writeHeader(
std::string_view(name.is8Bit() ? reinterpret_cast<const char*>(name.characters8()) : name.utf8().data(), name.length()),
- std::string_view(value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length())
- );
+ std::string_view(value.is8Bit() ? reinterpret_cast<const char*>(value.characters8()) : value.utf8().data(), value.length()));
}
}
@@ -718,12 +714,11 @@ WebCore__FetchHeaders* WebCore__FetchHeaders__createEmpty()
return new WebCore::FetchHeaders({ WebCore::FetchHeaders::Guard::None, {} });
}
void WebCore__FetchHeaders__append(WebCore__FetchHeaders* headers, const ZigString* arg1, const ZigString* arg2,
- JSC__JSGlobalObject* lexicalGlobalObject)
+ JSC__JSGlobalObject* lexicalGlobalObject)
{
auto throwScope = DECLARE_THROW_SCOPE(lexicalGlobalObject->vm());
WebCore::propagateException(*lexicalGlobalObject, throwScope,
- headers->append(Zig::toString(*arg1), Zig::toString(*arg2))
- );
+ headers->append(Zig::toString(*arg1), Zig::toString(*arg2)));
}
WebCore__FetchHeaders* WebCore__FetchHeaders__cast_(JSC__JSValue JSValue0, JSC__VM* vm)
{
@@ -752,8 +747,7 @@ WebCore__FetchHeaders* WebCore__FetchHeaders__createFromJS(JSC__JSGlobalObject*
// ExceptionOr<void>. So we need to check for the exception and, if set,
// translate it to JSValue and throw it.
WebCore::propagateException(*lexicalGlobalObject, throwScope,
- headers->fill(WTFMove(init.value()))
- );
+ headers->fill(WTFMove(init.value())));
}
return headers;
}
@@ -770,8 +764,7 @@ JSC__JSValue WebCore__FetchHeaders__clone(WebCore__FetchHeaders* headers, JSC__J
Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(arg1);
auto* clone = new WebCore::FetchHeaders({ WebCore::FetchHeaders::Guard::None, {} });
WebCore::propagateException(*arg1, throwScope,
- clone->fill(*headers)
- );
+ clone->fill(*headers));
return JSC::JSValue::encode(WebCore::toJSNewlyCreated(arg1, globalObject, WTFMove(clone)));
}
@@ -780,8 +773,7 @@ WebCore__FetchHeaders* WebCore__FetchHeaders__cloneThis(WebCore__FetchHeaders* h
auto throwScope = DECLARE_THROW_SCOPE(lexicalGlobalObject->vm());
auto* clone = new WebCore::FetchHeaders({ WebCore::FetchHeaders::Guard::None, {} });
WebCore::propagateException(*lexicalGlobalObject, throwScope,
- clone->fill(*headers)
- );
+ clone->fill(*headers));
return clone;
}
@@ -936,9 +928,8 @@ JSC__JSValue WebCore__FetchHeaders__createValue(JSC__JSGlobalObject* arg0, Strin
}
Ref<WebCore::FetchHeaders> headers = WebCore::FetchHeaders::create();
- WebCore::propagateException(*arg0, throwScope,
- headers->fill(WebCore::FetchHeaders::Init(WTFMove(pairs)))
- );
+ WebCore::propagateException(*arg0, throwScope,
+ headers->fill(WebCore::FetchHeaders::Init(WTFMove(pairs))));
pairs.releaseBuffer();
return JSC::JSValue::encode(WebCore::toJSNewlyCreated(arg0, reinterpret_cast<Zig::GlobalObject*>(arg0), WTFMove(headers)));
}
@@ -965,15 +956,13 @@ void WebCore__FetchHeaders__put_(WebCore__FetchHeaders* headers, const ZigString
{
auto throwScope = DECLARE_THROW_SCOPE(global->vm());
WebCore::propagateException(*global, throwScope,
- headers->set(Zig::toString(*arg1), Zig::toString(*arg2))
- );
+ headers->set(Zig::toString(*arg1), Zig::toString(*arg2)));
}
void WebCore__FetchHeaders__remove(WebCore__FetchHeaders* headers, const ZigString* arg1, JSC__JSGlobalObject* global)
{
auto throwScope = DECLARE_THROW_SCOPE(global->vm());
WebCore::propagateException(*global, throwScope,
- headers->remove(Zig::toString(*arg1))
- );
+ headers->remove(Zig::toString(*arg1)));
}
void WebCore__FetchHeaders__fastRemove_(WebCore__FetchHeaders* headers, unsigned char headerName)
@@ -3762,6 +3751,40 @@ restart:
}
}
+inline bool propertyCompare(const std::pair<String, JSValue>& a, const std::pair<String, JSValue>& b)
+{
+ return codePointCompare(a.first.impl(), b.first.impl()) < 0;
+}
+
+void JSC__JSValue__forEachPropertyOrdered(JSC__JSValue JSValue0, JSC__JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC__JSGlobalObject* arg0, void* ctx, ZigString* arg2, JSC__JSValue JSValue3, bool isSymbol))
+{
+ JSC::JSValue value = JSC::JSValue::decode(JSValue0);
+ JSC::JSObject* object = value.getObject();
+ if (!object)
+ return;
+
+ JSC::VM& vm = globalObject->vm();
+ auto scope = DECLARE_CATCH_SCOPE(vm);
+
+ JSC::PropertyNameArray properties(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
+ JSC::JSObject::getOwnPropertyNames(object, globalObject, properties, DontEnumPropertiesMode::Include);
+
+ Vector<std::pair<String, JSValue>> ordered_properties;
+ for (auto property : properties) {
+ JSValue propertyValue = object->getDirect(vm, property);
+ ordered_properties.append(std::pair<String, JSValue>(property.isSymbol() && !property.isPrivateName() ? property.impl() : property.string(), propertyValue));
+ }
+
+ std::sort(ordered_properties.begin(), ordered_properties.end(), propertyCompare);
+
+ for (auto item : ordered_properties) {
+ ZigString key = toZigString(item.first);
+ JSValue propertyValue = item.second;
+ JSC::EnsureStillAliveScope ensureStillAliveScope(propertyValue);
+ iter(globalObject, arg2, &key, JSC::JSValue::encode(propertyValue), propertyValue.isSymbol());
+ }
+}
+
extern "C" JSC__JSValue JSC__JSValue__createRopeString(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
{
return JSValue::encode(JSC::jsString(globalObject, JSC::JSValue::decode(JSValue0).toString(globalObject), JSC::JSValue::decode(JSValue1).toString(globalObject)));
@@ -3796,39 +3819,44 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC__JSGlobalObject* arg0
JSC::JSValue::decode(JSValue4));
}
-extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1) {
+extern "C" JSC__AbortSignal* JSC__AbortSignal__signal(JSC__AbortSignal* arg0, JSC__JSValue JSValue1)
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
abortSignal->signalAbort(JSC::JSValue::decode(JSValue1));
return arg0;
}
-extern "C" bool JSC__AbortSignal__aborted(JSC__AbortSignal* arg0) {
+extern "C" bool JSC__AbortSignal__aborted(JSC__AbortSignal* arg0)
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
return abortSignal->aborted();
}
-extern "C" JSC__JSValue JSC__AbortSignal__abortReason(JSC__AbortSignal* arg0) {
+extern "C" JSC__JSValue JSC__AbortSignal__abortReason(JSC__AbortSignal* arg0)
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
return JSC::JSValue::encode(abortSignal->reason().getValue());
}
-
-extern "C" JSC__AbortSignal* JSC__AbortSignal__ref(JSC__AbortSignal* arg0) {
+extern "C" JSC__AbortSignal* JSC__AbortSignal__ref(JSC__AbortSignal* arg0)
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
abortSignal->ref();
return arg0;
}
-extern "C" JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0) {
+extern "C" JSC__AbortSignal* JSC__AbortSignal__unref(JSC__AbortSignal* arg0)
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
abortSignal->deref();
return arg0;
}
-extern "C" JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* ctx, void (*callback)(void* ctx, JSC__JSValue reason)) {
+extern "C" JSC__AbortSignal* JSC__AbortSignal__addListener(JSC__AbortSignal* arg0, void* ctx, void (*callback)(void* ctx, JSC__JSValue reason))
+{
WebCore::AbortSignal* abortSignal = reinterpret_cast<WebCore::AbortSignal*>(arg0);
-
- if(abortSignal->aborted()){
+
+ if (abortSignal->aborted()) {
callback(ctx, JSC::JSValue::encode(abortSignal->reason().getValue()));
return arg0;
}
@@ -3859,7 +3887,7 @@ extern "C" JSC__JSValue JSC__AbortSignal__createAbortError(const ZigString* mess
error->putDirect(
vm, vm.propertyNames->name,
JSC::JSValue(JSC::jsOwnedString(vm, ABORT_ERROR_NAME)),
- 0);
+ 0);
if (code.len > 0) {
auto clientData = WebCore::clientData(vm);
@@ -3881,7 +3909,7 @@ extern "C" JSC__JSValue JSC__AbortSignal__createTimeoutError(const ZigString* me
error->putDirect(
vm, vm.propertyNames->name,
JSC::JSValue(JSC::jsOwnedString(vm, TIMEOUT_ERROR_NAME)),
- 0);
+ 0);
if (code.len > 0) {
auto clientData = WebCore::clientData(vm);
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 64b2b060d..800fd0af9 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -211,12 +211,12 @@ pub const ZigString = extern struct {
pub fn substring(this: ZigString, offset: usize, maxlen: usize) ZigString {
var len: usize = undefined;
- if(maxlen == 0){
+ if (maxlen == 0) {
len = this.len;
- }else {
+ } else {
len = @max(this.len, maxlen);
}
-
+
if (this.is16Bit()) {
return ZigString.from16Slice(this.utf16SliceAligned()[@min(this.len, offset)..len]);
}
@@ -2911,6 +2911,15 @@ pub const JSValue = enum(JSValueReprInt) {
cppFn("forEachProperty", .{ this, globalThis, ctx, callback });
}
+ pub fn forEachPropertyOrdered(
+ this: JSValue,
+ globalObject: *JSC.JSGlobalObject,
+ ctx: ?*anyopaque,
+ callback: PropertyIteratorFn,
+ ) void {
+ cppFn("forEachPropertyOrdered", .{ this, globalObject, ctx, callback });
+ }
+
pub fn coerce(this: JSValue, comptime T: type, globalThis: *JSC.JSGlobalObject) T {
return switch (T) {
ZigString => this.getZigString(globalThis),
@@ -3638,6 +3647,19 @@ pub const JSValue = enum(JSValueReprInt) {
return cppFn("strictDeepEquals", .{ this, other, global });
}
+ pub const DiffMethod = enum(u8) {
+ none,
+ character,
+ word,
+ line,
+ };
+
+ pub fn determineDiffMethod(this: JSValue, other: JSValue, global: *JSGlobalObject) DiffMethod {
+ if ((this.isString() and other.isString()) or (this.jsType() == .RegExpObject and other.jsType() == .RegExpObject) or (this.isBuffer(global) and other.isBuffer(global))) return .character;
+ if (this.isObject() and other.isObject()) return .line;
+ return .none;
+ }
+
pub fn asString(this: JSValue) *JSString {
return cppFn("asString", .{
this,
@@ -3852,6 +3874,7 @@ pub const JSValue = enum(JSValueReprInt) {
"fastGet_",
"forEach",
"forEachProperty",
+ "forEachPropertyOrdered",
"fromEntries",
"fromInt64NoTruncate",
"fromUInt64NoTruncate",
diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig
index 70aae7df3..aeced9a69 100644
--- a/src/bun.js/bindings/exports.zig
+++ b/src/bun.js/bindings/exports.zig
@@ -967,6 +967,8 @@ pub const ZigConsoleClient = struct {
enable_colors,
true,
true,
+ false,
+ false,
)
else if (message_type == .Log) {
_ = console.writer.write("\n") catch 0;
@@ -1015,6 +1017,8 @@ pub const ZigConsoleClient = struct {
enable_colors: bool,
add_newline: bool,
flush: bool,
+ order_properties: bool,
+ quote_strings: bool,
) void {
var fmt: ZigConsoleClient.Formatter = undefined;
defer {
@@ -1026,7 +1030,12 @@ pub const ZigConsoleClient = struct {
}
if (len == 1) {
- fmt = ZigConsoleClient.Formatter{ .remaining_values = &[_]JSValue{}, .globalThis = global };
+ fmt = ZigConsoleClient.Formatter{
+ .remaining_values = &[_]JSValue{},
+ .globalThis = global,
+ .ordered_properties = order_properties,
+ .quote_strings = quote_strings,
+ };
const tag = ZigConsoleClient.Formatter.Tag.get(vals[0], global);
var unbuffered_writer = if (comptime Writer != RawWriter)
@@ -1099,7 +1108,12 @@ pub const ZigConsoleClient = struct {
}
var this_value: JSValue = vals[0];
- fmt = ZigConsoleClient.Formatter{ .remaining_values = vals[0..len][1..], .globalThis = global };
+ fmt = ZigConsoleClient.Formatter{
+ .remaining_values = vals[0..len][1..],
+ .globalThis = global,
+ .ordered_properties = order_properties,
+ .quote_strings = quote_strings,
+ };
var tag: ZigConsoleClient.Formatter.Tag.Result = undefined;
var any = false;
@@ -1163,6 +1177,7 @@ pub const ZigConsoleClient = struct {
failed: bool = false,
estimated_line_length: usize = 0,
always_newline_scope: bool = false,
+ ordered_properties: bool = false,
pub fn goodTimeForANewLine(this: *@This()) bool {
if (this.estimated_line_length > 80) {
@@ -2541,7 +2556,11 @@ pub const ZigConsoleClient = struct {
.parent = value,
};
- value.forEachProperty(this.globalThis, &iter, Iterator.forEach);
+ if (this.ordered_properties) {
+ value.forEachPropertyOrdered(this.globalThis, &iter, Iterator.forEach);
+ } else {
+ value.forEachProperty(this.globalThis, &iter, Iterator.forEach);
+ }
if (iter.i == 0) {
if (value.isClass(this.globalThis) and !value.isCallable(this.globalThis.vm()))
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index a81695ac1..50c37dde6 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -1,5 +1,5 @@
// clang-format off
-//-- AUTOGENERATED FILE -- 1676656020
+//-- AUTOGENERATED FILE -- 1676945760
#pragma once
#include <stddef.h>
@@ -296,6 +296,7 @@ CPP_DECL bool JSC__JSValue__eqlValue(JSC__JSValue JSValue0, JSC__JSValue JSValue
CPP_DECL JSC__JSValue JSC__JSValue__fastGet_(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, unsigned char arg2);
CPP_DECL void JSC__JSValue__forEach(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void(* ArgFn3)(JSC__VM* arg0, JSC__JSGlobalObject* arg1, void* arg2, JSC__JSValue JSValue3)) __attribute__((nonnull (3)));
CPP_DECL void JSC__JSValue__forEachProperty(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void(* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, ZigString* arg2, JSC__JSValue JSValue3, bool arg4)) __attribute__((nonnull (3)));
+CPP_DECL void JSC__JSValue__forEachPropertyOrdered(JSC__JSValue JSValue0, JSC__JSGlobalObject* arg1, void* arg2, void(* ArgFn3)(JSC__JSGlobalObject* arg0, void* arg1, ZigString* arg2, JSC__JSValue JSValue3, bool arg4)) __attribute__((nonnull (3)));
CPP_DECL JSC__JSValue JSC__JSValue__fromEntries(JSC__JSGlobalObject* arg0, ZigString* arg1, ZigString* arg2, size_t arg3, bool arg4);
CPP_DECL JSC__JSValue JSC__JSValue__fromInt64NoTruncate(JSC__JSGlobalObject* arg0, int64_t arg1);
CPP_DECL JSC__JSValue JSC__JSValue__fromUInt64NoTruncate(JSC__JSGlobalObject* arg0, uint64_t arg1);
diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig
index ac936e280..12c72a717 100644
--- a/src/bun.js/bindings/headers.zig
+++ b/src/bun.js/bindings/headers.zig
@@ -209,6 +209,7 @@ pub extern fn JSC__JSValue__eqlValue(JSValue0: JSC__JSValue, JSValue1: JSC__JSVa
pub extern fn JSC__JSValue__fastGet_(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: u8) JSC__JSValue;
pub extern fn JSC__JSValue__forEach(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*bindings.VM, *bindings.JSGlobalObject, ?*anyopaque, JSC__JSValue) callconv(.C) void) void;
pub extern fn JSC__JSValue__forEachProperty(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*bindings.JSGlobalObject, ?*anyopaque, [*c]ZigString, JSC__JSValue, bool) callconv(.C) void) void;
+pub extern fn JSC__JSValue__forEachPropertyOrdered(JSValue0: JSC__JSValue, arg1: *bindings.JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*bindings.JSGlobalObject, ?*anyopaque, [*c]ZigString, JSC__JSValue, bool) callconv(.C) void) void;
pub extern fn JSC__JSValue__fromEntries(arg0: *bindings.JSGlobalObject, arg1: [*c]ZigString, arg2: [*c]ZigString, arg3: usize, arg4: bool) JSC__JSValue;
pub extern fn JSC__JSValue__fromInt64NoTruncate(arg0: *bindings.JSGlobalObject, arg1: i64) JSC__JSValue;
pub extern fn JSC__JSValue__fromUInt64NoTruncate(arg0: *bindings.JSGlobalObject, arg1: u64) JSC__JSValue;
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index 1ae0bd51a..90c595037 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -8,6 +8,8 @@ const HTTPClient = @import("bun").HTTP;
const NetworkThread = HTTPClient.NetworkThread;
const Environment = @import("../../env.zig");
+const DiffMatchPatch = @import("../../deps/diffz/DiffMatchPatch.zig");
+
const JSC = @import("bun").JSC;
const js = JSC.C;
@@ -61,6 +63,161 @@ fn notImplementedProp(
return null;
}
+pub const DiffFormatter = struct {
+ received: JSValue,
+ expected: JSValue,
+ globalObject: *JSC.JSGlobalObject,
+ not: bool = false,
+
+ pub fn format(this: DiffFormatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
+ var received_buf = MutableString.init(default_allocator, 0) catch unreachable;
+ var expected_buf = MutableString.init(default_allocator, 0) catch unreachable;
+ defer {
+ received_buf.deinit();
+ expected_buf.deinit();
+ }
+
+ {
+ var buffered_writer_ = bun.MutableString.BufferedWriter{ .context = &received_buf };
+ var buffered_writer = &buffered_writer_;
+
+ var buf_writer = buffered_writer.writer();
+ const Writer = @TypeOf(buf_writer);
+
+ JSC.ZigConsoleClient.format(
+ .Debug,
+ this.globalObject,
+ @ptrCast([*]const JSValue, &this.received),
+ 1,
+ Writer,
+ Writer,
+ buf_writer,
+ false,
+ false,
+ false,
+ true,
+ true,
+ );
+ buffered_writer.flush() catch unreachable;
+
+ buffered_writer_.context = &expected_buf;
+
+ JSC.ZigConsoleClient.format(
+ .Debug,
+ this.globalObject,
+ @ptrCast([*]const JSValue, &this.expected),
+ 1,
+ Writer,
+ Writer,
+ buf_writer,
+ false,
+ false,
+ false,
+ true,
+ true,
+ );
+ buffered_writer.flush() catch unreachable;
+ }
+
+ const received_slice = received_buf.toOwnedSliceLeaky();
+ const expected_slice = expected_buf.toOwnedSliceLeaky();
+
+ if (this.not) {
+ const not_fmt = "Expected: not <green>{s}<r>";
+ if (Output.enable_ansi_colors) {
+ try writer.print(Output.prettyFmt(not_fmt, true), .{expected_slice});
+ } else {
+ try writer.print(Output.prettyFmt(not_fmt, false), .{expected_slice});
+ }
+ return;
+ }
+
+ const equal_fmt = "<d>{s}<r>";
+ const delete_fmt = "<red>{s}<r>";
+ const insert_fmt = "<green>{s}<r>";
+
+ switch (this.received.determineDiffMethod(this.expected, this.globalObject)) {
+ .none => {
+ const fmt = "Expected: <green>{any}<r>\nReceived: <red>{any}<r>";
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = this.globalObject, .quote_strings = true };
+ if (Output.enable_ansi_colors) {
+ try writer.print(Output.prettyFmt(fmt, true), .{
+ this.expected.toFmt(this.globalObject, &formatter),
+ this.received.toFmt(this.globalObject, &formatter),
+ });
+ return;
+ }
+
+ try writer.print(Output.prettyFmt(fmt, true), .{
+ this.expected.toFmt(this.globalObject, &formatter),
+ this.received.toFmt(this.globalObject, &formatter),
+ });
+ return;
+ },
+ .character => {
+ var dmp = DiffMatchPatch.default;
+ dmp.diff_timeout = 200;
+ var diffs = try dmp.diff(default_allocator, received_slice, expected_slice, false);
+ defer diffs.deinit(default_allocator);
+
+ try writer.writeAll(Output.prettyFmt("Expected: ", true));
+ for (diffs.items) |df| {
+ switch (df.operation) {
+ .delete => continue,
+ .insert => try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text}),
+ .equal => try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}),
+ }
+ }
+
+ try writer.writeAll(Output.prettyFmt("\nReceived: ", true));
+ for (diffs.items) |df| {
+ switch (df.operation) {
+ .insert => continue,
+ .delete => try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text}),
+ .equal => try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text}),
+ }
+ }
+ return;
+ },
+ .line => {
+ var dmp = DiffMatchPatch.default;
+ dmp.diff_timeout = 200;
+ var diffs = try dmp.diffLines(default_allocator, received_slice, expected_slice);
+ defer diffs.deinit(default_allocator);
+
+ var insert_count: usize = 0;
+ var delete_count: usize = 0;
+
+ for (diffs.items) |df| {
+ switch (df.operation) {
+ .equal => {
+ try writer.print(Output.prettyFmt(equal_fmt, true), .{df.text});
+ },
+ .insert => {
+ for (df.text) |c| {
+ if (c == '\n') insert_count += 1;
+ }
+ try writer.print(Output.prettyFmt(insert_fmt, true), .{df.text});
+ },
+ .delete => {
+ for (df.text) |c| {
+ if (c == '\n') delete_count += 1;
+ }
+ try writer.print(Output.prettyFmt(delete_fmt, true), .{df.text});
+ },
+ }
+ }
+
+ try writer.print(Output.prettyFmt("\n\n<green>- Expected - {d}<r>\n", true), .{insert_count});
+ try writer.print(Output.prettyFmt("<red>+ Received + {d}<r>", true), .{delete_count});
+ return;
+ },
+ .word => {},
+ }
+ return;
+ }
+};
+
const ArrayIdentityContext = @import("../../identity_context.zig").ArrayIdentityContext;
pub const TestRunner = struct {
tests: TestRunner.Test.List = .{},
@@ -327,38 +484,89 @@ pub const Expect = struct {
}
active_test_expectation_counter.actual += 1;
- const left = arguments[0];
- left.ensureStillAlive();
- const right = Expect.capturedValueGetCached(thisValue) orelse {
+ const right = arguments[0];
+ right.ensureStillAlive();
+ const left = Expect.capturedValueGetCached(thisValue) orelse {
globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{});
return .zero;
};
- right.ensureStillAlive();
+ left.ensureStillAlive();
const not = this.op.contains(.not);
- var pass = left.isSameValue(right, globalObject);
+ var pass = right.isSameValue(left, globalObject);
if (comptime Environment.allow_assert) {
- std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, left.asObjectRef(), right.asObjectRef()));
+ std.debug.assert(pass == JSC.C.JSValueIsStrictEqual(globalObject, right.asObjectRef(), left.asObjectRef()));
}
if (not) pass = !pass;
if (pass) return thisValue;
// handle failure
- var lhs_fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
- var rhs_fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
- if (comptime Environment.allow_assert) {
- Output.prettyErrorln("\nJSType: {s}\nJSType: {s}\n\n", .{ @tagName(left.jsType()), @tagName(right.jsType()) });
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
+ if (not) {
+ const signature = comptime getSignature("toBe", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\nExpected: not <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)});
+ return .zero;
}
- if (not) {
- globalObject.throw("\n\tExpected: not {any}\n\tReceived: {any}", .{ left.toFmt(globalObject, &lhs_fmt), right.toFmt(globalObject, &rhs_fmt) });
- } else {
- globalObject.throw("\n\tExpected: {any}\n\tReceived: {any}", .{ left.toFmt(globalObject, &lhs_fmt), right.toFmt(globalObject, &rhs_fmt) });
+ const signature = comptime getSignature("toBe", "<green>expected<r>", false);
+ if (left.deepEquals(right, globalObject) or left.strictDeepEquals(right, globalObject)) {
+ const fmt = signature ++
+ "\n\n<d>If this test should pass, replace \"toBe\" with \"toEqual\" or \"toStrictEqual\"<r>" ++
+ "\n\nExpected: <green>{any}<r>\n" ++
+ "Received: serializes to the same string\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{right.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{right.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ if (right.isString() and left.isString()) {
+ const diff_format = DiffFormatter{
+ .expected = right,
+ .received = left,
+ .globalObject = globalObject,
+ .not = not,
+ };
+ const fmt = signature ++ "\n\n{any}\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format});
+ return .zero;
}
+
+ const fmt = signature ++ "\n\nExpected: <green>{any}<r>\nReceived: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ right.toFmt(globalObject, &formatter),
+ left.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{
+ right.toFmt(globalObject, &formatter),
+ left.toFmt(globalObject, &formatter),
+ });
return .zero;
}
+ pub fn getSignature(comptime matcher_name: string, comptime args: string, comptime not: bool) string {
+ const received = "<d>expect(<r><red>received<r><d>).<r>";
+ comptime if (not) {
+ return received ++ "not<d>.<r>" ++ matcher_name ++ "<d>(<r>" ++ args ++ "<d>)<r>";
+ };
+ return received ++ matcher_name ++ "<d>(<r>" ++ args ++ "<d>)<r>";
+ }
+
pub fn toHaveLength(
this: *Expect,
globalObject: *JSC.JSGlobalObject,
@@ -438,10 +646,27 @@ pub const Expect = struct {
// handle failure
if (not) {
- globalObject.throw("\n\tExpected: not {d}\n\tReceived: {d}", .{ expected_length, actual_length });
- } else {
- globalObject.throw("\n\tExpected: {d}\n\tReceived: {d}", .{ expected_length, actual_length });
+ const expected_line = "Expected length: not <green>{d}<r>\n";
+ const fmt = comptime getSignature("toHaveLength", "<green>expected<r>", true) ++ "\n\n" ++ expected_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_length});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_length});
+ return .zero;
+ }
+
+ const expected_line = "Expected length: <green>{d}<r>\n";
+ const received_line = "Received length: <red>{d}<r>\n";
+ const fmt = comptime getSignature("toHaveLength", "<green>expected<r>", false) ++ "\n\n" ++
+ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_length, actual_length });
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_length, actual_length });
return .zero;
}
@@ -501,12 +726,30 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
+ const expected_fmt = expected.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected to not contain \"{any}\"", .{expected.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected to contain \"{any}\"", .{expected.toFmt(globalObject, &fmt)});
+ const expected_line = "Expected to contain: not <green>{any}<r>\n";
+ const fmt = comptime getSignature("toContain", "<green>expected<r>", true) ++ "\n\n" ++ expected_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_fmt});
+ return .zero;
+ }
+
+ const expected_line = "Expected to contain: <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toContain", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
return .zero;
}
@@ -536,12 +779,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not truthy.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be truthy.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeTruthy", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeTruthy", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -564,12 +823,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not undefined.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be undefined.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeUndefined", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeUndefined", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -596,12 +871,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not NaN.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be NaN.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeNaN", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeNaN", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -623,12 +914,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not null.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be null.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeNull", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeNull", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -650,12 +957,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not defined.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be defined.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeDefined", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeDefined", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -680,12 +1003,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected \"{any}\" to be not falsy.", .{value.toFmt(globalObject, &fmt)});
- } else {
- globalObject.throw("Expected \"{any}\" to be falsy.", .{value.toFmt(globalObject, &fmt)});
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeFalsy", "", true) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
+ return .zero;
+ }
+
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeFalsy", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{value_fmt});
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{value_fmt});
return .zero;
}
@@ -722,12 +1061,26 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not };
+
if (not) {
- globalObject.throw("Expected values to not be equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected values to be equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) });
+ const signature = comptime getSignature("toEqual", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\n{any}\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter});
+ return .zero;
}
+
+ const signature = comptime getSignature("toEqual", "<green>expected<r>", false);
+ const fmt = signature ++ "\n\n{any}\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter});
return .zero;
}
@@ -764,12 +1117,26 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const diff_formatter = DiffFormatter{ .received = value, .expected = expected, .globalObject = globalObject, .not = not };
+
if (not) {
- globalObject.throw("Expected values to not be strictly equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected values to be strictly equal:\n\tExpected: {any}\n\tReceived: {any}", .{ expected.toFmt(globalObject, &fmt), value.toFmt(globalObject, &fmt) });
+ const signature = comptime getSignature("toStrictEqual", "<green>expected<r>", true);
+ const fmt = signature ++ "\n\n{any}\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter});
+ return .zero;
}
+
+ const signature = comptime getSignature("toStrictEqual", "<green>expected<r>", false);
+ const fmt = signature ++ "\n\n{any}\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_formatter});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_formatter});
return .zero;
}
@@ -794,8 +1161,8 @@ pub const Expect = struct {
const expected_property_path = arguments[0];
expected_property_path.ensureStillAlive();
- const expected_value: ?JSValue = if (arguments.len > 1) arguments[1] else null;
- if (expected_value) |ev| ev.ensureStillAlive();
+ const expected_property: ?JSValue = if (arguments.len > 1) arguments[1] else null;
+ if (expected_property) |ev| ev.ensureStillAlive();
const value = Expect.capturedValueGetCached(thisValue) orelse {
globalObject.throw("Internal consistency error: the expect(value) was garbage collected but it should not have been!", .{});
@@ -812,33 +1179,97 @@ pub const Expect = struct {
var path_string = ZigString.Empty;
expected_property_path.toZigString(&path_string, globalObject);
- const expected_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path);
+ const received_property = value.getIfPropertyExistsFromPath(globalObject, expected_property_path);
- var pass = !expected_property.isEmpty();
+ var pass = !received_property.isEmpty();
- if (pass and expected_value != null) {
- pass = expected_property.deepEquals(expected_value.?, globalObject);
+ if (pass and expected_property != null) {
+ pass = received_property.deepEquals(expected_property.?, globalObject);
}
if (not) pass = !pass;
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject, .quote_strings = true };
if (not) {
- if (!expected_property.isEmpty() and expected_value != null) {
- globalObject.throw("Expected property \"{any}\" to not be equal to: {any}", .{ expected_property.toFmt(globalObject, &fmt), expected_value.?.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected \"{any}\" to not have property: {any}", .{ value.toFmt(globalObject, &fmt), expected_property_path.toFmt(globalObject, &fmt) });
+ if (expected_property != null) {
+ const signature = comptime getSignature("toHaveProperty", "<green>path<r><d>, <r><green>value<r>", true);
+ if (!received_property.isEmpty()) {
+ const fmt = signature ++ "\n\nExpected path: <green>{any}<r>\n\nExpected value: not <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ expected_property.?.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ expected_property.?.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
}
- } else {
- if (!expected_property.isEmpty() and expected_value != null) {
- globalObject.throw("Expected property \"{any}\" to be equal to: {any}", .{ expected_property.toFmt(globalObject, &fmt), expected_value.?.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected \"{any}\" to have property: {any}", .{ value.toFmt(globalObject, &fmt), expected_property_path.toFmt(globalObject, &fmt) });
+
+ const signature = comptime getSignature("toHaveProperty", "<green>path<r>", true);
+ const fmt = signature ++ "\n\nExpected path: not <green>{any}<r>\n\nReceived value: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ received_property.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ received_property.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+
+ if (expected_property != null) {
+ const signature = comptime getSignature("toHaveProperty", "<green>path<r><d>, <r><green>value<r>", false);
+ if (!received_property.isEmpty()) {
+ // deep equal case
+ const fmt = signature ++ "\n\n{any}\n";
+ const diff_format = DiffFormatter{
+ .received = received_property,
+ .expected = expected_property.?,
+ .globalObject = globalObject,
+ };
+
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format});
+ return .zero;
}
+
+ const fmt = signature ++ "\n\nExpected path: <green>{any}<r>\n\nExpected value: <green>{any}<r>\n\n" ++
+ "Unable to find property\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ expected_property.?.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{
+ expected_property_path.toFmt(globalObject, &formatter),
+ expected_property.?.toFmt(globalObject, &formatter),
+ });
+ return .zero;
}
+ const signature = comptime getSignature("toHaveProperty", "<green>path<r>", false);
+ const fmt = signature ++ "\n\nExpected path: <green>{any}<r>\n\nUnable to find property\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_property_path.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_property_path.toFmt(globalObject, &formatter)});
return .zero;
}
@@ -896,12 +1327,32 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
+ const expected_fmt = other_value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected {any} to not be greater than {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected {any} to be greater than {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
+ const expected_line = "Expected: not \\> <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeGreaterThan", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ const expected_line = "Expected: \\> <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeGreaterThan", "<green>expected<r>", false) ++ "\n\n" ++
+ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
}
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
return .zero;
}
@@ -959,11 +1410,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
+ const expected_fmt = other_value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected {any} to not be greater than or equal to {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected {any} to be greater than or equal to {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
+ const expected_line = "Expected: not \\>= <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeGreaterThanOrEqual", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ const expected_line = "Expected: \\>= <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeGreaterThanOrEqual", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
}
return .zero;
}
@@ -1022,11 +1490,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
+ const expected_fmt = other_value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected {any} to not be less than {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected {any} to be less than {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
+ const expected_line = "Expected: not \\< <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeLessThan", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ const expected_line = "Expected: \\< <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeLessThan", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
}
return .zero;
}
@@ -1085,11 +1570,28 @@ pub const Expect = struct {
if (pass) return thisValue;
// handle failure
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const value_fmt = value.toFmt(globalObject, &formatter);
+ const expected_fmt = other_value.toFmt(globalObject, &formatter);
if (not) {
- globalObject.throw("Expected {any} to not be less than or equal to {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
- } else {
- globalObject.throw("Expected {any} to be less than or equal to {any}", .{ value.toFmt(globalObject, &fmt), other_value.toFmt(globalObject, &fmt) });
+ const expected_line = "Expected: not \\<= <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeLessThanOrEqual", "<green>expected<r>", true) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, value_fmt });
+ return .zero;
+ }
+
+ const expected_line = "Expected: \\<= <green>{any}<r>\n";
+ const received_line = "Received: <red>{any}<r>\n";
+ const fmt = comptime getSignature("toBeLessThanOrEqual", "<green>expected<r>", false) ++ "\n\n" ++ expected_line ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(comptime Output.prettyFmt(fmt, true), .{ expected_fmt, value_fmt });
+ return .zero;
}
return .zero;
}
@@ -1108,7 +1610,7 @@ pub const Expect = struct {
active_test_expectation_counter.actual += 1;
- const expected_value = if (arguments.len > 0) brk: {
+ const expected_value: JSValue = if (arguments.len > 0) brk: {
const value = arguments[0];
if (value.isEmptyOrUndefinedOrNull() or !value.isObject() and !value.isString()) {
var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
@@ -1160,59 +1662,286 @@ pub const Expect = struct {
};
const did_throw = result_ != null;
- const matched_expectation = did_throw == !not;
- if (!matched_expectation) {
- if (!not)
- globalObject.throw("Expected function to throw", .{})
- else {
- var fmt = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
- globalObject.throw("Expected function not to throw. Received:\n\t{any}", .{result_.?.toFmt(globalObject, &fmt)});
+ if (not) {
+ const signature = comptime getSignature("toThrow", "<green>expected<r>", true);
+
+ if (!did_throw) return thisValue;
+
+ const result: JSValue = result_.?;
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+
+ if (expected_value.isEmpty()) {
+ const signature_no_args = comptime getSignature("toThrow", "", true);
+ if (result.isError()) {
+ const name = result.getIfPropertyExistsImpl(globalObject, "name", 4);
+ const message = result.getIfPropertyExistsImpl(globalObject, "message", 7);
+ const fmt = signature_no_args ++ "\n\nError name: <red>{any}<r>\nError message: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ name.toFmt(globalObject, &formatter),
+ message.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{
+ name.toFmt(globalObject, &formatter),
+ message.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+
+ // non error thrown
+ const fmt = signature_no_args ++ "\n\nThrown value: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{result.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{result.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ if (expected_value.isString()) {
+ const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7);
+
+ // partial match (regex not supported)
+ {
+ var expected_string = ZigString.Empty;
+ var received_string = ZigString.Empty;
+ expected_value.toZigString(&expected_string, globalObject);
+ received_message.toZigString(&received_string, globalObject);
+ const expected_slice = expected_string.toSlice(default_allocator);
+ const received_slice = received_string.toSlice(default_allocator);
+ defer {
+ expected_slice.deinit();
+ received_slice.deinit();
+ }
+ if (!strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue;
+ }
+
+ const fmt = signature ++ "\n\nExpected substring: not <green>{any}<r>\nReceived message: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{
+ expected_value.toFmt(globalObject, &formatter),
+ received_message.toFmt(globalObject, &formatter),
+ });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{
+ expected_value.toFmt(globalObject, &formatter),
+ received_message.toFmt(globalObject, &formatter),
+ });
+ return .zero;
}
+ if (expected_value.get(globalObject, "message")) |expected_message| {
+ const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7);
+ // no partial match for this case
+ if (!expected_message.isSameValue(received_message, globalObject)) return thisValue;
+
+ const fmt = signature ++ "\n\nExpected message: not <green>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ if (!result.isInstanceOf(globalObject, expected_value)) return thisValue;
+
+ var expected_class = ZigString.Empty;
+ expected_value.getClassName(globalObject, &expected_class);
+ const received_message = result.getIfPropertyExistsImpl(globalObject, "message", 7);
+ const fmt = signature ++ "\n\nExpected constructor: not <green>{s}<r>\n\nReceived message: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_class, received_message.toFmt(globalObject, &formatter) });
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_class, received_message.toFmt(globalObject, &formatter) });
return .zero;
}
- // If you throw a string, it's treated as the message of an Error
- // If you are expected not to throw and you didn't throw, then you pass
- // If you are expected to throw a specific message and you throw a different one, then you fail.
- if (matched_expectation and (!expected_value.isCell() or not))
- return thisValue;
+ const signature = comptime getSignature("toThrow", "<green>expected<r>", false);
+ if (did_throw) {
+ if (expected_value.isEmpty()) return thisValue;
+
+ const result: JSValue = result_.?;
+ const _received_message = result.get(globalObject, "message");
+
+ if (expected_value.isString()) {
+ if (_received_message) |received_message| {
+ // partial match (regex not supported)
+ var expected_string = ZigString.Empty;
+ var received_string = ZigString.Empty;
+ expected_value.toZigString(&expected_string, globalObject);
+ received_message.toZigString(&received_string, globalObject);
+ const expected_slice = expected_string.toSlice(default_allocator);
+ const received_slice = received_string.toSlice(default_allocator);
+ defer {
+ expected_slice.deinit();
+ received_slice.deinit();
+ }
+ if (strings.contains(received_slice.slice(), expected_slice.slice())) return thisValue;
+ }
- const result = result_ orelse JSC.JSValue.jsUndefined();
+ // error: message from received error does not match expected string
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
- const expected_error = expected_value.toError();
+ if (_received_message) |received_message| {
+ const expected_value_fmt = expected_value.toFmt(globalObject, &formatter);
+ const received_message_fmt = received_message.toFmt(globalObject, &formatter);
+ const fmt = signature ++ "\n\n" ++ "Expected substring: <green>{any}<r>\nReceived message: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_value_fmt, received_message_fmt });
+ return .zero;
+ }
- if (expected_value.isString() or expected_error != null) {
- const expected = brk: {
- if (expected_value.isString()) break :brk expected_value;
- break :brk expected_error.?.get(globalObject, "message");
- };
- const actual: ?JSValue = if (result.isObject())
- result.get(globalObject, "message")
- else
- null;
- // TODO support partial match
- const pass = brk: {
- if (expected) |expected_message|
- if (actual) |actual_message|
- break :brk expected_message.isSameValue(actual_message, globalObject);
- break :brk false;
- };
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_value_fmt, received_message_fmt });
+ return .zero;
+ }
+
+ const expected_fmt = expected_value.toFmt(globalObject, &formatter);
+ const received_fmt = result.toFmt(globalObject, &formatter);
+ const fmt = signature ++ "\n\n" ++ "Expected substring: <green>{any}<r>\nReceived value: <red>{any}<r>";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
+
+ if (expected_value.get(globalObject, "message")) |expected_message| {
+ if (_received_message) |received_message| {
+ if (received_message.isSameValue(expected_message, globalObject)) return thisValue;
+ }
+
+ // error: message from received error does not match expected error message.
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+
+ if (_received_message) |received_message| {
+ const expected_fmt = expected_message.toFmt(globalObject, &formatter);
+ const received_fmt = received_message.toFmt(globalObject, &formatter);
+ const fmt = signature ++ "\n\nExpected message: <green>{any}<r>\nReceived message: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
- if (pass) return thisValue;
- globalObject.throw("\n\tExpected: {s}\n\tReceived: {s}", .{
- if (expected) |message| message.getZigString(globalObject) else ZigString.init("undefined"),
- if (actual) |message| message.getZigString(globalObject) else ZigString.init("undefined"),
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
+
+ const expected_fmt = expected_message.toFmt(globalObject, &formatter);
+ const received_fmt = result.toFmt(globalObject, &formatter);
+ const fmt = signature ++ "\n\nExpected message: <green>{any}<r>\nReceived value: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{ expected_fmt, received_fmt });
+ return .zero;
+ }
+
+ if (result.isInstanceOf(globalObject, expected_value)) return thisValue;
+
+ // error: received error not instance of received error constructor
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ var expected_class = ZigString.Empty;
+ var received_class = ZigString.Empty;
+ expected_value.getClassName(globalObject, &expected_class);
+ result.getClassName(globalObject, &received_class);
+ const fmt = signature ++ "\n\nExpected constructor: <green>{s}<r>\nReceived constructor: <red>{s}<r>\n\n";
+
+ if (_received_message) |received_message| {
+ const message_fmt = fmt ++ "Received message: <red>{any}<r>\n";
+ const received_message_fmt = received_message.toFmt(globalObject, &formatter);
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(message_fmt, true), .{
+ expected_class,
+ received_class,
+ received_message_fmt,
+ });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(message_fmt, false), .{
+ expected_class,
+ received_class,
+ received_message_fmt,
+ });
+ return .zero;
+ }
+
+ const received_fmt = result.toFmt(globalObject, &formatter);
+ const value_fmt = fmt ++ "Received value: <red>{any}<r>\n";
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(value_fmt, true), .{
+ expected_class,
+ received_class,
+ received_fmt,
+ });
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(value_fmt, false), .{
+ expected_class,
+ received_class,
+ received_fmt,
});
return .zero;
}
- if (result.isInstanceOf(globalObject, expected_value)) return thisValue;
- globalObject.throw("\n\tExpected type: {s}\n\tReceived type: {s}", .{
- expected_value.getName(globalObject),
- if (result.get(globalObject, "name")) |name| name.getZigString(globalObject) else ZigString.init("<Unknown>"),
- });
+ // did not throw
+ var formatter = JSC.ZigConsoleClient.Formatter{ .globalThis = globalObject };
+ const received_line = "Received function did not throw\n";
+
+ if (expected_value.isEmpty()) {
+ const fmt = comptime getSignature("toThrow", "", false) ++ "\n\n" ++ received_line;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, false), .{});
+ return .zero;
+ }
+
+ if (expected_value.isString()) {
+ const expected_fmt = "\n\nExpected substring: <green>{any}<r>\n\n" ++ received_line;
+ const fmt = signature ++ expected_fmt;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_value.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ if (expected_value.get(globalObject, "message")) |expected_message| {
+ const expected_fmt = "\n\nExpected message: <green>{any}<r>\n\n" ++ received_line;
+ const fmt = signature ++ expected_fmt;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_message.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ globalObject.throw(Output.prettyFmt(fmt, false), .{expected_message.toFmt(globalObject, &formatter)});
+ return .zero;
+ }
+
+ const expected_fmt = "\n\nExpected constructor: <green>{s}<r>\n\n" ++ received_line;
+ var expected_class = ZigString.Empty;
+ expected_value.getClassName(globalObject, &expected_class);
+ const fmt = signature ++ expected_fmt;
+ if (Output.enable_ansi_colors) {
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class});
+ return .zero;
+ }
+ globalObject.throw(Output.prettyFmt(fmt, true), .{expected_class});
return .zero;
}
diff --git a/src/deps/diffz/DiffMatchPatch.zig b/src/deps/diffz/DiffMatchPatch.zig
new file mode 100644
index 000000000..a85bc950d
--- /dev/null
+++ b/src/deps/diffz/DiffMatchPatch.zig
@@ -0,0 +1,2247 @@
+// MIT License
+
+// Copyright (c) 2023 diffz authors
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+const DiffMatchPatch = @This();
+
+const std = @import("std");
+const testing = std.testing;
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+const DiffList = ArrayListUnmanaged(Diff);
+
+/// DMP with default configuration options
+pub const default = DiffMatchPatch{};
+
+pub const Diff = struct {
+ pub const Operation = enum {
+ insert,
+ delete,
+ equal,
+ };
+
+ operation: Operation,
+ text: []const u8,
+
+ pub fn format(value: Diff, _: anytype, _: anytype, writer: anytype) !void {
+ try writer.print("({s}, \"{s}\")", .{
+ switch (value.operation) {
+ .equal => "=",
+ .insert => "+",
+ .delete => "-",
+ },
+ value.text,
+ });
+ }
+
+ pub fn init(operation: Operation, text: []const u8) Diff {
+ return .{ .operation = operation, .text = text };
+ }
+ pub fn eql(a: Diff, b: Diff) bool {
+ return a.operation == b.operation and std.mem.eql(u8, a.text, b.text);
+ }
+};
+
+/// Number of milliseconds to map a diff before giving up (0 for infinity).
+diff_timeout: u64 = 1000,
+/// Cost of an empty edit operation in terms of edit characters.
+diff_edit_cost: u16 = 4,
+
+/// At what point is no match declared (0.0 = perfection, 1.0 = very loose).
+match_threshold: f32 = 0.5,
+/// How far to search for a match (0 = exact location, 1000+ = broad match).
+/// A match this many characters away from the expected location will add
+/// 1.0 to the score (0.0 is a perfect match).
+match_distance: u32 = 1000,
+/// The number of bits in an int.
+match_max_bits: u16 = 32,
+
+/// When deleting a large block of text (over ~64 characters), how close
+/// do the contents have to be to match the expected contents. (0.0 =
+/// perfection, 1.0 = very loose). Note that Match_Threshold controls
+/// how closely the end points of a delete need to match.
+patch_delete_threshold: f32 = 0.5,
+/// Chunk size for context length.
+patch_margin: u16 = 4,
+
+pub const DiffError = error{OutOfMemory};
+
+/// It is recommended that you use an Arena for this operation.
+///
+/// Find the differences between two texts.
+/// @param before Old string to be diffed.
+/// @param after New string to be diffed.
+/// @param checklines Speedup flag. If false, then don't run a
+/// line-level diff first to identify the changed areas.
+/// If true, then run a faster slightly less optimal diff.
+/// @return List of Diff objects.
+pub fn diff(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ before: []const u8,
+ after: []const u8,
+ /// If false, then don't run a line-level diff first
+ /// to identify the changed areas. If true, then run
+ /// a faster slightly less optimal diff.
+ check_lines: bool,
+) DiffError!DiffList {
+ const deadline = if (dmp.diff_timeout == 0)
+ std.math.maxInt(u64)
+ else
+ @intCast(u64, std.time.milliTimestamp()) + dmp.diff_timeout;
+ return dmp.diffInternal(allocator, before, after, check_lines, deadline);
+}
+
+/// Find difference between two texts by line.
+/// @param text1 Old string to be diffed.
+/// @param text2 New string to be diffed.
+/// @param deadline Time when the diff should be complete by.
+/// @return List of Diff objects.
+pub fn diffLines(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ text1_in: []const u8,
+ text2_in: []const u8,
+) DiffError!DiffList {
+ const deadline = if (dmp.diff_timeout == 0)
+ std.math.maxInt(u64)
+ else
+ @intCast(u64, std.time.milliTimestamp()) + dmp.diff_timeout;
+
+ var a = try diffLinesToChars(allocator, text1_in, text2_in);
+ var diffs = try dmp.diffInternal(allocator, a.chars_1, a.chars_2, false, deadline);
+ try diffCharsToLines(allocator, diffs.items, a.line_array.items);
+
+ return diffs;
+}
+
+fn diffInternal(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ before: []const u8,
+ after: []const u8,
+ check_lines: bool,
+ deadline: u64,
+) DiffError!DiffList {
+ // Check for equality (speedup).
+ var diffs = DiffList{};
+ if (std.mem.eql(u8, before, after)) {
+ if (before.len != 0) {
+ try diffs.append(allocator, Diff.init(.equal, try allocator.dupe(u8, before)));
+ }
+ return diffs;
+ }
+
+ // Trim off common prefix (speedup).
+ var common_length = diffCommonPrefix(before, after);
+ const common_prefix = before[0..common_length];
+ var trimmed_before = before[common_length..];
+ var trimmed_after = after[common_length..];
+
+ // Trim off common suffix (speedup).
+ common_length = diffCommonSuffix(trimmed_before, trimmed_after);
+ var common_suffix = trimmed_before[trimmed_before.len - common_length ..];
+ trimmed_before = trimmed_before[0 .. trimmed_before.len - common_length];
+ trimmed_after = trimmed_after[0 .. trimmed_after.len - common_length];
+
+ // Compute the diff on the middle block.
+ diffs = try dmp.diffCompute(allocator, trimmed_before, trimmed_after, check_lines, deadline);
+
+ // Restore the prefix and suffix.
+ if (common_prefix.len != 0) {
+ try diffs.insert(allocator, 0, Diff.init(.equal, try allocator.dupe(u8, common_prefix)));
+ }
+ if (common_suffix.len != 0) {
+ try diffs.append(allocator, Diff.init(.equal, try allocator.dupe(u8, common_suffix)));
+ }
+
+ try diffCleanupMerge(allocator, &diffs);
+ return diffs;
+}
+
+fn diffCommonPrefix(before: []const u8, after: []const u8) usize {
+ const n = std.math.min(before.len, after.len);
+ var i: usize = 0;
+
+ while (i < n) : (i += 1) {
+ if (before[i] != after[i]) {
+ return i;
+ }
+ }
+
+ return n;
+}
+
+fn diffCommonSuffix(before: []const u8, after: []const u8) usize {
+ const n = std.math.min(before.len, after.len);
+ var i: usize = 1;
+
+ while (i <= n) : (i += 1) {
+ if (before[before.len - i] != after[after.len - i]) {
+ return i - 1;
+ }
+ }
+
+ return n;
+}
+
+/// Find the differences between two texts. Assumes that the texts do not
+/// have any common prefix or suffix.
+/// @param before Old string to be diffed.
+/// @param after New string to be diffed.
+/// @param checklines Speedup flag. If false, then don't run a
+/// line-level diff first to identify the changed areas.
+/// If true, then run a faster slightly less optimal diff.
+/// @param deadline Time when the diff should be complete by.
+/// @return List of Diff objects.
+fn diffCompute(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ before: []const u8,
+ after: []const u8,
+ check_lines: bool,
+ deadline: u64,
+) DiffError!DiffList {
+ var diffs = DiffList{};
+
+ if (before.len == 0) {
+ // Just add some text (speedup).
+ try diffs.append(allocator, Diff.init(.insert, try allocator.dupe(u8, after)));
+ return diffs;
+ }
+
+ if (after.len == 0) {
+ // Just delete some text (speedup).
+ try diffs.append(allocator, Diff.init(.delete, try allocator.dupe(u8, before)));
+ return diffs;
+ }
+
+ const long_text = if (before.len > after.len) before else after;
+ const short_text = if (before.len > after.len) after else before;
+
+ if (std.mem.indexOf(u8, long_text, short_text)) |index| {
+ // Shorter text is inside the longer text (speedup).
+ const op: Diff.Operation = if (before.len > after.len)
+ .delete
+ else
+ .insert;
+ try diffs.append(allocator, Diff.init(op, try allocator.dupe(u8, long_text[0..index])));
+ try diffs.append(allocator, Diff.init(.equal, try allocator.dupe(u8, short_text)));
+ try diffs.append(allocator, Diff.init(op, try allocator.dupe(u8, long_text[index + short_text.len ..])));
+ return diffs;
+ }
+
+ if (short_text.len == 1) {
+ // Single character string.
+ // After the previous speedup, the character can't be an equality.
+ try diffs.append(allocator, Diff.init(.delete, before));
+ try diffs.append(allocator, Diff.init(.insert, after));
+ return diffs;
+ }
+
+ // Check to see if the problem can be split in two.
+ if (try dmp.diffHalfMatch(allocator, before, after)) |half_match| {
+ // A half-match was found, sort out the return data.
+
+ // Send both pairs off for separate processing.
+ var diffs_a = try dmp.diffInternal(
+ allocator,
+ half_match.prefix_before,
+ half_match.prefix_after,
+ check_lines,
+ deadline,
+ );
+ var diffs_b = try dmp.diffInternal(
+ allocator,
+ half_match.suffix_before,
+ half_match.suffix_after,
+ check_lines,
+ deadline,
+ );
+ defer diffs_b.deinit(allocator);
+
+ var tmp_diffs = diffs;
+ defer tmp_diffs.deinit(allocator);
+
+ // Merge the results.
+ diffs = diffs_a;
+ try diffs.append(allocator, Diff.init(.equal, half_match.common_middle));
+ try diffs.appendSlice(allocator, diffs_b.items);
+ return diffs;
+ }
+
+ if (check_lines and before.len > 100 and after.len > 100) {
+ return dmp.diffLineMode(allocator, before, after, deadline);
+ }
+
+ return dmp.diffBisect(allocator, before, after, deadline);
+}
+
+const HalfMatchResult = struct {
+ prefix_before: []const u8,
+ suffix_before: []const u8,
+ prefix_after: []const u8,
+ suffix_after: []const u8,
+ common_middle: []const u8,
+};
+
+/// Do the two texts share a Substring which is at least half the length of
+/// the longer text?
+/// This speedup can produce non-minimal diffs.
+/// @param before First string.
+/// @param after Second string.
+/// @return Five element String array, containing the prefix of text1, the
+/// suffix of text1, the prefix of text2, the suffix of text2 and the
+/// common middle. Or null if there was no match.
+fn diffHalfMatch(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ before: []const u8,
+ after: []const u8,
+) DiffError!?HalfMatchResult {
+ if (dmp.diff_timeout <= 0) {
+ // Don't risk returning a non-optimal diff if we have unlimited time.
+ return null;
+ }
+ const long_text = if (before.len > after.len) before else after;
+ const short_text = if (before.len > after.len) after else before;
+
+ if (long_text.len < 4 or short_text.len * 2 < long_text.len) {
+ return null; // Pointless.
+ }
+
+ // First check if the second quarter is the seed for a half-match.
+ var half_match_1 = try dmp.diffHalfMatchInternal(allocator, long_text, short_text, (long_text.len + 3) / 4);
+ // Check again based on the third quarter.
+ var half_match_2 = try dmp.diffHalfMatchInternal(allocator, long_text, short_text, (long_text.len + 1) / 2);
+
+ var half_match: ?HalfMatchResult = null;
+ if (half_match_1 == null and half_match_2 == null) {
+ return null;
+ } else if (half_match_2 == null) {
+ half_match = half_match_1.?;
+ } else if (half_match_1 == null) {
+ half_match = half_match_2.?;
+ } else {
+ // Both matched. Select the longest.
+ half_match = if (half_match_1.?.common_middle.len > half_match_2.?.common_middle.len)
+ half_match_1
+ else
+ half_match_2;
+ }
+
+ // A half-match was found, sort out the return data.
+ if (before.len > after.len) {
+ return half_match;
+ } else {
+ const half_match_yes = half_match.?;
+ return .{
+ .prefix_before = half_match_yes.prefix_after,
+ .suffix_before = half_match_yes.suffix_after,
+ .prefix_after = half_match_yes.prefix_before,
+ .suffix_after = half_match_yes.suffix_before,
+ .common_middle = half_match_yes.common_middle,
+ };
+ }
+}
+
+/// Does a Substring of shorttext exist within longtext such that the
+/// Substring is at least half the length of longtext?
+/// @param longtext Longer string.
+/// @param shorttext Shorter string.
+/// @param i Start index of quarter length Substring within longtext.
+/// @return Five element string array, containing the prefix of longtext, the
+/// suffix of longtext, the prefix of shorttext, the suffix of shorttext
+/// and the common middle. Or null if there was no match.
+fn diffHalfMatchInternal(
+ _: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ long_text: []const u8,
+ short_text: []const u8,
+ i: usize,
+) DiffError!?HalfMatchResult {
+ // Start with a 1/4 length Substring at position i as a seed.
+ const seed = long_text[i .. i + long_text.len / 4];
+ var j: isize = -1;
+
+ var best_common = std.ArrayListUnmanaged(u8){};
+ var best_long_text_a: []const u8 = "";
+ var best_long_text_b: []const u8 = "";
+ var best_short_text_a: []const u8 = "";
+ var best_short_text_b: []const u8 = "";
+
+ while (j < short_text.len and b: {
+ j = @intCast(isize, std.mem.indexOf(u8, short_text[@intCast(usize, j + 1)..], seed) orelse break :b false) + j + 1;
+ break :b true;
+ }) {
+ var prefix_length = diffCommonPrefix(long_text[i..], short_text[@intCast(usize, j)..]);
+ var suffix_length = diffCommonSuffix(long_text[0..i], short_text[0..@intCast(usize, j)]);
+ if (best_common.items.len < suffix_length + prefix_length) {
+ best_common.items.len = 0;
+ try best_common.appendSlice(allocator, short_text[@intCast(usize, j - @intCast(isize, suffix_length)) .. @intCast(usize, j - @intCast(isize, suffix_length)) + suffix_length]);
+ try best_common.appendSlice(allocator, short_text[@intCast(usize, j) .. @intCast(usize, j) + prefix_length]);
+
+ best_long_text_a = long_text[0 .. i - suffix_length];
+ best_long_text_b = long_text[i + prefix_length ..];
+ best_short_text_a = short_text[0..@intCast(usize, j - @intCast(isize, suffix_length))];
+ best_short_text_b = short_text[@intCast(usize, j + @intCast(isize, prefix_length))..];
+ }
+ }
+ if (best_common.items.len * 2 >= long_text.len) {
+ return .{
+ .prefix_before = best_long_text_a,
+ .suffix_before = best_long_text_b,
+ .prefix_after = best_short_text_a,
+ .suffix_after = best_short_text_b,
+ .common_middle = best_common.items,
+ };
+ } else {
+ return null;
+ }
+}
+
+/// Find the 'middle snake' of a diff, split the problem in two
+/// and return the recursively constructed diff.
+/// See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+/// @param before Old string to be diffed.
+/// @param after New string to be diffed.
+/// @param deadline Time at which to bail if not yet complete.
+/// @return List of Diff objects.
+fn diffBisect(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ before: []const u8,
+ after: []const u8,
+ deadline: u64,
+) DiffError!DiffList {
+ const before_length = @intCast(isize, before.len);
+ const after_length = @intCast(isize, after.len);
+ const max_d = @intCast(isize, (before.len + after.len + 1) / 2);
+ const v_offset = max_d;
+ const v_length = 2 * max_d;
+
+ var v1 = try ArrayListUnmanaged(isize).initCapacity(allocator, @intCast(usize, v_length));
+ v1.items.len = @intCast(usize, v_length);
+ var v2 = try ArrayListUnmanaged(isize).initCapacity(allocator, @intCast(usize, v_length));
+ v2.items.len = @intCast(usize, v_length);
+
+ var x: usize = 0;
+ while (x < v_length) : (x += 1) {
+ v1.items[x] = -1;
+ v2.items[x] = -1;
+ }
+ v1.items[@intCast(usize, v_offset + 1)] = 0;
+ v2.items[@intCast(usize, v_offset + 1)] = 0;
+ const delta = before_length - after_length;
+ // If the total number of characters is odd, then the front path will
+ // collide with the reverse path.
+ const front = (@mod(delta, 2) != 0);
+ // Offsets for start and end of k loop.
+ // Prevents mapping of space beyond the grid.
+ var k1start: isize = 0;
+ var k1end: isize = 0;
+ var k2start: isize = 0;
+ var k2end: isize = 0;
+
+ var d: isize = 0;
+ while (d < max_d) : (d += 1) {
+ // Bail out if deadline is reached.
+ if (@intCast(u64, std.time.milliTimestamp()) > deadline) {
+ break;
+ }
+
+ // Walk the front path one step.
+ var k1 = -d + k1start;
+ while (k1 <= d - k1end) : (k1 += 2) {
+ var k1_offset = v_offset + k1;
+ var x1: isize = 0;
+ if (k1 == -d or (k1 != d and
+ v1.items[@intCast(usize, k1_offset - 1)] < v1.items[@intCast(usize, k1_offset + 1)]))
+ {
+ x1 = v1.items[@intCast(usize, k1_offset + 1)];
+ } else {
+ x1 = v1.items[@intCast(usize, k1_offset - 1)] + 1;
+ }
+ var y1 = x1 - k1;
+ while (x1 < before_length and
+ y1 < after_length and before[@intCast(usize, x1)] == after[@intCast(usize, y1)])
+ {
+ x1 += 1;
+ y1 += 1;
+ }
+ v1.items[@intCast(usize, k1_offset)] = x1;
+ if (x1 > before_length) {
+ // Ran off the right of the graph.
+ k1end += 2;
+ } else if (y1 > after_length) {
+ // Ran off the bottom of the graph.
+ k1start += 2;
+ } else if (front) {
+ var k2_offset = v_offset + delta - k1;
+ if (k2_offset >= 0 and k2_offset < v_length and v2.items[@intCast(usize, k2_offset)] != -1) {
+ // Mirror x2 onto top-left coordinate system.
+ const x2 = before_length - v2.items[@intCast(usize, k2_offset)];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return dmp.diffBisectSplit(allocator, before, after, x1, y1, deadline);
+ }
+ }
+ }
+ }
+
+ // Walk the reverse path one step.
+ var k2: isize = -d + k2start;
+ while (k2 <= d - k2end) : (k2 += 2) {
+ const k2_offset = v_offset + k2;
+ var x2: isize = 0;
+ if (k2 == -d or (k2 != d and
+ v2.items[@intCast(usize, k2_offset - 1)] < v2.items[@intCast(usize, k2_offset + 1)]))
+ {
+ x2 = v2.items[@intCast(usize, k2_offset + 1)];
+ } else {
+ x2 = v2.items[@intCast(usize, k2_offset - 1)] + 1;
+ }
+ var y2: isize = x2 - k2;
+ while (x2 < before_length and y2 < after_length and
+ before[@intCast(usize, before_length - x2 - 1)] ==
+ after[@intCast(usize, after_length - y2 - 1)])
+ {
+ x2 += 1;
+ y2 += 1;
+ }
+ v2.items[@intCast(usize, k2_offset)] = x2;
+ if (x2 > before_length) {
+ // Ran off the left of the graph.
+ k2end += 2;
+ } else if (y2 > after_length) {
+ // Ran off the top of the graph.
+ k2start += 2;
+ } else if (!front) {
+ const k1_offset = v_offset + delta - k2;
+ if (k1_offset >= 0 and k1_offset < v_length and v1.items[@intCast(usize, k1_offset)] != -1) {
+ const x1 = v1.items[@intCast(usize, k1_offset)];
+ const y1 = v_offset + x1 - k1_offset;
+ // Mirror x2 onto top-left coordinate system.
+ x2 = before_length - v2.items[@intCast(usize, k2_offset)];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return dmp.diffBisectSplit(allocator, before, after, x1, y1, deadline);
+ }
+ }
+ }
+ }
+ }
+ // Diff took too long and hit the deadline or
+ // number of diffs equals number of characters, no commonality at all.
+ var diffs = DiffList{};
+ try diffs.append(allocator, Diff.init(.delete, try allocator.dupe(u8, before)));
+ try diffs.append(allocator, Diff.init(.insert, try allocator.dupe(u8, after)));
+ return diffs;
+}
+
+/// Given the location of the 'middle snake', split the diff in two parts
+/// and recurse.
+/// @param text1 Old string to be diffed.
+/// @param text2 New string to be diffed.
+/// @param x Index of split point in text1.
+/// @param y Index of split point in text2.
+/// @param deadline Time at which to bail if not yet complete.
+/// @return LinkedList of Diff objects.
+fn diffBisectSplit(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ text1: []const u8,
+ text2: []const u8,
+ x: isize,
+ y: isize,
+ deadline: u64,
+) DiffError!DiffList {
+ const text1a = text1[0..@intCast(usize, x)];
+ const text2a = text2[0..@intCast(usize, y)];
+ const text1b = text1[@intCast(usize, x)..];
+ const text2b = text2[@intCast(usize, y)..];
+
+ // Compute both diffs serially.
+ var diffs = try dmp.diffInternal(allocator, text1a, text2a, false, deadline);
+ var diffsb = try dmp.diffInternal(allocator, text1b, text2b, false, deadline);
+ defer diffsb.deinit(allocator);
+
+ try diffs.appendSlice(allocator, diffsb.items);
+ return diffs;
+}
+
+/// Do a quick line-level diff on both strings, then rediff the parts for
+/// greater accuracy.
+/// This speedup can produce non-minimal diffs.
+/// @param text1 Old string to be diffed.
+/// @param text2 New string to be diffed.
+/// @param deadline Time when the diff should be complete by.
+/// @return List of Diff objects.
+fn diffLineMode(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ text1_in: []const u8,
+ text2_in: []const u8,
+ deadline: u64,
+) DiffError!DiffList {
+ // Scan the text on a line-by-line basis first.
+ var a = try diffLinesToChars(allocator, text1_in, text2_in);
+ var text1 = a.chars_1;
+ var text2 = a.chars_2;
+ var line_array = a.line_array;
+
+ var diffs: DiffList = try dmp.diffInternal(allocator, text1, text2, false, deadline);
+
+ // Convert the diff back to original text.
+ try diffCharsToLines(allocator, diffs.items, line_array.items);
+ // Eliminate freak matches (e.g. blank lines)
+ try diffCleanupSemantic(allocator, &diffs);
+
+ // Rediff any replacement blocks, this time character-by-character.
+ // Add a dummy entry at the end.
+ try diffs.append(allocator, Diff.init(.equal, ""));
+
+ var pointer: usize = 0;
+ var count_delete: usize = 0;
+ var count_insert: usize = 0;
+ var text_delete = ArrayListUnmanaged(u8){};
+ var text_insert = ArrayListUnmanaged(u8){};
+ defer {
+ text_delete.deinit(allocator);
+ text_insert.deinit(allocator);
+ }
+
+ while (pointer < diffs.items.len) {
+ switch (diffs.items[pointer].operation) {
+ .insert => {
+ count_insert += 1;
+ // text_insert += diffs.items[pointer].text;
+ try text_insert.appendSlice(allocator, diffs.items[pointer].text);
+ },
+ .delete => {
+ count_delete += 1;
+ // text_delete += diffs.items[pointer].text;
+ try text_delete.appendSlice(allocator, diffs.items[pointer].text);
+ },
+ .equal => {
+ // Upon reaching an equality, check for prior redundancies.
+ if (count_delete >= 1 and count_insert >= 1) {
+ // Delete the offending records and add the merged ones.
+ // diffs.RemoveRange(pointer - count_delete - count_insert, count_delete + count_insert);
+ try diffs.replaceRange(
+ allocator,
+ pointer - count_delete - count_insert,
+ count_delete + count_insert,
+ &.{},
+ );
+ pointer = pointer - count_delete - count_insert;
+ var sub_diff = try dmp.diffInternal(allocator, text_delete.items, text_insert.items, false, deadline);
+ // diffs.InsertRange(pointer, sub_diff);
+ try diffs.insertSlice(allocator, pointer, sub_diff.items);
+ pointer = pointer + sub_diff.items.len;
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete.items.len = 0;
+ text_insert.items.len = 0;
+ },
+ }
+ pointer += 1;
+ }
+ // diffs.RemoveAt(diffs.Count - 1); // Remove the dummy entry at the end.
+ diffs.items.len -= 1;
+
+ return diffs;
+}
+
+const LinesToCharsResult = struct {
+ chars_1: []const u8,
+ chars_2: []const u8,
+ line_array: ArrayListUnmanaged([]const u8),
+};
+
+/// Split two texts into a list of strings. Reduce the texts to a string of
+/// hashes where each Unicode character represents one line.
+/// @param text1 First string.
+/// @param text2 Second string.
+/// @return Three element Object array, containing the encoded text1, the
+/// encoded text2 and the List of unique strings. The zeroth element
+/// of the List of unique strings is intentionally blank.
+fn diffLinesToChars(
+ allocator: std.mem.Allocator,
+ text1: []const u8,
+ text2: []const u8,
+) DiffError!LinesToCharsResult {
+ var line_array = ArrayListUnmanaged([]const u8){};
+ var line_hash = std.StringHashMapUnmanaged(usize){};
+ // e.g. line_array[4] == "Hello\n"
+ // e.g. line_hash.get("Hello\n") == 4
+
+ // "\x00" is a valid character, but various debuggers don't like it.
+ // So we'll insert a junk entry to avoid generating a null character.
+ try line_array.append(allocator, "");
+
+ // Allocate 2/3rds of the space for text1, the rest for text2.
+ var chars1 = try diffLinesToCharsMunge(allocator, text1, &line_array, &line_hash, 170);
+ var chars2 = try diffLinesToCharsMunge(allocator, text2, &line_array, &line_hash, 255);
+ return .{ .chars_1 = chars1, .chars_2 = chars2, .line_array = line_array };
+}
+
+/// Split a text into a list of strings. Reduce the texts to a string of
+/// hashes where each Unicode character represents one line.
+/// @param text String to encode.
+/// @param lineArray List of unique strings.
+/// @param lineHash Map of strings to indices.
+/// @param maxLines Maximum length of lineArray.
+/// @return Encoded string.
+fn diffLinesToCharsMunge(
+ allocator: std.mem.Allocator,
+ text: []const u8,
+ line_array: *ArrayListUnmanaged([]const u8),
+ line_hash: *std.StringHashMapUnmanaged(usize),
+ max_lines: usize,
+) DiffError![]const u8 {
+ var line_start: isize = 0;
+ var line_end: isize = -1;
+ var line: []const u8 = "";
+ var chars = ArrayListUnmanaged(u8){};
+ // Walk the text, pulling out a Substring for each line.
+ // text.split('\n') would would temporarily double our memory footprint.
+ // Modifying text would create many large strings to garbage collect.
+ while (line_end < @intCast(isize, text.len) - 1) {
+ line_end = b: {
+ break :b @intCast(isize, std.mem.indexOf(u8, text[@intCast(usize, line_start)..], "\n") orelse
+ break :b @intCast(isize, text.len - 1)) + line_start;
+ };
+ line = text[@intCast(usize, line_start) .. @intCast(usize, line_start) + @intCast(usize, line_end + 1 - line_start)];
+
+ if (line_hash.get(line)) |value| {
+ try chars.append(allocator, @intCast(u8, value));
+ } else {
+ if (line_array.items.len == max_lines) {
+ // Bail out at 255 because char 256 == char 0.
+ line = text[@intCast(usize, line_start)..];
+ line_end = @intCast(isize, text.len);
+ }
+ try line_array.append(allocator, line);
+ try line_hash.put(allocator, line, line_array.items.len - 1);
+ try chars.append(allocator, @intCast(u8, line_array.items.len - 1));
+ }
+ line_start = line_end + 1;
+ }
+ return try chars.toOwnedSlice(allocator);
+}
+
+/// Rehydrate the text in a diff from a string of line hashes to real lines
+/// of text.
+/// @param diffs List of Diff objects.
+/// @param lineArray List of unique strings.
+fn diffCharsToLines(
+ allocator: std.mem.Allocator,
+ diffs: []Diff,
+ line_array: []const []const u8,
+) DiffError!void {
+ var text = ArrayListUnmanaged(u8){};
+ defer text.deinit(allocator);
+
+ for (diffs) |*d| {
+ text.items.len = 0;
+ var j: usize = 0;
+ while (j < d.text.len) : (j += 1) {
+ try text.appendSlice(allocator, line_array[d.text[j]]);
+ }
+ d.text = try allocator.dupe(u8, text.items);
+ }
+}
+
+/// Reorder and merge like edit sections. Merge equalities.
+/// Any edit section can move as long as it doesn't cross an equality.
+/// @param diffs List of Diff objects.
+fn diffCleanupMerge(allocator: std.mem.Allocator, diffs: *DiffList) DiffError!void {
+ // Add a dummy entry at the end.
+ try diffs.append(allocator, Diff.init(.equal, ""));
+ var pointer: usize = 0;
+ var count_delete: usize = 0;
+ var count_insert: usize = 0;
+
+ var text_delete = ArrayListUnmanaged(u8){};
+ defer text_delete.deinit(allocator);
+
+ var text_insert = ArrayListUnmanaged(u8){};
+ defer text_insert.deinit(allocator);
+
+ var common_length: usize = undefined;
+ while (pointer < diffs.items.len) {
+ switch (diffs.items[pointer].operation) {
+ .insert => {
+ count_insert += 1;
+ try text_insert.appendSlice(allocator, diffs.items[pointer].text);
+ pointer += 1;
+ },
+ .delete => {
+ count_delete += 1;
+ try text_delete.appendSlice(allocator, diffs.items[pointer].text);
+ pointer += 1;
+ },
+ .equal => {
+ // Upon reaching an equality, check for prior redundancies.
+ if (count_delete + count_insert > 1) {
+ if (count_delete != 0 and count_insert != 0) {
+ // Factor out any common prefixies.
+ common_length = diffCommonPrefix(text_insert.items, text_delete.items);
+ if (common_length != 0) {
+ if ((pointer - count_delete - count_insert) > 0 and
+ diffs.items[pointer - count_delete - count_insert - 1].operation == .equal)
+ {
+ // diffs.items[pointer - count_delete - count_insert - 1].text
+ // += text_insert.Substring(0, common_length);
+
+ const ii = pointer - count_delete - count_insert - 1;
+ var nt = try allocator.alloc(u8, diffs.items[ii].text.len + common_length);
+
+ // try diffs.items[pointer - count_delete - count_insert - 1].text.append(allocator, text_insert.items[0..common_length]);
+ std.mem.copy(u8, nt, diffs.items[ii].text);
+ std.mem.copy(u8, nt[diffs.items[ii].text.len..], text_insert.items[0..common_length]);
+
+ // allocator.free(diffs.items[ii].text);
+ diffs.items[ii].text = nt;
+ } else {
+ // diffs.Insert(0, Diff.init(.equal,
+ // text_insert.Substring(0, common_length)));
+ const text = std.ArrayListUnmanaged(u8){
+ .items = try allocator.dupe(u8, text_insert.items[0..common_length]),
+ };
+ try diffs.insert(allocator, 0, Diff.init(.equal, try allocator.dupe(u8, text.items)));
+ pointer += 1;
+ }
+ try text_insert.replaceRange(allocator, 0, common_length, &.{});
+ try text_delete.replaceRange(allocator, 0, common_length, &.{});
+ }
+ // Factor out any common suffixies.
+ // @ZigPort this seems very wrong
+ common_length = diffCommonSuffix(text_insert.items, text_delete.items);
+ if (common_length != 0) {
+ diffs.items[pointer].text = try std.mem.concat(allocator, u8, &.{
+ text_insert.items[text_insert.items.len - common_length ..],
+ diffs.items[pointer].text,
+ });
+ text_insert.items.len -= common_length;
+ text_delete.items.len -= common_length;
+ }
+ }
+ // Delete the offending records and add the merged ones.
+ pointer -= count_delete + count_insert;
+ try diffs.replaceRange(allocator, pointer, count_delete + count_insert, &.{});
+
+ if (text_delete.items.len != 0) {
+ try diffs.replaceRange(allocator, pointer, 0, &.{
+ Diff.init(.delete, try allocator.dupe(u8, text_delete.items)),
+ });
+ pointer += 1;
+ }
+ if (text_insert.items.len != 0) {
+ try diffs.replaceRange(allocator, pointer, 0, &.{
+ Diff.init(.insert, try allocator.dupe(u8, text_insert.items)),
+ });
+ pointer += 1;
+ }
+ pointer += 1;
+ } else if (pointer != 0 and diffs.items[pointer - 1].operation == .equal) {
+ // Merge this equality with the previous one.
+ // TODO: Fix using realloc or smth
+
+ var nt = try allocator.alloc(u8, diffs.items[pointer - 1].text.len + diffs.items[pointer].text.len);
+
+ // try diffs.items[pointer - count_delete - count_insert - 1].text.append(allocator, text_insert.items[0..common_length]);
+ std.mem.copy(u8, nt, diffs.items[pointer - 1].text);
+ std.mem.copy(u8, nt[diffs.items[pointer - 1].text.len..], diffs.items[pointer].text);
+
+ // allocator.free(diffs.items[pointer - 1].text);
+ diffs.items[pointer - 1].text = nt;
+ // allocator.free(diffs.items[pointer].text);
+
+ // try diffs.items[pointer - 1].text.append(allocator, diffs.items[pointer].text.items);
+ _ = diffs.orderedRemove(pointer);
+ } else {
+ pointer += 1;
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete.items.len = 0;
+ text_insert.items.len = 0;
+ },
+ }
+ }
+ if (diffs.items[diffs.items.len - 1].text.len == 0) {
+ diffs.items.len -= 1;
+ }
+
+ // Second pass: look for single edits surrounded on both sides by
+ // equalities which can be shifted sideways to eliminate an equality.
+ // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+ var changes = false;
+ pointer = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < (diffs.items.len - 1)) {
+ if (diffs.items[pointer - 1].operation == .equal and
+ diffs.items[pointer + 1].operation == .equal)
+ {
+ // This is a single edit surrounded by equalities.
+ if (std.mem.endsWith(u8, diffs.items[pointer].text, diffs.items[pointer - 1].text)) {
+ // Shift the edit over the previous equality.
+ // diffs.items[pointer].text = diffs.items[pointer - 1].text +
+ // diffs.items[pointer].text[0 .. diffs.items[pointer].text.len -
+ // diffs.items[pointer - 1].text.len];
+ // diffs.items[pointer + 1].text = diffs.items[pointer - 1].text + diffs.items[pointer + 1].text;
+
+ const pt = try std.mem.concat(allocator, u8, &.{
+ diffs.items[pointer - 1].text,
+ diffs.items[pointer].text[0 .. diffs.items[pointer].text.len -
+ diffs.items[pointer - 1].text.len],
+ });
+ const p1t = try std.mem.concat(allocator, u8, &.{
+ diffs.items[pointer - 1].text,
+ diffs.items[pointer + 1].text,
+ });
+
+ // allocator.free(diffs.items[pointer].text);
+ // allocator.free(diffs.items[pointer + 1].text);
+
+ diffs.items[pointer].text = pt;
+ diffs.items[pointer + 1].text = p1t;
+
+ try diffs.replaceRange(allocator, pointer - 1, 1, &.{});
+ changes = true;
+ } else if (std.mem.startsWith(u8, diffs.items[pointer].text, diffs.items[pointer + 1].text)) {
+ // Shift the edit over the next equality.
+ // diffs.items[pointer - 1].text += diffs.items[pointer + 1].text;
+ // diffs.items[pointer].text =
+ // diffs.items[pointer].text[diffs.items[pointer + 1].text.len..] + diffs.items[pointer + 1].text;
+
+ const pm1t = try std.mem.concat(allocator, u8, &.{
+ diffs.items[pointer - 1].text,
+ diffs.items[pointer + 1].text,
+ });
+ const pt = try std.mem.concat(allocator, u8, &.{
+ diffs.items[pointer].text[diffs.items[pointer + 1].text.len..],
+ diffs.items[pointer + 1].text,
+ });
+
+ // allocator.free(diffs.items[pointer - 1].text);
+ // allocator.free(diffs.items[pointer].text);
+
+ diffs.items[pointer - 1].text = pm1t;
+ diffs.items[pointer].text = pt;
+
+ try diffs.replaceRange(allocator, pointer + 1, 1, &.{});
+ changes = true;
+ }
+ }
+ pointer += 1;
+ }
+ // If shifts were made, the diff needs reordering and another shift sweep.
+ if (changes) {
+ try diffCleanupMerge(allocator, diffs);
+ }
+}
+
+/// Reduce the number of edits by eliminating semantically trivial
+/// equalities.
+/// @param diffs List of Diff objects.
+fn diffCleanupSemantic(allocator: std.mem.Allocator, diffs: *DiffList) DiffError!void {
+ var changes = false;
+ // Stack of indices where equalities are found.
+ var equalities = ArrayListUnmanaged(isize){};
+ // Always equal to equalities[equalitiesLength-1][1]
+ var last_equality: ?[]const u8 = null;
+ var pointer: isize = 0; // Index of current position.
+ // Number of characters that changed prior to the equality.
+ var length_insertions1: usize = 0;
+ var length_deletions1: usize = 0;
+ // Number of characters that changed after the equality.
+ var length_insertions2: usize = 0;
+ var length_deletions2: usize = 0;
+ while (pointer < diffs.items.len) {
+ if (diffs.items[@intCast(usize, pointer)].operation == .equal) { // Equality found.
+ try equalities.append(allocator, pointer);
+ length_insertions1 = length_insertions2;
+ length_deletions1 = length_deletions2;
+ length_insertions2 = 0;
+ length_deletions2 = 0;
+ last_equality = diffs.items[@intCast(usize, pointer)].text;
+ } else { // an insertion or deletion
+ if (diffs.items[@intCast(usize, pointer)].operation == .insert) {
+ length_insertions2 += diffs.items[@intCast(usize, pointer)].text.len;
+ } else {
+ length_deletions2 += diffs.items[@intCast(usize, pointer)].text.len;
+ }
+ // Eliminate an equality that is smaller or equal to the edits on both
+ // sides of it.
+ if (last_equality != null and
+ (last_equality.?.len <= std.math.max(length_insertions1, length_deletions1)) and
+ (last_equality.?.len <= std.math.max(length_insertions2, length_deletions2)))
+ {
+ // Duplicate record.
+ try diffs.insert(
+ allocator,
+ @intCast(usize, equalities.items[equalities.items.len - 1]),
+ Diff.init(.delete, try allocator.dupe(u8, last_equality.?)),
+ );
+ // Change second copy to insert.
+ diffs.items[@intCast(usize, equalities.items[equalities.items.len - 1] + 1)].operation = .insert;
+ // Throw away the equality we just deleted.
+ _ = equalities.pop();
+ if (equalities.items.len > 0) {
+ _ = equalities.pop();
+ }
+ pointer = if (equalities.items.len > 0) equalities.items[equalities.items.len - 1] else -1;
+ length_insertions1 = 0; // Reset the counters.
+ length_deletions1 = 0;
+ length_insertions2 = 0;
+ length_deletions2 = 0;
+ last_equality = null;
+ changes = true;
+ }
+ }
+ pointer += 1;
+ }
+
+ // Normalize the diff.
+ if (changes) {
+ try diffCleanupMerge(allocator, diffs);
+ }
+ try diffCleanupSemanticLossless(allocator, diffs);
+
+ // Find any overlaps between deletions and insertions.
+ // e.g: <del>abcxxx</del><ins>xxxdef</ins>
+ // -> <del>abc</del>xxx<ins>def</ins>
+ // e.g: <del>xxxabc</del><ins>defxxx</ins>
+ // -> <ins>def</ins>xxx<del>abc</del>
+ // Only extract an overlap if it is as big as the edit ahead or behind it.
+ pointer = 1;
+ while (pointer < diffs.items.len) {
+ if (diffs.items[@intCast(usize, pointer - 1)].operation == .delete and
+ diffs.items[@intCast(usize, pointer)].operation == .insert)
+ {
+ const deletion = diffs.items[@intCast(usize, pointer - 1)].text;
+ const insertion = diffs.items[@intCast(usize, pointer)].text;
+ var overlap_length1: usize = diffCommonOverlap(deletion, insertion);
+ var overlap_length2: usize = diffCommonOverlap(insertion, deletion);
+ if (overlap_length1 >= overlap_length2) {
+ if (@intToFloat(f32, overlap_length1) >= @intToFloat(f32, deletion.len) / 2.0 or
+ @intToFloat(f32, overlap_length1) >= @intToFloat(f32, insertion.len) / 2.0)
+ {
+ // Overlap found.
+ // Insert an equality and trim the surrounding edits.
+ try diffs.insert(
+ allocator,
+ @intCast(usize, pointer),
+ Diff.init(.equal, try allocator.dupe(u8, insertion[0..overlap_length1])),
+ );
+ diffs.items[@intCast(usize, pointer - 1)].text =
+ try allocator.dupe(u8, deletion[0 .. deletion.len - overlap_length1]);
+ diffs.items[@intCast(usize, pointer + 1)].text =
+ try allocator.dupe(u8, insertion[overlap_length1..]);
+ pointer += 1;
+ }
+ } else {
+ if (@intToFloat(f32, overlap_length2) >= @intToFloat(f32, deletion.len) / 2.0 or
+ @intToFloat(f32, overlap_length2) >= @intToFloat(f32, insertion.len) / 2.0)
+ {
+ // Reverse overlap found.
+ // Insert an equality and swap and trim the surrounding edits.
+ try diffs.insert(
+ allocator,
+ @intCast(usize, pointer),
+ Diff.init(.equal, try allocator.dupe(u8, deletion[0..overlap_length2])),
+ );
+ diffs.items[@intCast(usize, pointer - 1)].operation = .insert;
+ diffs.items[@intCast(usize, pointer - 1)].text =
+ try allocator.dupe(u8, insertion[0 .. insertion.len - overlap_length2]);
+ diffs.items[@intCast(usize, pointer + 1)].operation = .delete;
+ diffs.items[@intCast(usize, pointer + 1)].text =
+ try allocator.dupe(u8, deletion[overlap_length2..]);
+ pointer += 1;
+ }
+ }
+ pointer += 1;
+ }
+ pointer += 1;
+ }
+}
+
+/// Look for single edits surrounded on both sides by equalities
+/// which can be shifted sideways to align the edit to a word boundary.
+/// e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
+pub fn diffCleanupSemanticLossless(
+ allocator: std.mem.Allocator,
+ diffs: *DiffList,
+) DiffError!void {
+ var pointer: usize = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < @intCast(isize, diffs.items.len) - 1) {
+ if (diffs.items[pointer - 1].operation == .equal and
+ diffs.items[pointer + 1].operation == .equal)
+ {
+ // This is a single edit surrounded by equalities.
+ var equality_1 = std.ArrayListUnmanaged(u8){};
+ defer equality_1.deinit(allocator);
+ try equality_1.appendSlice(allocator, diffs.items[pointer - 1].text);
+
+ var edit = std.ArrayListUnmanaged(u8){};
+ defer edit.deinit(allocator);
+ try edit.appendSlice(allocator, diffs.items[pointer].text);
+
+ var equality_2 = std.ArrayListUnmanaged(u8){};
+ defer equality_2.deinit(allocator);
+ try equality_2.appendSlice(allocator, diffs.items[pointer + 1].text);
+
+ // First, shift the edit as far left as possible.
+ const common_offset = diffCommonSuffix(equality_1.items, edit.items);
+ if (common_offset > 0) {
+ // TODO: Use buffer
+ const common_string = try allocator.dupe(u8, edit.items[edit.items.len - common_offset ..]);
+ defer allocator.free(common_string);
+
+ equality_1.items.len = equality_1.items.len - common_offset;
+
+ // edit.items.len = edit.items.len - common_offset;
+ const not_common = try allocator.dupe(u8, edit.items[0 .. edit.items.len - common_offset]);
+ defer allocator.free(not_common);
+
+ edit.items.len = 0;
+ try edit.appendSlice(allocator, common_string);
+ try edit.appendSlice(allocator, not_common);
+
+ try equality_2.insertSlice(allocator, 0, common_string);
+ }
+
+ // Second, step character by character right,
+ // looking for the best fit.
+ var best_equality_1 = ArrayListUnmanaged(u8){};
+ defer best_equality_1.deinit(allocator);
+ try best_equality_1.appendSlice(allocator, equality_1.items);
+
+ var best_edit = ArrayListUnmanaged(u8){};
+ defer best_edit.deinit(allocator);
+ try best_edit.appendSlice(allocator, edit.items);
+
+ var best_equality_2 = ArrayListUnmanaged(u8){};
+ defer best_equality_2.deinit(allocator);
+ try best_equality_2.appendSlice(allocator, equality_2.items);
+
+ var best_score = diffCleanupSemanticScore(equality_1.items, edit.items) +
+ diffCleanupSemanticScore(edit.items, equality_2.items);
+
+ while (edit.items.len != 0 and equality_2.items.len != 0 and edit.items[0] == equality_2.items[0]) {
+ try equality_1.append(allocator, edit.items[0]);
+
+ _ = edit.orderedRemove(0);
+ try edit.append(allocator, equality_2.items[0]);
+
+ _ = equality_2.orderedRemove(0);
+
+ const score = diffCleanupSemanticScore(equality_1.items, edit.items) +
+ diffCleanupSemanticScore(edit.items, equality_2.items);
+ // The >= encourages trailing rather than leading whitespace on
+ // edits.
+ if (score >= best_score) {
+ best_score = score;
+
+ best_equality_1.items.len = 0;
+ try best_equality_1.appendSlice(allocator, equality_1.items);
+
+ best_edit.items.len = 0;
+ try best_edit.appendSlice(allocator, edit.items);
+
+ best_equality_2.items.len = 0;
+ try best_equality_2.appendSlice(allocator, equality_2.items);
+ }
+ }
+
+ if (!std.mem.eql(u8, diffs.items[pointer - 1].text, best_equality_1.items)) {
+ // We have an improvement, save it back to the diff.
+ if (best_equality_1.items.len != 0) {
+ diffs.items[pointer - 1].text = try allocator.dupe(u8, best_equality_1.items);
+ } else {
+ _ = diffs.orderedRemove(pointer - 1);
+ pointer -= 1;
+ }
+ diffs.items[pointer].text = try allocator.dupe(u8, best_edit.items);
+ if (best_equality_2.items.len != 0) {
+ diffs.items[pointer + 1].text = try allocator.dupe(u8, best_equality_2.items);
+ } else {
+ _ = diffs.orderedRemove(pointer + 1);
+ pointer -= 1;
+ }
+ }
+ }
+ pointer += 1;
+ }
+}
+
+/// Given two strings, compute a score representing whether the internal
+/// boundary falls on logical boundaries.
+/// Scores range from 6 (best) to 0 (worst).
+/// @param one First string.
+/// @param two Second string.
+/// @return The score.
+fn diffCleanupSemanticScore(one: []const u8, two: []const u8) usize {
+ if (one.len == 0 or two.len == 0) {
+ // Edges are the best.
+ return 6;
+ }
+
+ // Each port of this function behaves slightly differently due to
+ // subtle differences in each language's definition of things like
+ // 'whitespace'. Since this function's purpose is largely cosmetic,
+ // the choice has been made to use each language's native features
+ // rather than force total conformity.
+ const char1 = one[one.len - 1];
+ const char2 = two[0];
+ const nonAlphaNumeric1 = !std.ascii.isAlphanumeric(char1);
+ const nonAlphaNumeric2 = !std.ascii.isAlphanumeric(char2);
+ const whitespace1 = nonAlphaNumeric1 and std.ascii.isWhitespace(char1);
+ const whitespace2 = nonAlphaNumeric2 and std.ascii.isWhitespace(char2);
+ const lineBreak1 = whitespace1 and std.ascii.isControl(char1);
+ const lineBreak2 = whitespace2 and std.ascii.isControl(char2);
+ const blankLine1 = lineBreak1 and
+ // BLANKLINEEND.IsMatch(one);
+ (std.mem.endsWith(u8, one, "\n\n") or std.mem.endsWith(u8, one, "\n\r\n"));
+ const blankLine2 = lineBreak2 and
+ // BLANKLINESTART.IsMatch(two);
+ (std.mem.startsWith(u8, two, "\n\n") or
+ std.mem.startsWith(u8, two, "\r\n\n") or
+ std.mem.startsWith(u8, two, "\n\r\n") or
+ std.mem.startsWith(u8, two, "\r\n\r\n"));
+
+ if (blankLine1 or blankLine2) {
+ // Five points for blank lines.
+ return 5;
+ } else if (lineBreak1 or lineBreak2) {
+ // Four points for line breaks.
+ return 4;
+ } else if (nonAlphaNumeric1 and !whitespace1 and whitespace2) {
+ // Three points for end of sentences.
+ return 3;
+ } else if (whitespace1 or whitespace2) {
+ // Two points for whitespace.
+ return 2;
+ } else if (nonAlphaNumeric1 or nonAlphaNumeric2) {
+ // One point for non-alphanumeric.
+ return 1;
+ }
+ return 0;
+}
+
+// Define some regex patterns for matching boundaries.
+// private Regex BLANKLINEEND = new Regex("\\n\\r?\\n\\Z");
+// \n\n
+// \n\r\n
+// private Regex BLANKLINESTART = new Regex("\\A\\r?\\n\\r?\\n");
+// \n\n
+// \r\n\n
+// \n\r\n
+// \r\n\r\n
+
+/// Reduce the number of edits by eliminating operationally trivial
+/// equalities.
+pub fn diffCleanupEfficiency(
+ dmp: DiffMatchPatch,
+ allocator: std.mem.Allocator,
+ diffs: *DiffList,
+) DiffError!void {
+ var changes = false;
+ // Stack of indices where equalities are found.
+ var equalities = DiffList{};
+ // Always equal to equalities[equalitiesLength-1][1]
+ var last_equality = "";
+ var pointer: isize = 0; // Index of current position.
+ // Is there an insertion operation before the last equality.
+ var pre_ins = false;
+ // Is there a deletion operation before the last equality.
+ var pre_del = false;
+ // Is there an insertion operation after the last equality.
+ var post_ins = false;
+ // Is there a deletion operation after the last equality.
+ var post_del = false;
+ while (pointer < diffs.Count) {
+ if (diffs.items[pointer].operation == .equal) { // Equality found.
+ if (diffs.items[pointer].text.len < dmp.diff_edit_cost and (post_ins or post_del)) {
+ // Candidate found.
+ equalities.Push(pointer);
+ pre_ins = post_ins;
+ pre_del = post_del;
+ last_equality = diffs.items[pointer].text;
+ } else {
+ // Not a candidate, and can never become one.
+ equalities.items.len = 0;
+ last_equality = "";
+ }
+ post_ins = false;
+ post_del = false;
+ } else { // An insertion or deletion.
+ if (diffs.items[pointer].operation == .delete) {
+ post_del = true;
+ } else {
+ post_ins = true;
+ }
+ // Five types to be split:
+ // <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+ // <ins>A</ins>X<ins>C</ins><del>D</del>
+ // <ins>A</ins><del>B</del>X<ins>C</ins>
+ // <ins>A</del>X<ins>C</ins><del>D</del>
+ // <ins>A</ins><del>B</del>X<del>C</del>
+ if ((last_equality.Length != 0) and
+ ((pre_ins and pre_del and post_ins and post_del) or
+ ((last_equality.Length < dmp.diff_edit_cost / 2) and
+ ((if (pre_ins) 1 else 0) + (if (pre_del) 1 else 0) + (if (post_ins) 1 else 0) + (if (post_del) 1 else 0)) == 3)))
+ {
+ // Duplicate record.
+ try diffs.insert(
+ allocator,
+ equalities.items[equalities.items.len - 1],
+ Diff.init(.delete, try allocator.dupe(u8, last_equality)),
+ );
+ // Change second copy to insert.
+ diffs.items[equalities.items[equalities.items.len - 1] + 1].operation = .insert;
+ _ = equalities.pop(); // Throw away the equality we just deleted.
+ last_equality = "";
+ if (pre_ins and pre_del) {
+ // No changes made which could affect previous entry, keep going.
+ post_ins = true;
+ post_del = true;
+ equalities.items.len = 0;
+ } else {
+ if (equalities.items.len > 0) {
+ _ = equalities.pop();
+ }
+
+ pointer = if (equalities.items.len > 0) equalities.items[equalities.items.len - 1] else -1;
+ post_ins = false;
+ post_del = false;
+ }
+ changes = true;
+ }
+ }
+ pointer += 1;
+ }
+
+ if (changes) {
+ try diffCleanupMerge(allocator, diffs);
+ }
+}
+
+/// Determine if the suffix of one string is the prefix of another.
+/// @param text1 First string.
+/// @param text2 Second string.
+/// @return The number of characters common to the end of the first
+/// string and the start of the second string.
+fn diffCommonOverlap(text1_in: []const u8, text2_in: []const u8) usize {
+ var text1 = text1_in;
+ var text2 = text2_in;
+
+ // Cache the text lengths to prevent multiple calls.
+ var text1_length = text1.len;
+ var text2_length = text2.len;
+ // Eliminate the null case.
+ if (text1_length == 0 or text2_length == 0) {
+ return 0;
+ }
+ // Truncate the longer string.
+ if (text1_length > text2_length) {
+ text1 = text1[text1_length - text2_length ..];
+ } else if (text1_length < text2_length) {
+ text2 = text2[0..text1_length];
+ }
+ const text_length = @min(text1_length, text2_length);
+ // Quick check for the worst case.
+ if (std.mem.eql(u8, text1, text2)) {
+ return text_length;
+ }
+
+ // Start by looking for a single character match
+ // and increase length until no match is found.
+ // Performance analysis: https://neil.fraser.name/news/2010/11/04/
+ var best: usize = 0;
+ var length: usize = 1;
+ while (true) {
+ const pattern = text1[text_length - length ..];
+ const found = std.mem.indexOf(u8, text2, pattern) orelse
+ return best;
+
+ length += found;
+
+ if (found == 0 or std.mem.eql(u8, text1[text_length - length ..], text2[0..length])) {
+ best = length;
+ length += 1;
+ }
+ }
+}
+
+// pub fn main() void {
+// var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+// defer arena.deinit();
+
+// var bruh = default.diff(arena.allocator(), "Hello World.", "Goodbye World.", true);
+// std.log.err("{any}", .{bruh});
+// }
+
+// test {
+// var arena = std.heap.ArenaAllocator.init(testing.allocator);
+// defer arena.deinit();
+
+// var bruh = try default.diff(arena.allocator(), "Hello World.", "Goodbye World.", true);
+// try diffCleanupSemantic(arena.allocator(), &bruh);
+// for (bruh.items) |b| {
+// std.log.err("{any}", .{b});
+// }
+
+// // for (bruh.items) |b| {
+// // std.log.err("{s} {s}", .{ switch (b.operation) {
+// // .equal => "",
+// // .insert => "+",
+// // .delete => "-",
+// // }, b.text });
+// // }
+// }
+
+// TODO: Allocate all text in diffs to
+// not cause segfault while freeing; not a problem
+// at the moment because we don't free anything :P
+
+test diffCommonPrefix {
+ // Detect any common suffix.
+ try testing.expectEqual(@as(usize, 0), diffCommonPrefix("abc", "xyz")); // Null case
+ try testing.expectEqual(@as(usize, 4), diffCommonPrefix("1234abcdef", "1234xyz")); // Non-null case
+ try testing.expectEqual(@as(usize, 4), diffCommonPrefix("1234", "1234xyz")); // Whole case
+}
+
+test diffCommonSuffix {
+ // Detect any common suffix.
+ try testing.expectEqual(@as(usize, 0), diffCommonSuffix("abc", "xyz")); // Null case
+ try testing.expectEqual(@as(usize, 4), diffCommonSuffix("abcdef1234", "xyz1234")); // Non-null case
+ try testing.expectEqual(@as(usize, 4), diffCommonSuffix("1234", "xyz1234")); // Whole case
+}
+
+test diffCommonOverlap {
+ // Detect any suffix/prefix overlap.
+ try testing.expectEqual(@as(usize, 0), diffCommonOverlap("", "abcd")); // Null case
+ try testing.expectEqual(@as(usize, 3), diffCommonOverlap("abc", "abcd")); // Whole case
+ try testing.expectEqual(@as(usize, 0), diffCommonOverlap("123456", "abcd")); // No overlap
+ try testing.expectEqual(@as(usize, 3), diffCommonOverlap("123456xxx", "xxxabcd")); // Overlap
+
+ // Some overly clever languages (C#) may treat ligatures as equal to their
+ // component letters. E.g. U+FB01 == 'fi'
+ try testing.expectEqual(@as(usize, 0), diffCommonOverlap("fi", "\u{fb01}")); // Unicode
+}
+
+test diffHalfMatch {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ var one_timeout = DiffMatchPatch{};
+ one_timeout.diff_timeout = 1;
+
+ try testing.expectEqual(
+ @as(?HalfMatchResult, null),
+ try one_timeout.diffHalfMatch(arena.allocator(), "1234567890", "abcdef"),
+ ); // No match #1
+ try testing.expectEqual(
+ @as(?HalfMatchResult, null),
+ try one_timeout.diffHalfMatch(arena.allocator(), "12345", "23"),
+ ); // No match #2
+
+ // Single matches
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "12",
+ .suffix_before = "90",
+ .prefix_after = "a",
+ .suffix_after = "z",
+ .common_middle = "345678",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "1234567890", "a345678z")); // Single Match #1
+
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "a",
+ .suffix_before = "z",
+ .prefix_after = "12",
+ .suffix_after = "90",
+ .common_middle = "345678",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "a345678z", "1234567890")); // Single Match #2
+
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "abc",
+ .suffix_before = "z",
+ .prefix_after = "1234",
+ .suffix_after = "0",
+ .common_middle = "56789",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "abc56789z", "1234567890")); // Single Match #3
+
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "a",
+ .suffix_before = "xyz",
+ .prefix_after = "1",
+ .suffix_after = "7890",
+ .common_middle = "23456",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "a23456xyz", "1234567890")); // Single Match #4
+
+ // Multiple matches
+ try testing.expectEqualDeep(
+ @as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "12123",
+ .suffix_before = "123121",
+ .prefix_after = "a",
+ .suffix_after = "z",
+ .common_middle = "1234123451234",
+ }),
+ try one_timeout.diffHalfMatch(arena.allocator(), "121231234123451234123121", "a1234123451234z"),
+ ); // Multiple Matches #1
+
+ try testing.expectEqualDeep(
+ @as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "",
+ .suffix_before = "-=-=-=-=-=",
+ .prefix_after = "x",
+ .suffix_after = "",
+ .common_middle = "x-=-=-=-=-=-=-=",
+ }),
+ try one_timeout.diffHalfMatch(arena.allocator(), "x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-="),
+ ); // Multiple Matches #2
+
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "-=-=-=-=-=",
+ .suffix_before = "",
+ .prefix_after = "",
+ .suffix_after = "y",
+ .common_middle = "-=-=-=-=-=-=-=y",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); // Multiple Matches #3
+
+ // Other cases
+ // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy
+ try testing.expectEqualDeep(@as(?HalfMatchResult, HalfMatchResult{
+ .prefix_before = "qHillo",
+ .suffix_before = "w",
+ .prefix_after = "x",
+ .suffix_after = "Hulloy",
+ .common_middle = "HelloHe",
+ }), try one_timeout.diffHalfMatch(arena.allocator(), "qHilloHelloHew", "xHelloHeHulloy")); // Non-optimal halfmatch
+
+ one_timeout.diff_timeout = 0;
+ try testing.expectEqualDeep(@as(?HalfMatchResult, null), try one_timeout.diffHalfMatch(arena.allocator(), "qHilloHelloHew", "xHelloHeHulloy")); // Non-optimal halfmatch
+}
+
+test diffLinesToChars {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ // Convert lines down to characters.
+ var tmp_array_list = std.ArrayList([]const u8).init(arena.allocator());
+ try tmp_array_list.append("");
+ try tmp_array_list.append("alpha\n");
+ try tmp_array_list.append("beta\n");
+
+ var result = try diffLinesToChars(arena.allocator(), "alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n");
+ try testing.expectEqualStrings("\u{0001}\u{0002}\u{0001}", result.chars_1); // Shared lines #1
+ try testing.expectEqualStrings("\u{0002}\u{0001}\u{0002}", result.chars_2); // Shared lines #2
+ try testing.expectEqualDeep(tmp_array_list.items, result.line_array.items); // Shared lines #3
+
+ tmp_array_list.items.len = 0;
+ try tmp_array_list.append("");
+ try tmp_array_list.append("alpha\r\n");
+ try tmp_array_list.append("beta\r\n");
+ try tmp_array_list.append("\r\n");
+
+ result = try diffLinesToChars(arena.allocator(), "", "alpha\r\nbeta\r\n\r\n\r\n");
+ try testing.expectEqualStrings("", result.chars_1); // Empty string and blank lines #1
+ try testing.expectEqualStrings("\u{0001}\u{0002}\u{0003}\u{0003}", result.chars_2); // Empty string and blank lines #2
+ try testing.expectEqualDeep(tmp_array_list.items, result.line_array.items); // Empty string and blank lines #3
+
+ tmp_array_list.items.len = 0;
+ try tmp_array_list.append("");
+ try tmp_array_list.append("a");
+ try tmp_array_list.append("b");
+
+ result = try diffLinesToChars(arena.allocator(), "a", "b");
+ try testing.expectEqualStrings("\u{0001}", result.chars_1); // No linebreaks #1.
+ try testing.expectEqualStrings("\u{0002}", result.chars_2); // No linebreaks #2.
+ try testing.expectEqualDeep(tmp_array_list.items, result.line_array.items); // No linebreaks #3.
+
+ // TODO: More than 256 to reveal any 8-bit limitations but this requires
+ // some unicode logic that I don't want to deal with
+
+ // TODO: Fix this
+
+ // const n: u8 = 255;
+ // tmp_array_list.items.len = 0;
+
+ // var line_list = std.ArrayList(u8).init(arena.allocator());
+ // var char_list = std.ArrayList(u8).init(arena.allocator());
+
+ // var i: u8 = 0;
+ // while (i < n) : (i += 1) {
+ // try tmp_array_list.append(&.{ i, '\n' });
+ // try line_list.appendSlice(&.{ i, '\n' });
+ // try char_list.append(i);
+ // }
+ // try testing.expectEqual(@as(usize, n), tmp_array_list.items.len); // Test initialization fail #1
+ // try testing.expectEqual(@as(usize, n), char_list.items.len); // Test initialization fail #2
+ // try tmp_array_list.insert(0, "");
+ // result = try diffLinesToChars(arena.allocator(), line_list.items, "");
+ // try testing.expectEqualStrings(char_list.items, result.chars_1);
+ // try testing.expectEqualStrings("", result.chars_2);
+ // try testing.expectEqualDeep(tmp_array_list.items, result.line_array.items);
+}
+
+test diffCharsToLines {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ try testing.expect((Diff.init(.equal, "a")).eql(Diff.init(.equal, "a")));
+ try testing.expect(!(Diff.init(.insert, "a")).eql(Diff.init(.equal, "a")));
+ try testing.expect(!(Diff.init(.equal, "a")).eql(Diff.init(.equal, "b")));
+ try testing.expect(!(Diff.init(.equal, "a")).eql(Diff.init(.delete, "b")));
+
+ // Convert chars up to lines.
+ var diffs = std.ArrayList(Diff).init(arena.allocator());
+ try diffs.appendSlice(&.{
+ Diff{ .operation = .equal, .text = try arena.allocator().dupe(u8, "\u{0001}\u{0002}\u{0001}") },
+ Diff{ .operation = .insert, .text = try arena.allocator().dupe(u8, "\u{0002}\u{0001}\u{0002}") },
+ });
+ var tmp_vector = std.ArrayList([]const u8).init(arena.allocator());
+ try tmp_vector.append("");
+ try tmp_vector.append("alpha\n");
+ try tmp_vector.append("beta\n");
+ try diffCharsToLines(arena.allocator(), diffs.items, tmp_vector.items);
+
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ Diff.init(.equal, "alpha\nbeta\nalpha\n"),
+ Diff.init(.insert, "beta\nalpha\nbeta\n"),
+ }), diffs.items);
+
+ // TODO: Implement exhaustive tests
+}
+
+test diffCleanupMerge {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ // Cleanup a messy diff.
+ var diffs = DiffList{};
+ try testing.expectEqualDeep(@as([]const Diff, &[0]Diff{}), diffs.items); // Null case
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .equal, .text = "a" },
+ .{ .operation = .delete, .text = "b" },
+ .{ .operation = .insert, .text = "c" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .equal, .text = "a" },
+ .{ .operation = .delete, .text = "b" },
+ .{ .operation = .insert, .text = "c" },
+ }), diffs.items); // No change case
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .equal, .text = "a" },
+ .{ .operation = .equal, .text = "b" },
+ .{ .operation = .equal, .text = "c" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .equal, .text = "abc" },
+ }), diffs.items); // Merge equalities
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .delete, .text = "a" },
+ .{ .operation = .delete, .text = "b" },
+ .{ .operation = .delete, .text = "c" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .delete, .text = "abc" },
+ }), diffs.items); // Merge deletions
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .insert, .text = "a" },
+ .{ .operation = .insert, .text = "b" },
+ .{ .operation = .insert, .text = "c" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .insert, .text = "abc" },
+ }), diffs.items); // Merge insertions
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .delete, .text = "a" },
+ .{ .operation = .insert, .text = "b" },
+ .{ .operation = .delete, .text = "c" },
+ .{ .operation = .insert, .text = "d" },
+ .{ .operation = .equal, .text = "e" },
+ .{ .operation = .equal, .text = "f" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .delete, .text = "ac" },
+ .{ .operation = .insert, .text = "bd" },
+ .{ .operation = .equal, .text = "ef" },
+ }), diffs.items); // Merge interweave
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .delete, .text = "a" },
+ .{ .operation = .insert, .text = "abc" },
+ .{ .operation = .delete, .text = "dc" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .equal, .text = "a" },
+ .{ .operation = .delete, .text = "d" },
+ .{ .operation = .insert, .text = "b" },
+ .{ .operation = .equal, .text = "c" },
+ }), diffs.items); // Prefix and suffix detection
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .equal, .text = "x" },
+ .{ .operation = .delete, .text = "a" },
+ .{ .operation = .insert, .text = "abc" },
+ .{ .operation = .delete, .text = "dc" },
+ .{ .operation = .equal, .text = "y" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .equal, .text = "xa" },
+ .{ .operation = .delete, .text = "d" },
+ .{ .operation = .insert, .text = "b" },
+ .{ .operation = .equal, .text = "cy" },
+ }), diffs.items); // Prefix and suffix detection with equalities
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .equal, .text = "a" },
+ .{ .operation = .insert, .text = "ba" },
+ .{ .operation = .equal, .text = "c" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .insert, .text = "ab" },
+ .{ .operation = .equal, .text = "ac" },
+ }), diffs.items); // Slide edit left
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ .{ .operation = .equal, .text = "c" },
+ .{ .operation = .insert, .text = "ab" },
+ .{ .operation = .equal, .text = "a" },
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ .{ .operation = .equal, .text = "ca" },
+ .{ .operation = .insert, .text = "ba" },
+ }), diffs.items); // Slide edit right
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ Diff.init(.equal, "a"),
+ Diff.init(.delete, "b"),
+ Diff.init(.equal, "c"),
+ Diff.init(.delete, "ac"),
+ Diff.init(.equal, "x"),
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ Diff.init(.delete, "abc"),
+ Diff.init(.equal, "acx"),
+ }), diffs.items); // Slide edit left recursive
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ Diff.init(.equal, "x"),
+ Diff.init(.delete, "ca"),
+ Diff.init(.equal, "c"),
+ Diff.init(.delete, "b"),
+ Diff.init(.equal, "a"),
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ Diff.init(.equal, "xca"),
+ Diff.init(.delete, "cba"),
+ }), diffs.items); // Slide edit right recursive
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ Diff.init(.delete, "b"),
+ Diff.init(.insert, "ab"),
+ Diff.init(.equal, "c"),
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ Diff.init(.insert, "a"),
+ Diff.init(.equal, "bc"),
+ }), diffs.items); // Empty merge
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &[_]Diff{
+ Diff.init(.equal, ""),
+ Diff.init(.insert, "a"),
+ Diff.init(.equal, "b"),
+ });
+ try diffCleanupMerge(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{
+ Diff.init(.insert, "a"),
+ Diff.init(.equal, "b"),
+ }), diffs.items); // Empty equality
+}
+
+test diffCleanupSemanticLossless {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ var diffs = DiffList{};
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[0]Diff{}), diffs.items); // Null case
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "AAA\r\n\r\nBBB"),
+ Diff.init(.insert, "\r\nDDD\r\n\r\nBBB"),
+ Diff.init(.equal, "\r\nEEE"),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "AAA\r\n\r\n"),
+ Diff.init(.insert, "BBB\r\nDDD\r\n\r\n"),
+ Diff.init(.equal, "BBB\r\nEEE"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "AAA\r\nBBB"),
+ Diff.init(.insert, " DDD\r\nBBB"),
+ Diff.init(.equal, " EEE"),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "AAA\r\n"),
+ Diff.init(.insert, "BBB DDD\r\n"),
+ Diff.init(.equal, "BBB EEE"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "The c"),
+ Diff.init(.insert, "ow and the c"),
+ Diff.init(.equal, "at."),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "The "),
+ Diff.init(.insert, "cow and the "),
+ Diff.init(.equal, "cat."),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "The-c"),
+ Diff.init(.insert, "ow-and-the-c"),
+ Diff.init(.equal, "at."),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "The-"),
+ Diff.init(.insert, "cow-and-the-"),
+ Diff.init(.equal, "cat."),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "a"),
+ Diff.init(.delete, "a"),
+ Diff.init(.equal, "ax"),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.delete, "a"),
+ Diff.init(.equal, "aax"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "xa"),
+ Diff.init(.delete, "a"),
+ Diff.init(.equal, "a"),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "xaa"),
+ Diff.init(.delete, "a"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "The xxx. The "),
+ Diff.init(.insert, "zzz. The "),
+ Diff.init(.equal, "yyy."),
+ });
+ try diffCleanupSemanticLossless(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &.{
+ Diff.init(.equal, "The xxx."),
+ Diff.init(.insert, " The zzz."),
+ Diff.init(.equal, " The yyy."),
+ }), diffs.items);
+}
+
+fn rebuildtexts(allocator: std.mem.Allocator, diffs: DiffList) ![2][]const u8 {
+ var text = [2]std.ArrayList(u8){
+ std.ArrayList(u8).init(allocator),
+ std.ArrayList(u8).init(allocator),
+ };
+
+ for (diffs.items) |myDiff| {
+ if (myDiff.operation != .insert) {
+ try text[0].appendSlice(myDiff.text);
+ }
+ if (myDiff.operation != .delete) {
+ try text[1].appendSlice(myDiff.text);
+ }
+ }
+ return .{
+ try text[0].toOwnedSlice(),
+ try text[1].toOwnedSlice(),
+ };
+}
+
+test diffBisect {
+ var arena = std.heap.ArenaAllocator.init(talloc);
+ defer arena.deinit();
+
+ // Normal.
+ const a = "cat";
+ const b = "map";
+ // Since the resulting diff hasn't been normalized, it would be ok if
+ // the insertion and deletion pairs are swapped.
+ // If the order changes, tweak this test as required.
+ var diffs = DiffList{};
+ defer diffs.deinit(arena.allocator());
+ var this = default;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "c"),
+ Diff.init(.insert, "m"),
+ Diff.init(.equal, "a"),
+ Diff.init(.delete, "t"),
+ Diff.init(.insert, "p"),
+ });
+ // Travis TODO not sure if maxInt(u64) is correct for DateTime.MaxValue
+ try testing.expectEqualDeep(diffs, try this.diffBisect(arena.allocator(), a, b, std.math.maxInt(u64))); // Normal.
+
+ // Timeout.
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "cat"),
+ Diff.init(.insert, "map"),
+ });
+ // Travis TODO not sure if 0 is correct for DateTime.MinValue
+ try testing.expectEqualDeep(diffs, try this.diffBisect(arena.allocator(), a, b, 0)); // Timeout.
+}
+
+const talloc = testing.allocator;
+test diff {
+ var arena = std.heap.ArenaAllocator.init(talloc);
+ defer arena.deinit();
+
+ // Perform a trivial diff.
+ var diffs = DiffList{};
+ defer diffs.deinit(arena.allocator());
+ var this = DiffMatchPatch{};
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "", "", false)).items); // diff: Null case.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{Diff.init(.equal, "abc")});
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "abc", "abc", false)).items); // diff: Equality.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.equal, "ab"), Diff.init(.insert, "123"), Diff.init(.equal, "c") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "abc", "ab123c", false)).items); // diff: Simple insertion.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.equal, "a"), Diff.init(.delete, "123"), Diff.init(.equal, "bc") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "a123bc", "abc", false)).items); // diff: Simple deletion.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.equal, "a"), Diff.init(.insert, "123"), Diff.init(.equal, "b"), Diff.init(.insert, "456"), Diff.init(.equal, "c") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "abc", "a123b456c", false)).items); // diff: Two insertions.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.equal, "a"), Diff.init(.delete, "123"), Diff.init(.equal, "b"), Diff.init(.delete, "456"), Diff.init(.equal, "c") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "a123b456c", "abc", false)).items); // diff: Two deletions.
+
+ // Perform a real diff.
+ // Switch off the timeout.
+ this.diff_timeout = 0;
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.delete, "a"), Diff.init(.insert, "b") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "a", "b", false)).items); // diff: Simple case #1.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.delete, "Apple"), Diff.init(.insert, "Banana"), Diff.init(.equal, "s are a"), Diff.init(.insert, "lso"), Diff.init(.equal, " fruit.") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "Apples are a fruit.", "Bananas are also fruit.", false)).items); // diff: Simple case #2.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.delete, "a"), Diff.init(.insert, "\u{0680}"), Diff.init(.equal, "x"), Diff.init(.delete, "\t"), Diff.init(.insert, "\x00") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "ax\t", "\u{0680}x\x00", false)).items); // diff: Simple case #3.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.delete, "1"), Diff.init(.equal, "a"), Diff.init(.delete, "y"), Diff.init(.equal, "b"), Diff.init(.delete, "2"), Diff.init(.insert, "xab") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "1ayb2", "abxab", false)).items); // diff: Overlap #1.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.insert, "xaxcx"), Diff.init(.equal, "abc"), Diff.init(.delete, "y") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "abcy", "xaxcxabc", false)).items); // diff: Overlap #2.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.delete, "ABCD"), Diff.init(.equal, "a"), Diff.init(.delete, "="), Diff.init(.insert, "-"), Diff.init(.equal, "bcd"), Diff.init(.delete, "="), Diff.init(.insert, "-"), Diff.init(.equal, "efghijklmnopqrs"), Diff.init(.delete, "EFGHIJKLMNOefg") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false)).items); // diff: Overlap #3.
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{ Diff.init(.insert, " "), Diff.init(.equal, "a"), Diff.init(.insert, "nd"), Diff.init(.equal, " [[Pennsylvania]]"), Diff.init(.delete, " and [[New") });
+ try testing.expectEqualDeep(diffs.items, (try this.diff(arena.allocator(), "a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false)).items); // diff: Large equality.
+
+ this.diff_timeout = 100; // 100ms
+ // Increase the text lengths by 1024 times to ensure a timeout.
+ {
+ const a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n" ** 1024;
+ const b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n" ** 1024;
+ const start_time = std.time.milliTimestamp();
+ _ = try this.diff(arena.allocator(), a, b, false); // Travis - TODO not sure what the third arg should be
+ const end_time = std.time.milliTimestamp();
+ // Test that we took at least the timeout period.
+ try testing.expect(this.diff_timeout <= end_time - start_time); // diff: Timeout min.
+ // Test that we didn't take forever (be forgiving).
+ // Theoretically this test could fail very occasionally if the
+ // OS task swaps or locks up for a second at the wrong moment.
+ try testing.expect((this.diff_timeout) * 10000 * 2 > end_time - start_time); // diff: Timeout max.
+ this.diff_timeout = 0;
+ }
+ {
+ // Test the linemode speedup.
+ // Must be long to pass the 100 char cutoff.
+ const a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n";
+ const b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n";
+ try testing.expectEqualDeep(try this.diff(arena.allocator(), a, b, true), try this.diff(arena.allocator(), a, b, false)); // diff: Simple line-mode.
+ }
+ {
+ const a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
+ const b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij";
+ try testing.expectEqualDeep(try this.diff(arena.allocator(), a, b, true), try this.diff(arena.allocator(), a, b, false)); // diff: Single line-mode.
+ }
+
+ const a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n";
+ const b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n";
+ const texts_linemode = try rebuildtexts(arena.allocator(), try this.diff(arena.allocator(), a, b, true));
+ defer {
+ arena.allocator().free(texts_linemode[0]);
+ arena.allocator().free(texts_linemode[1]);
+ }
+ const texts_textmode = try rebuildtexts(arena.allocator(), try this.diff(arena.allocator(), a, b, false));
+ defer {
+ arena.allocator().free(texts_textmode[0]);
+ arena.allocator().free(texts_textmode[1]);
+ }
+ try testing.expectEqualDeep(texts_textmode, texts_linemode); // diff: Overlap line-mode.
+
+ // Test null inputs -- not needed because nulls can't be passed in C#.
+}
+
+test diffCleanupSemantic {
+ var arena = std.heap.ArenaAllocator.init(talloc);
+ defer arena.deinit();
+
+ // Cleanup semantically trivial equalities.
+ // Null case.
+ var diffs = DiffList{};
+ defer diffs.deinit(arena.allocator());
+ // var this = default;
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqual(@as(usize, 0), diffs.items.len); // Null case
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "ab"),
+ Diff.init(.insert, "cd"),
+ Diff.init(.equal, "12"),
+ Diff.init(.delete, "e"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // No elimination #1
+ Diff.init(.delete, "ab"),
+ Diff.init(.insert, "cd"),
+ Diff.init(.equal, "12"),
+ Diff.init(.delete, "e"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "abc"),
+ Diff.init(.insert, "ABC"),
+ Diff.init(.equal, "1234"),
+ Diff.init(.delete, "wxyz"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // No elimination #2
+ Diff.init(.delete, "abc"),
+ Diff.init(.insert, "ABC"),
+ Diff.init(.equal, "1234"),
+ Diff.init(.delete, "wxyz"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "a"),
+ Diff.init(.equal, "b"),
+ Diff.init(.delete, "c"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Simple elimination
+ Diff.init(.delete, "abc"),
+ Diff.init(.insert, "b"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "ab"),
+ Diff.init(.equal, "cd"),
+ Diff.init(.delete, "e"),
+ Diff.init(.equal, "f"),
+ Diff.init(.insert, "g"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Backpass elimination
+ Diff.init(.delete, "abcdef"),
+ Diff.init(.insert, "cdfg"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.insert, "1"),
+ Diff.init(.equal, "A"),
+ Diff.init(.delete, "B"),
+ Diff.init(.insert, "2"),
+ Diff.init(.equal, "_"),
+ Diff.init(.insert, "1"),
+ Diff.init(.equal, "A"),
+ Diff.init(.delete, "B"),
+ Diff.init(.insert, "2"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Multiple elimination
+ Diff.init(.delete, "AB_AB"),
+ Diff.init(.insert, "1A2_1A2"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.equal, "The c"),
+ Diff.init(.delete, "ow and the c"),
+ Diff.init(.equal, "at."),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Word boundaries
+ Diff.init(.equal, "The "),
+ Diff.init(.delete, "cow and the "),
+ Diff.init(.equal, "cat."),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "abcxx"),
+ Diff.init(.insert, "xxdef"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // No overlap elimination
+ Diff.init(.delete, "abcxx"),
+ Diff.init(.insert, "xxdef"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "abcxxx"),
+ Diff.init(.insert, "xxxdef"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Overlap elimination
+ Diff.init(.delete, "abc"),
+ Diff.init(.equal, "xxx"),
+ Diff.init(.insert, "def"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "xxxabc"),
+ Diff.init(.insert, "defxxx"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Reverse overlap elimination
+ Diff.init(.insert, "def"),
+ Diff.init(.equal, "xxx"),
+ Diff.init(.delete, "abc"),
+ }), diffs.items);
+
+ diffs.items.len = 0;
+ try diffs.appendSlice(arena.allocator(), &.{
+ Diff.init(.delete, "abcd1212"),
+ Diff.init(.insert, "1212efghi"),
+ Diff.init(.equal, "----"),
+ Diff.init(.delete, "A3"),
+ Diff.init(.insert, "3BC"),
+ });
+ try diffCleanupSemantic(arena.allocator(), &diffs);
+ try testing.expectEqualDeep(@as([]const Diff, &[_]Diff{ // Two overlap eliminations
+ Diff.init(.delete, "abcd"),
+ Diff.init(.equal, "1212"),
+ Diff.init(.insert, "efghi"),
+ Diff.init(.equal, "----"),
+ Diff.init(.delete, "A"),
+ Diff.init(.equal, "3"),
+ Diff.init(.insert, "BC"),
+ }), diffs.items);
+}
diff --git a/src/output.zig b/src/output.zig
index ec073baa1..e46e824e0 100644
--- a/src/output.zig
+++ b/src/output.zig
@@ -458,9 +458,11 @@ pub fn prettyFmt(comptime fmt: string, comptime is_enabled: bool) string {
switch (c) {
'\\' => {
i += 1;
- if (fmt.len < i) {
+ if (i < fmt.len) {
switch (fmt[i]) {
'<', '>' => {
+ new_fmt[new_fmt_i] = fmt[i];
+ new_fmt_i += 1;
i += 1;
},
else => {
@@ -468,6 +470,7 @@ pub fn prettyFmt(comptime fmt: string, comptime is_enabled: bool) string {
new_fmt_i += 1;
new_fmt[new_fmt_i] = fmt[i];
new_fmt_i += 1;
+ i += 1;
},
}
}
diff --git a/test/bun.js/install/bun-link.test.ts b/test/bun.js/install/bun-link.test.ts
index b892c00e0..c2cf459e2 100644
--- a/test/bun.js/install/bun-link.test.ts
+++ b/test/bun.js/install/bun-link.test.ts
@@ -50,7 +50,7 @@ it("should link package", async () => {
const err1 = await new Response(stderr1).text();
expect(err1.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]);
expect(stdout1).toBeDefined();
- expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`);
+ expect(await new Response(stdout1).text()).toContain(`Success! Registered \\"${link_name}\\"`);
expect(await exited1).toBe(0);
const {
@@ -152,7 +152,7 @@ it("should link scoped package", async () => {
const err1 = await new Response(stderr1).text();
expect(err1.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]);
expect(stdout1).toBeDefined();
- expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`);
+ expect(await new Response(stdout1).text()).toContain(`Success! Registered \\"${link_name}\\"`);
expect(await exited1).toBe(0);
const {
diff --git a/test/bun.js/test-test.test.ts b/test/bun.js/test-test.test.ts
index 00a760abf..06573cc7d 100644
--- a/test/bun.js/test-test.test.ts
+++ b/test/bun.js/test-test.test.ts
@@ -239,11 +239,9 @@ test("deepEquals throw getters", () => {
}
}
- try {
+ expect(() => {
expect(new B()).not.toEqual(new C());
- } catch (e) {
- expect(e.message).toContain("b");
- }
+ }).toThrow();
let o = [
{