test: get zig build test working (#18207)

### What does this PR do?
Lets us write and run unit tests directly in Zig.

Running Zig unit tests in CI is blocked by https://github.com/ziglang/zig/issues/23281. We can un-comment relevant code once this is fixed.

#### Workflow
> I'll finish writing this up later, but some initial points are below.
> Tl;Dr: `bun build:test`

Test binaries can be made for any kind of build. They are called `<bun>-test` and live next to their corresponding `bun` bin. For example, debug tests compile to `build/debug/bun-debug-test`.

Test binaries re-use most cmake/zig build steps from normal bun binaries, so building one after a normal bun build is pretty fast.

### How did you verify your code works?
I tested that my tests run tests.
This commit is contained in:
Don Isaac
2025-04-08 15:31:53 -07:00
committed by GitHub
parent d028e1aaa3
commit dff1f555b4
26 changed files with 708 additions and 2371 deletions

View File

@@ -285,6 +285,40 @@ pub fn build(b: *Build) !void {
step.dependOn(addInstallObjectFile(b, bun_obj, "bun-zig", obj_format));
}
// zig build test
{
var step = b.step("test", "Build Bun's unit test suite");
var o = build_options;
var unit_tests = b.addTest(.{
.name = "bun-test",
.optimize = build_options.optimize,
.root_source_file = b.path("src/unit_test.zig"),
.test_runner = .{ .path = b.path("src/main_test.zig"), .mode = .simple },
.target = build_options.target,
.use_llvm = !build_options.no_llvm,
.use_lld = if (build_options.os == .mac) false else !build_options.no_llvm,
.omit_frame_pointer = false,
.strip = false,
});
configureObj(b, &o, unit_tests);
// Setting `linker_allow_shlib_undefined` causes the linker to ignore
// all undefined symbols. We want this because all we care about is the
// object file Zig creates; we perform our own linking later. There is
// currently no way to make a test build that only creates an object
// file w/o creating an executable.
//
// See: https://github.com/ziglang/zig/issues/23374
unit_tests.linker_allow_shlib_undefined = true;
unit_tests.link_function_sections = true;
unit_tests.link_data_sections = true;
unit_tests.bundle_ubsan_rt = false;
const bin = unit_tests.getEmittedBin();
const obj = bin.dirname().path(b, "bun-test.o");
const cpy_obj = b.addInstallFile(obj, "bun-test.o");
step.dependOn(&cpy_obj.step);
}
// zig build windows-shim
{
var step = b.step("windows-shim", "Build the Windows shim (bun_shim_impl.exe + bun_shim_debug.exe)");
@@ -456,6 +490,11 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile {
.omit_frame_pointer = false,
.strip = false, // stripped at the end
});
configureObj(b, opts, obj);
return obj;
}
fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void {
if (opts.enable_asan) {
if (@hasField(Build.Module, "sanitize_address")) {
obj.root_module.sanitize_address = true;
@@ -495,8 +534,6 @@ pub fn addBunObject(b: *Build, opts: *BunBuildOptions) *Compile {
const translate_c = getTranslateC(b, opts.target, opts.optimize);
obj.root_module.addImport("translated-c-headers", translate_c.createModule());
return obj;
}
const ObjectFormat = enum {

View File

@@ -26,6 +26,15 @@ else()
setx(DEBUG OFF)
endif()
optionx(BUN_TEST BOOL "Build Bun's unit test suite instead of the normal build" DEFAULT OFF)
if (BUN_TEST)
setx(TEST ON)
else()
setx(TEST OFF)
endif()
if(CMAKE_BUILD_TYPE MATCHES "MinSizeRel")
setx(ENABLE_SMOL ON)
endif()
@@ -62,7 +71,14 @@ if(ARCH STREQUAL "x64")
optionx(ENABLE_BASELINE BOOL "If baseline features should be used for older CPUs (e.g. disables AVX, AVX2)" DEFAULT OFF)
endif()
optionx(ENABLE_LOGS BOOL "If debug logs should be enabled" DEFAULT ${DEBUG})
# Disabling logs by default for tests yields faster builds
if (DEBUG AND NOT TEST)
set(DEFAULT_ENABLE_LOGS ON)
else()
set(DEFAULT_ENABLE_LOGS OFF)
endif()
optionx(ENABLE_LOGS BOOL "If debug logs should be enabled" DEFAULT ${DEFAULT_ENABLE_LOGS})
optionx(ENABLE_ASSERTIONS BOOL "If debug assertions should be enabled" DEFAULT ${DEBUG})
optionx(ENABLE_CANARY BOOL "If canary features should be enabled" DEFAULT ON)

View File

@@ -29,6 +29,9 @@ else()
endif()
set(ZIG_NAME bootstrap-${ZIG_ARCH}-${ZIG_OS_ABI})
if(ZIG_COMPILER_SAFE)
set(ZIG_NAME ${ZIG_NAME}-ReleaseSafe)
endif()
set(ZIG_FILENAME ${ZIG_NAME}.zip)
if(CMAKE_HOST_WIN32)

View File

@@ -12,6 +12,10 @@ else()
set(bunStrip bun)
endif()
if(TEST)
set(bun ${bun}-test)
endif()
set(bunExe ${bun}${CMAKE_EXECUTABLE_SUFFIX})
if(bunStrip)
@@ -528,7 +532,6 @@ file(GLOB_RECURSE BUN_ZIG_SOURCES ${CONFIGURE_DEPENDS}
list(APPEND BUN_ZIG_SOURCES
${CWD}/build.zig
${CWD}/src/main.zig
${BUN_BINDGEN_ZIG_OUTPUTS}
)
@@ -550,7 +553,13 @@ else()
list(APPEND BUN_ZIG_GENERATED_SOURCES ${BUN_BAKE_RUNTIME_OUTPUTS})
endif()
set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-zig.o)
if (TEST)
set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-test.o)
set(ZIG_STEPS test)
else()
set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-zig.o)
set(ZIG_STEPS obj)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64")
if(APPLE)
@@ -579,10 +588,10 @@ register_command(
GROUP
console
COMMENT
"Building src/*.zig for ${ZIG_TARGET}"
"Building src/*.zig into ${BUN_ZIG_OUTPUT} for ${ZIG_TARGET}"
COMMAND
${ZIG_EXECUTABLE}
build obj
build ${ZIG_STEPS}
${CMAKE_ZIG_FLAGS}
--prefix ${BUILD_PATH}
-Dobj_format=${ZIG_OBJECT_FORMAT}
@@ -596,6 +605,7 @@ register_command(
-Dcodegen_path=${CODEGEN_PATH}
-Dcodegen_embed=$<IF:$<BOOL:${CODEGEN_EMBED}>,true,false>
--prominent-compile-errors
--summary all
${ZIG_FLAGS_BUN}
ARTIFACTS
${BUN_ZIG_OUTPUT}

View File

@@ -50,6 +50,7 @@ optionx(ZIG_OBJECT_FORMAT "obj|bc" "Output file format for Zig object files" DEF
optionx(ZIG_LOCAL_CACHE_DIR FILEPATH "The path to local the zig cache directory" DEFAULT ${CACHE_PATH}/zig/local)
optionx(ZIG_GLOBAL_CACHE_DIR FILEPATH "The path to the global zig cache directory" DEFAULT ${CACHE_PATH}/zig/global)
optionx(ZIG_COMPILER_SAFE BOOL "Download a ReleaseSafe build of the Zig compiler. Only availble on macos aarch64." DEFAULT OFF)
setenv(ZIG_LOCAL_CACHE_DIR ${ZIG_LOCAL_CACHE_DIR})
setenv(ZIG_GLOBAL_CACHE_DIR ${ZIG_GLOBAL_CACHE_DIR})
@@ -78,6 +79,7 @@ register_command(
-DZIG_PATH=${ZIG_PATH}
-DZIG_COMMIT=${ZIG_COMMIT}
-DENABLE_ASAN=${ENABLE_ASAN}
-DZIG_COMPILER_SAFE=${ZIG_COMPILER_SAFE}
-P ${CWD}/cmake/scripts/DownloadZig.cmake
SOURCES
${CWD}/cmake/scripts/DownloadZig.cmake

View File

@@ -57,6 +57,9 @@
"test:release": "node scripts/runner.node.mjs --exec-path ./build/release/bun",
"banned": "bun test test/internal/ban-words.test.ts",
"zig": "vendor/zig/zig.exe",
"zig:test": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -DBUN_TEST=ON -B build/debug",
"zig:test:release": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DBUNTEST=ON -B build/release",
"zig:test:ci": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Release -DBUN_TEST=ON -DZIG_OPTIMIZE=ReleaseSafe -DCMAKE_VERBOSE_MAKEFILE=ON -DCI=true -B build/release-ci --verbose --fresh",
"zig:fmt": "bun run zig-format",
"zig:check": "bun run zig build check --summary new",
"zig:check-all": "bun run zig build check-all --summary new",
@@ -76,6 +79,7 @@
"prettier:check": "bun run analysis:no-llvm --target prettier-check",
"prettier:extra": "bun run analysis:no-llvm --target prettier-extra",
"prettier:diff": "bun run analysis:no-llvm --target prettier-diff",
"node:test": "node ./scripts/runner.node.mjs --quiet --exec-path=$npm_execpath --node-tests "
"node:test": "node ./scripts/runner.node.mjs --quiet --exec-path=$npm_execpath --node-tests ",
"clean:zig": "rm -rf build/debug/cache/zig build/debug/CMakeCache.txt 'build/debug/*.o' .zig-cache zig-out || true"
}
}

View File

@@ -626,7 +626,7 @@ fn BakeRegisterProductionChunk(global: *JSC.JSGlobalObject, key: bun.String, sou
return result;
}
export fn BakeProdResolve(global: *JSC.JSGlobalObject, a_str: bun.String, specifier_str: bun.String) callconv(.C) bun.String {
pub export fn BakeProdResolve(global: *JSC.JSGlobalObject, a_str: bun.String, specifier_str: bun.String) callconv(.C) bun.String {
var sfa = std.heap.stackFallback(@sizeOf(bun.PathBuffer) * 2, bun.default_allocator);
const alloc = sfa.get();
@@ -836,7 +836,7 @@ pub const PerThread = struct {
};
/// Given a key, returns the source code to load.
export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String {
pub export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String {
var sfa = std.heap.stackFallback(4096, bun.default_allocator);
const allocator = sfa.get();
const utf8 = key.toUTF8(allocator);
@@ -866,3 +866,8 @@ const OpaqueFileId = FrameworkRouter.OpaqueFileId;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const VirtualMachine = JSC.VirtualMachine;
fn @"export"() void {
_ = BakeProdResolve;
_ = BakeProdLoad;
}

View File

@@ -776,10 +776,6 @@ pub const RefString = struct {
}
};
comptime {
std.testing.refAllDecls(RefString);
}
pub export fn MarkedArrayBuffer_deallocator(bytes_: *anyopaque, _: *anyopaque) void {
const mimalloc = @import("../allocators/mimalloc.zig");
// zig's memory allocator interface won't work here

View File

@@ -11,7 +11,6 @@ const mem = std.mem;
const Allocator = mem.Allocator;
const stackFallback = std.heap.stackFallback;
const assert = std.debug.assert;
const print = std.debug.print;
/// Comptime diff configuration. Defaults are usually sufficient.
pub const Options = struct {
@@ -487,8 +486,8 @@ const StrDiffer = Differ([]const u8, .{ .check_comma_disparity = true });
test StrDiffer {
const a = t.allocator;
inline for (.{
// .{ "foo", "foo" },
// .{ "foo", "bar" },
.{ "foo", "foo" },
.{ "foo", "bar" },
.{
// actual
\\[
@@ -512,85 +511,85 @@ test StrDiffer {
\\ 7
\\]
},
// // remove line
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// },
// // add some line
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// ,
// },
// // modify lines
// .{
// \\foo
// \\bar
// \\baz
// ,
// \\foo
// \\barrr
// \\baz
// },
// .{
// \\foooo
// \\bar
// \\baz
// ,
// \\foo
// \\bar
// \\baz
// },
// .{
// \\foo
// \\bar
// \\baz
// ,
// \\foo
// \\bar
// \\baz
// },
// .{
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// \\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor modified
// \\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
// \\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
// \\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
// \\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in also modified
// \\culpa qui officia deserunt mollit anim id est laborum.
// ,
// },
// remove line
.{
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
\\culpa qui officia deserunt mollit anim id est laborum.
,
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
\\culpa qui officia deserunt mollit anim id est laborum.
,
},
// add some line
.{
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
\\culpa qui officia deserunt mollit anim id est laborum.
,
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
\\culpa qui officia deserunt mollit anim id est laborum.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
,
},
// modify lines
.{
\\foo
\\bar
\\baz
,
\\foo
\\barrr
\\baz
},
.{
\\foooo
\\bar
\\baz
,
\\foo
\\bar
\\baz
},
.{
\\foo
\\bar
\\baz
,
\\foo
\\bar
\\baz
},
.{
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
\\culpa qui officia deserunt mollit anim id est laborum.
,
\\Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor modified
\\incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
\\nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
\\Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
\\fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in also modified
\\culpa qui officia deserunt mollit anim id est laborum.
,
},
}) |thing| {
var actual = try split(u8, a, thing[0]);
var expected = try split(u8, a, thing[1]);
@@ -600,9 +599,6 @@ test StrDiffer {
}
var d = try StrDiffer.diff(a, actual.items, expected.items);
defer d.deinit();
for (d.items) |diff| {
std.debug.print("{}\n", .{diff});
}
}
}

View File

@@ -1905,6 +1905,7 @@ pub const Process = struct {
// TODO: switch this to using *bun.wtf.String when it is added
pub fn Bun__Process__editWindowsEnvVar(k: bun.String, v: bun.String) callconv(.C) void {
comptime bun.assert(bun.Environment.isWindows);
if (k.tag == .Empty) return;
const wtf1 = k.value.WTFStringImpl;
var fixed_stack_allocator = std.heap.stackFallback(1025, bun.default_allocator);
@@ -1988,10 +1989,6 @@ pub const PathOrBlob = union(enum) {
}
};
comptime {
std.testing.refAllDecls(Process);
}
pub const uid_t = if (Environment.isPosix) std.posix.uid_t else bun.windows.libuv.uv_uid_t;
pub const gid_t = if (Environment.isPosix) std.posix.gid_t else bun.windows.libuv.uv_gid_t;

View File

@@ -1,8 +1,8 @@
const std = @import("std");
const bun = @import("root").bun;
const testing = std.testing;
const String = if (@import("builtin").is_test) TestString else bun.String;
const JSValue = if (@import("builtin").is_test) usize else bun.JSC.JSValue;
const String = bun.String;
const JSValue = bun.JSC.JSValue;
pub const OptionValueType = enum { boolean, string };
@@ -74,10 +74,12 @@ pub fn isOptionLikeValue(value: String) bool {
/// Find the long option associated with a short option. Looks for a configured
/// `short` and returns the short option itself if a long option is not found.
/// Example:
/// ```zig
/// findOptionByShortName('a', {}) // returns 'a'
/// findOptionByShortName('b', {
/// options: { bar: { short: 'b' } }
/// }) // returns "bar"
/// ```
pub fn findOptionByShortName(short_name: String, options: []const OptionDefinition) ?usize {
var long_option_index: ?usize = null;
for (options, 0..) |option, i| {
@@ -90,366 +92,3 @@ pub fn findOptionByShortName(short_name: String, options: []const OptionDefiniti
}
return long_option_index;
}
//
// TESTS
//
var no_options: []const OptionDefinition = &[_]OptionDefinition{};
/// Used only for tests, as lightweight substitute for bun.String
const TestString = struct {
str: []const u8,
fn length(this: TestString) usize {
return this.str.len;
}
fn hasPrefixComptime(this: TestString, comptime prefix: []const u8) bool {
return std.mem.startsWith(u8, this.str, prefix);
}
fn charAtU8(this: TestString, i: usize) u8 {
return this.str[i];
}
fn indexOfCharU8(this: TestString, chr: u8) ?usize {
return std.mem.indexOfScalar(u8, this.str, chr);
}
};
fn s(str: []const u8) TestString {
return TestString{ .str = str };
}
//
// misc
//
test "classifyToken: is option terminator" {
try testing.expectEqual(classifyToken(s("--"), no_options), .option_terminator);
}
test "classifyToken: is positional" {
try testing.expectEqual(classifyToken(s("abc"), no_options), .positional);
}
//
// isLoneLongOption
//
pub fn isLoneLongOption(value: String) bool {
return classifyToken(value, no_options) == .lone_long_option;
}
test "isLoneLongOption: when passed short option then returns false" {
try testing.expectEqual(isLoneLongOption(s("-s")), false);
}
test "isLoneLongOption: when passed short option group then returns false" {
try testing.expectEqual(isLoneLongOption(s("-abc")), false);
}
test "isLoneLongOption: when passed lone long option then returns true" {
try testing.expectEqual(isLoneLongOption(s("--foo")), true);
}
test "isLoneLongOption: when passed single character long option then returns true" {
try testing.expectEqual(isLoneLongOption(s("--f")), true);
}
test "isLoneLongOption: when passed long option and value then returns false" {
try testing.expectEqual(isLoneLongOption(s("--foo=bar")), false);
}
test "isLoneLongOption: when passed empty string then returns false" {
try testing.expectEqual(isLoneLongOption(s("")), false);
}
test "isLoneLongOption: when passed plain text then returns false" {
try testing.expectEqual(isLoneLongOption(s("foo")), false);
}
test "isLoneLongOption: when passed single dash then returns false" {
try testing.expectEqual(isLoneLongOption(s("-")), false);
}
test "isLoneLongOption: when passed double dash then returns false" {
try testing.expectEqual(isLoneLongOption(s("--")), false);
}
// This is a bit bogus, but simple consistent behaviour: long option follows double dash.
test "isLoneLongOption: when passed arg starting with triple dash then returns true" {
try testing.expectEqual(isLoneLongOption(s("---foo")), true);
}
// This is a bit bogus, but simple consistent behaviour: long option follows double dash.
test "isLoneLongOption: when passed '--=' then returns true" {
try testing.expectEqual(isLoneLongOption(s("--=")), true);
}
//
// isLoneShortOption
//
pub fn isLoneShortOption(value: String) bool {
return classifyToken(value, no_options) == .lone_short_option;
}
test "isLoneShortOption: when passed short option then returns true" {
try testing.expectEqual(isLoneShortOption(s("-s")), true);
}
test "isLoneShortOption: when passed short option group (or might be short and value) then returns false" {
try testing.expectEqual(isLoneShortOption(s("-abc")), false);
}
test "isLoneShortOption: when passed long option then returns false" {
try testing.expectEqual(isLoneShortOption(s("--foo")), false);
}
test "isLoneShortOption: when passed long option with value then returns false" {
try testing.expectEqual(isLoneShortOption(s("--foo=bar")), false);
}
test "isLoneShortOption: when passed empty string then returns false" {
try testing.expectEqual(isLoneShortOption(s("")), false);
}
test "isLoneShortOption: when passed plain text then returns false" {
try testing.expectEqual(isLoneShortOption(s("foo")), false);
}
test "isLoneShortOption: when passed single dash then returns false" {
try testing.expectEqual(isLoneShortOption(s("-")), false);
}
test "isLoneShortOption: when passed double dash then returns false" {
try testing.expectEqual(isLoneShortOption(s("--")), false);
}
//
// isLongOptionAndValue
//
pub fn isLongOptionAndValue(value: String) bool {
return classifyToken(value, no_options) == .long_option_and_value;
}
test "isLongOptionAndValue: when passed short option then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("-s")), false);
}
test "isLongOptionAndValue: when passed short option group then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("-abc")), false);
}
test "isLongOptionAndValue: when passed lone long option then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("--foo")), false);
}
test "isLongOptionAndValue: when passed long option and value then returns true" {
try testing.expectEqual(isLongOptionAndValue(s("--foo=bar")), true);
}
test "isLongOptionAndValue: when passed single character long option and value then returns true" {
try testing.expectEqual(isLongOptionAndValue(s("--f=bar")), true);
}
test "isLongOptionAndValue: when passed empty string then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("")), false);
}
test "isLongOptionAndValue: when passed plain text then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("foo")), false);
}
test "isLongOptionAndValue: when passed single dash then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("-")), false);
}
test "isLongOptionAndValue: when passed double dash then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("--")), false);
}
// This is a bit bogus, but simple consistent behaviour: long option follows double dash.
test "isLongOptionAndValue: when passed arg starting with triple dash and value then returns true" {
try testing.expectEqual(isLongOptionAndValue(s("---foo=bar")), true);
}
// This is a bit bogus, but simple consistent behaviour: long option follows double dash.
test "isLongOptionAndValue: when passed '--=' then returns false" {
try testing.expectEqual(isLongOptionAndValue(s("--=")), false);
}
//
// isOptionLikeValue
//
// Basically rejecting values starting with a dash, but run through the interesting possibilities.
test "isOptionLikeValue: when passed plain text then returns false" {
try testing.expectEqual(isOptionLikeValue(s("abc")), false);
}
//test "isOptionLikeValue: when passed digits then returns false" {
// try testing.expectEqual(isOptionLikeValue(123), false);
//}
test "isOptionLikeValue: when passed empty string then returns false" {
try testing.expectEqual(isOptionLikeValue(s("")), false);
}
// Special case, used as stdin/stdout et al and not reason to reject
test "isOptionLikeValue: when passed dash then returns false" {
try testing.expectEqual(isOptionLikeValue(s("-")), false);
}
test "isOptionLikeValue: when passed -- then returns true" {
// Not strictly option-like, but is supect
try testing.expectEqual(isOptionLikeValue(s("--")), true);
}
// Supporting undefined so can pass element off end of array without checking
//test "isOptionLikeValue: when passed undefined then returns false" {
// try testing.expectEqual(isOptionLikeValue(undefined), false);
//}
test "isOptionLikeValue: when passed short option then returns true" {
try testing.expectEqual(isOptionLikeValue(s("-a")), true);
}
test "isOptionLikeValue: when passed short option digit then returns true" {
try testing.expectEqual(isOptionLikeValue(s("-1")), true);
}
test "isOptionLikeValue: when passed negative number then returns true" {
try testing.expectEqual(isOptionLikeValue(s("-123")), true);
}
test "isOptionLikeValue: when passed short option group of short option with value then returns true" {
try testing.expectEqual(isOptionLikeValue(s("-abd")), true);
}
test "isOptionLikeValue: when passed long option then returns true" {
try testing.expectEqual(isOptionLikeValue(s("--foo")), true);
}
test "isOptionLikeValue: when passed long option with value then returns true" {
try testing.expectEqual(isOptionLikeValue(s("--foo=bar")), true);
}
//
// isShortOptionAndValue
//
pub fn isShortOptionAndValue(value: String, options: []const OptionDefinition) bool {
return classifyToken(value, options) == .short_option_and_value;
}
test "isShortOptionAndValue: when passed lone short option then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("-s"), no_options), false);
}
test "isShortOptionAndValue: when passed group with leading zero-config boolean then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("-ab"), no_options), false);
}
test "isShortOptionAndValue: when passed group with leading configured implicit boolean then returns false" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a' }};
try testing.expectEqual(isShortOptionAndValue(s("-ab"), options), false);
}
test "isShortOptionAndValue: when passed group with leading configured explicit boolean then returns false" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a', .type = .boolean }};
try testing.expectEqual(isShortOptionAndValue(s("-ab"), options), false);
}
test "isShortOptionAndValue: when passed group with leading configured string then returns true" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a', .type = .string }};
try testing.expectEqual(isShortOptionAndValue(s("-ab"), options), true);
}
test "isShortOptionAndValue: when passed long option then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("--foo"), no_options), false);
}
test "isShortOptionAndValue: when passed long option with value then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("--foo=bar"), no_options), false);
}
test "isShortOptionAndValue: when passed empty string then returns false" {
try testing.expectEqual(isShortOptionAndValue(s(""), no_options), false);
}
test "isShortOptionAndValue: when passed plain text then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("foo"), no_options), false);
}
test "isShortOptionAndValue: when passed single dash then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("-"), no_options), false);
}
test "isShortOptionAndValue: when passed double dash then returns false" {
try testing.expectEqual(isShortOptionAndValue(s("--"), no_options), false);
}
//
// isShortOptionGroup
//
pub fn isShortOptionGroup(value: String, options: []const OptionDefinition) bool {
return classifyToken(value, options) == .short_option_group;
}
test "isShortOptionGroup: when passed lone short option then returns false" {
try testing.expectEqual(isShortOptionGroup(s("-s"), no_options), false);
}
test "isShortOptionGroup: when passed group with leading zero-config boolean then returns true" {
try testing.expectEqual(isShortOptionGroup(s("-ab"), no_options), true);
}
test "isShortOptionGroup: when passed group with leading configured implicit boolean then returns true" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a' }};
try testing.expectEqual(isShortOptionGroup(s("-ab"), options), true);
}
test "isShortOptionGroup: when passed group with leading configured explicit boolean then returns true" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a', .type = .boolean }};
try testing.expectEqual(isShortOptionGroup(s("-ab"), options), true);
}
test "isShortOptionGroup: when passed group with leading configured string then returns false" {
const options = &[_]OptionDefinition{.{ .long_name = s("aaa"), .short_name = 'a', .type = .string }};
try testing.expectEqual(isShortOptionGroup(s("-ab"), options), false);
}
test "isShortOptionGroup: when passed group with trailing configured string then returns true" {
const options = &[_]OptionDefinition{.{ .long_name = s("bbb"), .short_name = 'b', .type = .string }};
try testing.expectEqual(isShortOptionGroup(s("-ab"), options), true);
}
// This one is dubious, but leave it to caller to handle.
test "isShortOptionGroup: when passed group with middle configured string then returns true" {
const options = &[_]OptionDefinition{.{ .long_name = s("bbb"), .short_name = 'b', .type = .string }};
try testing.expectEqual(isShortOptionGroup(s("-abc"), options), true);
}
test "isShortOptionGroup: when passed long option then returns false" {
try testing.expectEqual(isShortOptionGroup(s("--foo"), no_options), false);
}
test "isShortOptionGroup: when passed long option with value then returns false" {
try testing.expectEqual(isShortOptionGroup(s("--foo=bar"), no_options), false);
}
test "isShortOptionGroup: when passed empty string then returns false" {
try testing.expectEqual(isShortOptionGroup(s(""), no_options), false);
}
test "isShortOptionGroup: when passed plain text then returns false" {
try testing.expectEqual(isShortOptionGroup(s("foo"), no_options), false);
}
test "isShortOptionGroup: when passed single dash then returns false" {
try testing.expectEqual(isShortOptionGroup(s("-"), no_options), false);
}
test "isShortOptionGroup: when passed double dash then returns false" {
try testing.expectEqual(isShortOptionGroup(s("--"), no_options), false);
}

View File

@@ -1801,3 +1801,7 @@ fn handleTopLevelTestErrorBeforeJavaScriptStart(err: anyerror) noreturn {
}
Global.exit(1);
}
pub fn @"export"() void {
_ = &Scanner.BunTest__shouldGenerateCodeCoverage;
}

View File

@@ -113,53 +113,9 @@ pub const Version = struct {
return strings.eqlComptime(this.tag, current_version);
}
comptime {
_ = Bun__githubURL;
}
};
pub const UpgradeCheckerThread = struct {
pub fn spawn(env_loader: *DotEnv.Loader) void {
if (env_loader.map.get("BUN_DISABLE_UPGRADE_CHECK") != null or
env_loader.map.get("CI") != null or
strings.eqlComptime(env_loader.get("BUN_CANARY") orelse "0", "1"))
return;
var update_checker_thread = std.Thread.spawn(.{}, run, .{env_loader}) catch return;
update_checker_thread.detach();
}
fn _run(env_loader: *DotEnv.Loader) anyerror!void {
var rand = std.rand.DefaultPrng.init(@as(u64, @intCast(@max(std.time.milliTimestamp(), 0))));
const delay = rand.random().intRangeAtMost(u64, 100, 10000);
std.time.sleep(std.time.ns_per_ms * delay);
Output.Source.configureThread();
HTTP.HTTPThread.init(&.{});
defer {
js_ast.Expr.Data.Store.deinit();
js_ast.Stmt.Data.Store.deinit();
}
var version = (try UpgradeCommand.getLatestVersion(default_allocator, env_loader, null, null, false, true)) orelse return;
if (!version.isCurrent()) {
if (version.name()) |name| {
Output.prettyErrorln("\n<r><d>Bun v{s} is out. Run <b><cyan>bun upgrade<r> to upgrade.\n", .{name});
Output.flush();
}
}
version.buf.deinit();
}
fn run(env_loader: *DotEnv.Loader) void {
_run(env_loader) catch |err| {
if (Environment.isDebug) {
Output.prettyError("\n[UpgradeChecker] ERROR: {s}\n", .{@errorName(err)});
Output.flush();
}
};
pub fn @"export"() void {
_ = &Bun__githubURL;
_ = &Bun__githubBaselineURL;
}
};
@@ -1058,3 +1014,8 @@ pub const upgrade_js_bindings = struct {
return .undefined;
}
};
pub fn @"export"() void {
_ = &upgrade_js_bindings;
Version.@"export"();
}

View File

@@ -10,10 +10,6 @@ const Output = @import("../../output.zig");
pub const args = @import("clap/args.zig");
test "clap" {
testing.refAllDecls(@This());
}
pub const ComptimeClap = @import("clap/comptime.zig").ComptimeClap;
pub const StreamingClap = @import("clap/streaming.zig").StreamingClap;

View File

@@ -30,7 +30,7 @@ pub const allow_assert = isDebug or isTest or std.builtin.Mode.ReleaseSafe == @i
/// All calls to `@export` should be gated behind this check, so that code
/// generators that compile Zig code know not to reference and compile a ton of
/// unused code.
pub const export_cpp_apis = @import("builtin").output_mode == .Obj;
pub const export_cpp_apis = @import("builtin").output_mode == .Obj or isTest;
pub const build_options = @import("build_options");
@@ -45,7 +45,6 @@ pub const canary_revision = if (is_canary) build_options.canary_revision else ""
pub const dump_source = isDebug and !isTest;
pub const base_path = build_options.base_path;
pub const enable_logs = build_options.enable_logs;
pub const codegen_path = build_options.codegen_path;
pub const codegen_embed = build_options.codegen_embed;

204
src/main_test.zig Normal file
View File

@@ -0,0 +1,204 @@
const std = @import("std");
const builtin = @import("builtin");
pub const bun = @import("./bun.zig");
const recover = @import("test/recover.zig");
const TestFn = std.builtin.TestFn;
const Output = bun.Output;
const Environment = bun.Environment;
// pub const panic = bun.crash_handler.panic;
pub const panic = recover.panic;
pub const std_options = std.Options{
.enable_segfault_handler = false,
};
pub const io_mode = .blocking;
comptime {
bun.assert(builtin.target.cpu.arch.endian() == .little);
}
pub extern "C" var _environ: ?*anyopaque;
pub extern "C" var environ: ?*anyopaque;
pub fn main() void {
// This should appear before we make any calls at all to libuv.
// So it's safest to put it very early in the main function.
if (Environment.isWindows) {
_ = bun.windows.libuv.uv_replace_allocator(
@ptrCast(&bun.Mimalloc.mi_malloc),
@ptrCast(&bun.Mimalloc.mi_realloc),
@ptrCast(&bun.Mimalloc.mi_calloc),
@ptrCast(&bun.Mimalloc.mi_free),
);
environ = @ptrCast(std.os.environ.ptr);
_environ = @ptrCast(std.os.environ.ptr);
}
bun.initArgv(bun.default_allocator) catch |err| {
Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)});
};
Output.Source.Stdio.init();
defer Output.flush();
bun.StackCheck.configureThread();
const exit_code = runTests();
bun.Global.exit(exit_code);
}
const Stats = struct {
pass: u32,
fail: u32,
leak: u32,
panic: u32,
start: i64,
fn init() Stats {
var stats = std.mem.zeroes(Stats);
stats.start = std.time.milliTimestamp();
return stats;
}
/// Time elapsed since start in milliseconds
fn elapsed(this: *const Stats) i64 {
return std.time.milliTimestamp() - this.start;
}
/// Total number of tests run
fn total(this: *const Stats) u32 {
return this.pass + this.fail + this.leak + this.panic;
}
fn exitCode(this: *const Stats) u8 {
var result: u8 = 0;
if (this.fail > 0) result |= 1;
if (this.leak > 0) result |= 2;
if (this.panic > 0) result |= 4;
return result;
}
};
fn runTests() u8 {
var stats = Stats.init();
var stderr = std.io.getStdErr();
namebuf = std.heap.page_allocator.alloc(u8, namebuf_size) catch {
Output.panic("Failed to allocate name buffer", .{});
};
defer std.heap.page_allocator.free(namebuf);
const tests: []const TestFn = builtin.test_functions;
for (tests) |t| {
std.testing.allocator_instance = .{};
var did_lock = true;
stderr.lock(.exclusive) catch {
did_lock = false;
};
defer if (did_lock) stderr.unlock();
const start = std.time.milliTimestamp();
const result = recover.callForTest(t.func);
const elapsed = std.time.milliTimestamp() - start;
const name = extractName(t);
const memory_check = std.testing.allocator_instance.deinit();
if (result) |_| {
if (memory_check == .leak) {
Output.pretty("<yellow>leak</r> - {s} <i>({d}ms)</r>\n", .{ name, elapsed });
stats.leak += 1;
} else {
Output.pretty("<green>pass</r> - {s} <i>({d}ms)</r>\n", .{ name, elapsed });
stats.pass += 1;
}
} else |err| {
switch (err) {
error.Panic => {
Output.pretty("<magenta><b>panic</r> - {s} <i>({d}ms)</r>\n{s}", .{ t.name, elapsed, @errorName(err) });
stats.panic += 1;
},
else => {
Output.pretty("<red>fail</r> - {s} <i>({d}ms)</r>\n{s}", .{ t.name, elapsed, @errorName(err) });
stats.fail += 1;
},
}
}
}
const total = stats.total();
const total_time = stats.elapsed();
if (total == stats.pass) {
Output.pretty("\n<green>All tests passed</r>\n", .{});
} else {
Output.pretty("\n<green>{d}</r> passed", .{stats.pass});
if (stats.fail > 0)
Output.pretty(", <red>{d}</r> failed", .{stats.fail})
else
Output.pretty(", 0 failed", .{});
if (stats.leak > 0) Output.pretty(", <yellow>{d}</r> leaked", .{stats.leak});
if (stats.panic > 0) Output.pretty(", <magenta>{d}</r> panicked", .{stats.panic});
}
Output.pretty("\n\tRan <b>{d}</r> tests in <b>{d}</r>ms\n\n", .{ total, total_time });
return stats.exitCode();
}
// heap-allocated on start to avoid increasing binary size
threadlocal var namebuf: []u8 = undefined;
const namebuf_size = 4096;
comptime {
std.debug.assert(std.math.isPowerOfTwo(namebuf_size));
}
fn extractName(t: TestFn) []const u8 {
inline for (.{ ".test.", ".decltest." }) |test_sep| {
if (std.mem.lastIndexOf(u8, t.name, test_sep)) |marker| {
const prefix = t.name[0..marker];
const test_name = t.name[marker + test_sep.len ..];
const full_name = std.fmt.bufPrint(namebuf, "{s}\t{s}", .{ prefix, test_name }) catch @panic("name buffer too small");
return full_name;
}
}
return t.name;
}
pub const overrides = struct {
pub const mem = struct {
extern "C" fn wcslen(s: [*:0]const u16) usize;
pub fn indexOfSentinel(comptime T: type, comptime sentinel: T, p: [*:sentinel]const T) usize {
if (comptime T == u16 and sentinel == 0 and Environment.isWindows) {
return wcslen(p);
}
if (comptime T == u8 and sentinel == 0) {
return bun.C.strlen(p);
}
var i: usize = 0;
while (p[i] != sentinel) {
i += 1;
}
return i;
}
};
};
pub export fn Bun__panic(msg: [*]const u8, len: usize) noreturn {
Output.panic("{s}", .{msg[0..len]});
}
comptime {
_ = bun.bake.production.BakeProdResolve;
_ = bun.bake.production.BakeProdLoad;
_ = bun.bun_js.Bun__onRejectEntryPointResult;
_ = bun.bun_js.Bun__onResolveEntryPointResult;
_ = &@import("bun.js/node/buffer.zig").BufferVectorized;
@import("cli/upgrade_command.zig").@"export"();
@import("cli/test_command.zig").@"export"();
}

View File

@@ -870,7 +870,7 @@ pub const color_map = ComptimeStringMap(string, .{
const RESET: string = "\x1b[0m";
pub fn prettyFmt(comptime fmt: string, comptime is_enabled: bool) [:0]const u8 {
if (comptime bun.fast_debug_build_mode)
return fmt;
return fmt ++ "\x00";
comptime var new_fmt: [fmt.len * 4]u8 = undefined;
comptime var new_fmt_i: usize = 0;

View File

@@ -1,8 +1,6 @@
const tester = @import("../test/tester.zig");
const std = @import("std");
const strings = @import("../string_immutable.zig");
const FeatureFlags = @import("../feature_flags.zig");
const default_allocator = @import("../allocators/memory_allocator.zig").c_allocator;
const bun = @import("root").bun;
const Fs = @import("../fs.zig");

View File

@@ -1027,7 +1027,6 @@ pub const Test = struct {
}
pub fn make(comptime testName: string, data: anytype) !Router {
std.testing.refAllDecls(@import("./bun.js/bindings/exports.zig"));
try makeTest(testName, data);
const JSAst = bun.JSAst;
JSAst.Expr.Data.Store.create(default_allocator);

View File

@@ -1,27 +1,28 @@
const bun = @import("root").bun;
const ArrayList = std.ArrayList;
const std = @import("std");
const builtin = @import("builtin");
const ArrayList = std.ArrayList;
const Arena = std.heap.ArenaAllocator;
const Allocator = std.mem.Allocator;
const SmolStr = @import("../string.zig").SmolStr;
/// Using u16 because anymore tokens than that results in an unreasonably high
/// amount of brace expansion (like around 32k variants to expand)
pub const ExpansionVariant = packed struct {
start: u16 = 0,
end: u16 = 0,
};
const assert = bun.assert;
const log = bun.Output.scoped(.BRACES, false);
const TokenTag = enum { open, comma, text, close, eof };
const Token = union(TokenTag) {
/// Using u16 because anymore tokens than that results in an unreasonably high
/// amount of brace expansion (like around 32k variants to expand)
const ExpansionVariant = packed struct {
start: u16 = 0,
end: u16 = 0, // must be >= start
};
const Token = union(enum) {
open: ExpansionVariants,
comma,
text: SmolStr,
close,
eof,
const Tag = @typeInfo(Token).@"union".tag_type.?;
const ExpansionVariants = struct {
idx: u16 = 0,
@@ -58,33 +59,33 @@ pub const AST = struct {
const MAX_NESTED_BRACES = 10;
const StackError = error{
StackFull,
};
/// A stack on the stack
pub fn StackStack(comptime T: type, comptime SizeType: type, comptime N: SizeType) type {
fn StackStack(comptime T: type, comptime SizeType: type, comptime N: SizeType) type {
return struct {
items: [N]T = undefined,
len: SizeType = 0,
pub const Error = error{
StackFull,
};
pub fn top(this: *@This()) ?T {
fn top(this: *@This()) ?T {
if (this.len == 0) return null;
return this.items[this.len - 1];
}
pub fn topPtr(this: *@This()) ?*T {
fn topPtr(this: *@This()) ?*T {
if (this.len == 0) return null;
return &this.items[this.len - 1];
}
pub fn push(this: *@This(), value: T) Error!void {
if (this.len == N) return Error.StackFull;
fn push(this: *@This(), value: T) StackError!void {
if (this.len == N) return StackError.StackFull;
this.items[this.len] = value;
this.len += 1;
}
pub fn pop(this: *@This()) ?T {
fn pop(this: *@This()) ?T {
if (this.top()) |v| {
this.len -= 1;
return v;
@@ -95,10 +96,7 @@ pub fn StackStack(comptime T: type, comptime SizeType: type, comptime N: SizeTyp
}
/// This may have false positives but it is fast
pub fn fastDetect(src: []const u8) bool {
const Quote = enum { single, double };
_ = Quote;
fn fastDetect(src: []const u8) bool {
var has_open = false;
var has_close = false;
if (src.len < 16) {
@@ -151,13 +149,15 @@ pub fn fastDetect(src: []const u8) bool {
return false;
}
const ExpandError = StackError || ParserError;
/// `out` is preallocated by using the result from `calculateExpandedAmount`
pub fn expand(
allocator: Allocator,
tokens: []Token,
out: []std.ArrayList(u8),
contains_nested: bool,
) (error{StackFull} || ParserError)!void {
) ExpandError!void {
var out_key_counter: u16 = 1;
if (!contains_nested) {
var expansions_table = try buildExpansionTableAlloc(allocator, tokens);
@@ -176,7 +176,7 @@ fn expandNested(
out_key: u16,
out_key_counter: *u16,
start: u32,
) !void {
) ExpandError!void {
if (root.atoms == .single) {
if (start > 0) {
if (root.bubble_up) |bubble_up| {
@@ -302,9 +302,7 @@ fn expandFlat(
}
}
// pub fn expandNested()
pub fn calculateVariantsAmount(tokens: []const Token) u32 {
fn calculateVariantsAmount(tokens: []const Token) u32 {
var brace_count: u32 = 0;
var count: u32 = 0;
for (tokens) |tok| {
@@ -415,8 +413,8 @@ pub const Parser = struct {
return self.peek() == .eof;
}
fn expect(self: *Parser, toktag: TokenTag) Token {
assert(toktag == @as(TokenTag, self.peek()));
fn expect(self: *Parser, toktag: Token.Tag) Token {
assert(toktag == @as(Token.Tag, self.peek()));
if (self.check(toktag)) {
return self.advance();
}
@@ -424,15 +422,15 @@ pub const Parser = struct {
}
/// Consumes token if it matches
fn match(self: *Parser, toktag: TokenTag) bool {
if (@as(TokenTag, self.peek()) == toktag) {
fn match(self: *Parser, toktag: Token.Tag) bool {
if (@as(Token.Tag, self.peek()) == toktag) {
_ = self.advance();
return true;
}
return false;
}
fn match_any2(self: *Parser, comptime toktags: []const TokenTag) ?Token {
fn match_any2(self: *Parser, comptime toktags: []const Token.Tag) ?Token {
const peeked = self.peek();
inline for (toktags) |tag| {
if (peeked == tag) {
@@ -443,8 +441,8 @@ pub const Parser = struct {
return null;
}
fn match_any(self: *Parser, comptime toktags: []const TokenTag) bool {
const peeked = @as(TokenTag, self.peek());
fn match_any(self: *Parser, comptime toktags: []const Token.Tag) bool {
const peeked = @as(Token.Tag, self.peek());
inline for (toktags) |tag| {
if (peeked == tag) {
_ = self.advance();
@@ -454,8 +452,8 @@ pub const Parser = struct {
return false;
}
fn check(self: *Parser, toktag: TokenTag) bool {
return @as(TokenTag, self.peek()) == @as(TokenTag, toktag);
fn check(self: *Parser, toktag: Token.Tag) bool {
return @as(Token.Tag, self.peek()) == @as(Token.Tag, toktag);
}
fn peek(self: *Parser) Token {
@@ -480,18 +478,15 @@ pub const Parser = struct {
}
};
pub fn calculateExpandedAmount(tokens: []const Token) !u32 {
pub fn calculateExpandedAmount(tokens: []const Token) StackError!u32 {
var nested_brace_stack = StackStack(u8, u8, MAX_NESTED_BRACES){};
var variant_count: u32 = 0;
var i: usize = 0;
var prev_comma: bool = false;
while (i < tokens.len) : (i += 1) {
for (tokens) |tok| {
prev_comma = false;
switch (tokens[i]) {
.open => {
try nested_brace_stack.push(0);
},
switch (tok) {
.open => try nested_brace_stack.push(0),
.comma => {
const val = nested_brace_stack.topPtr().?;
val.* += 1;
@@ -518,16 +513,13 @@ pub fn calculateExpandedAmount(tokens: []const Token) !u32 {
return variant_count;
}
pub fn buildExpansionTableAlloc(alloc: Allocator, tokens: []Token) !std.ArrayList(ExpansionVariant) {
fn buildExpansionTableAlloc(alloc: Allocator, tokens: []Token) !std.ArrayList(ExpansionVariant) {
var table = std.ArrayList(ExpansionVariant).init(alloc);
try buildExpansionTable(tokens, &table);
return table;
}
pub fn buildExpansionTable(
tokens: []Token,
table: *std.ArrayList(ExpansionVariant),
) !void {
fn buildExpansionTable(tokens: []Token, table: *std.ArrayList(ExpansionVariant)) !void {
const BraceState = struct {
tok_idx: u16,
variants: u16,
@@ -594,7 +586,7 @@ const NewChars = @import("./shell.zig").ShellCharIter;
pub const Lexer = NewLexer(.ascii);
pub fn NewLexer(comptime encoding: Encoding) type {
fn NewLexer(comptime encoding: Encoding) type {
const Chars = NewChars(encoding);
return struct {
chars: Chars,
@@ -803,4 +795,32 @@ pub fn NewLexer(comptime encoding: Encoding) type {
};
}
const assert = bun.assert;
const t = std.testing;
test Lexer {
var arena = std.heap.ArenaAllocator.init(t.allocator);
defer arena.deinit();
const TestCase = struct { []const u8, []const Token };
const test_cases: []const TestCase = &[_]TestCase{
.{
"{}",
&[_]Token{ .{ .open = .{} }, .close, .eof },
},
.{
"{foo}",
&[_]Token{ .{ .open = .{} }, .{ .text = try SmolStr.fromSlice(arena.allocator(), "foo") }, .close, .eof },
},
};
for (test_cases) |test_case| {
const src, const expected = test_case;
// NOTE: don't use arena here so that we can test for memory leaks
var result = try Lexer.tokenize(t.allocator, src);
defer result.tokens.deinit();
try t.expectEqualSlices(
Token,
expected,
result.tokens.items,
);
}
}

View File

@@ -9,7 +9,7 @@ pub const SmolStr = packed struct {
cap: u32,
__ptr: [*]u8,
const Tag: usize = 0x8000000000000000;
const Tag: usize = 0x8000000000000000; // NOTE: only works on little endian systems
const NegatedTag: usize = ~Tag;
pub fn jsonStringify(self: *const SmolStr, writer: anytype) !void {
@@ -21,7 +21,30 @@ pub const SmolStr = packed struct {
__len: u7,
_tag: u1,
pub fn len(this: Inlined) u8 {
const max_len: comptime_int = @bitSizeOf(@FieldType(Inlined, "data")) / 8;
const empty: Inlined = .{
.data = 0,
.__len = 0,
._tag = 1,
};
/// ## Errors
/// if `str` is longer than `max_len`
pub fn init(str: []const u8) !Inlined {
if (str.len > max_len) {
@branchHint(.unlikely);
return error.StringTooLong;
}
var inlined = Inlined.empty;
if (str.len > 0) {
@memcpy(inlined.allChars()[0..str.len], str[0..str.len]);
inlined.setLen(@intCast(str.len));
}
return inlined;
}
pub inline fn len(this: Inlined) u8 {
return @intCast(this.__len);
}
@@ -29,12 +52,20 @@ pub const SmolStr = packed struct {
this.__len = new_len;
}
pub fn slice(this: *Inlined) []const u8 {
return this.allChars()[0..this.__len];
pub fn slice(this: *const Inlined) []const u8 {
return @constCast(this).ptr()[0..this.__len];
}
pub fn allChars(this: *Inlined) *[15]u8 {
return @as([*]u8, @ptrCast(@as(*u128, @ptrCast(this))))[0..15];
pub fn sliceMut(this: *Inlined) []u8 {
return this.ptr()[0..this.__len];
}
pub fn allChars(this: *Inlined) *[max_len]u8 {
return this.ptr()[0..max_len];
}
inline fn ptr(this: *Inlined) [*]u8 {
return @as([*]u8, @ptrCast(@as(*u128, @ptrCast(this))));
}
};
@@ -43,12 +74,7 @@ pub const SmolStr = packed struct {
}
pub fn empty() SmolStr {
const inlined = Inlined{
.data = 0,
.__len = 0,
._tag = 1,
};
return SmolStr.fromInlined(inlined);
return SmolStr.fromInlined(Inlined.empty);
}
pub fn len(this: *const SmolStr) u32 {
@@ -79,7 +105,10 @@ pub const SmolStr = packed struct {
return @as(usize, @intFromPtr(this.__ptr)) & Tag != 0;
}
/// ## Panics
/// if `this` is too long to fit in an inlined string
pub fn toInlined(this: *const SmolStr) Inlined {
assert(this.len() <= Inlined.max_len);
var inlined: Inlined = @bitCast(@as(u128, @bitCast(this.*)));
inlined._tag = 1;
return inlined;
@@ -113,25 +142,21 @@ pub const SmolStr = packed struct {
return SmolStr.fromInlined(inlined);
}
pub fn deinit(this: *SmolStr, allocator: Allocator) void {
if (!this.isInlined()) {
allocator.free(this.slice());
}
}
pub fn fromSlice(allocator: Allocator, values: []const u8) Allocator.Error!SmolStr {
if (values.len > 15) {
if (values.len > Inlined.max_len) {
var baby_list = try BabyList(u8).initCapacity(allocator, values.len);
baby_list.appendSliceAssumeCapacity(values);
return SmolStr.fromBabyList(baby_list);
}
var inlined = Inlined{
.data = 0,
.__len = 0,
._tag = 1,
};
if (values.len > 0) {
@memcpy(inlined.allChars()[0..values.len], values[0..values.len]);
inlined.setLen(@intCast(values.len));
}
// SAFETY: we already checked that `values` can fit in an inlined string
const inlined = Inlined.init(values) catch unreachable;
return SmolStr.fromInlined(inlined);
}
@@ -146,11 +171,10 @@ pub const SmolStr = packed struct {
pub fn appendChar(this: *SmolStr, allocator: Allocator, char: u8) Allocator.Error!void {
if (this.isInlined()) {
var inlined = this.toInlined();
if (inlined.len() + 1 > 15) {
if (inlined.len() + 1 > Inlined.max_len) {
var baby_list = try BabyList(u8).initCapacity(allocator, inlined.len() + 1);
baby_list.appendSliceAssumeCapacity(inlined.slice());
try baby_list.push(allocator, char);
// this.* = SmolStr.fromBabyList(baby_list);
this.__len = baby_list.len;
this.__ptr = baby_list.ptr;
this.cap = baby_list.cap;
@@ -159,7 +183,6 @@ pub const SmolStr = packed struct {
}
inlined.allChars()[inlined.len()] = char;
inlined.setLen(@intCast(inlined.len() + 1));
// this.* = SmolStr.fromInlined(inlined);
this.* = @bitCast(inlined);
this.markInlined();
return;
@@ -172,7 +195,6 @@ pub const SmolStr = packed struct {
};
try baby_list.push(allocator, char);
// this.* = SmolStr.fromBabyList(baby_list);
this.__len = baby_list.len;
this.__ptr = baby_list.ptr;
this.cap = baby_list.cap;
@@ -182,7 +204,7 @@ pub const SmolStr = packed struct {
pub fn appendSlice(this: *SmolStr, allocator: Allocator, values: []const u8) Allocator.Error!void {
if (this.isInlined()) {
var inlined = this.toInlined();
if (inlined.len() + values.len > 15) {
if (inlined.len() + values.len > Inlined.max_len) {
var baby_list = try BabyList(u8).initCapacity(allocator, inlined.len() + values.len);
baby_list.appendSliceAssumeCapacity(inlined.slice());
baby_list.appendSliceAssumeCapacity(values);
@@ -206,3 +228,47 @@ pub const SmolStr = packed struct {
return;
}
};
const t = std.testing;
test SmolStr {
// large strings are heap-allocated
{
var str = try SmolStr.fromSlice(t.allocator, "oh wow this is a long string");
defer str.deinit(t.allocator);
try t.expectEqualStrings("oh wow this is a long string", str.slice());
try t.expect(!str.isInlined());
}
// small strings are inlined
{
var str = try SmolStr.fromSlice(t.allocator, "hello");
defer str.deinit(t.allocator);
try t.expectEqualStrings("hello", str.slice());
try t.expect(str.isInlined());
// operations that grow a string beyond the inlined capacity force an allocation.
try str.appendSlice(t.allocator, " world, this makes it too long to be inlined");
try t.expectEqualStrings("hello world, this makes it too long to be inlined", str.slice());
try t.expect(!str.isInlined());
}
}
test "SmolStr.Inlined.init" {
var hello = try SmolStr.Inlined.init("hello");
try t.expectEqualStrings("hello", hello.slice());
try t.expectEqual(5, hello.len());
try t.expectEqual(1, hello._tag); // 1 = inlined
try t.expectError(error.StringTooLong, SmolStr.Inlined.init("this string is too long to be inlined within a u120"));
const empty = try SmolStr.Inlined.init("");
try t.expectEqual(empty, SmolStr.Inlined.empty);
}
test "Creating an inlined SmolStr does not allocate" {
var hello = try SmolStr.fromSlice(t.allocator, "hello");
// no `defer hello.deinit()` to ensure fromSlice does not allocate
try t.expectEqual(5, hello.len());
try t.expect(hello.isInlined());
}

File diff suppressed because it is too large Load Diff

131
src/test/recover.zig Normal file
View File

@@ -0,0 +1,131 @@
// Copyright © 2024 Dimitris Dinodimos.
//! Panic recover.
//! Regains control of the calling thread when the function panics or behaves
//! undefined.
const std = @import("std");
const builtin = @import("builtin");
const Context = if (builtin.os.tag == .windows)
std.os.windows.CONTEXT
else if (builtin.os.tag == .linux and builtin.abi == .musl)
musl.jmp_buf
else
std.c.ucontext_t;
threadlocal var top_ctx: ?*const Context = null;
/// Returns if there was no recover call in current thread.
/// Otherwise, does not return and execution continues from the current thread
/// recover call.
/// Call from root source file panic handler.
pub fn panicked() void {
if (top_ctx) |ctx| {
setContext(ctx);
}
}
// comptime function that extends T by combining its error set with error.Panic
fn ExtErrType(T: type) type {
const E = error{Panic};
const info = @typeInfo(T);
if (info != .error_union) {
return E!T;
}
return (info.error_union.error_set || E)!(info.error_union.payload);
}
// comptime function that returns the return type of function `func`
fn ReturnType(func: anytype) type {
const ti = @typeInfo(@TypeOf(func));
return ti.@"fn".return_type.?;
}
pub fn callForTest(
test_func: *const fn () anyerror!void,
) anyerror!void {
const prev_ctx: ?*const Context = top_ctx;
var ctx: Context = std.mem.zeroes(Context);
getContext(&ctx);
if (top_ctx != prev_ctx) {
top_ctx = prev_ctx;
return error.Panic;
}
top_ctx = &ctx;
defer top_ctx = prev_ctx;
return @call(.auto, test_func, .{});
}
/// Calls `func` with `args`, guarding from runtime errors.
/// Returns `error.Panic` when recovers from runtime error.
/// Otherwise returns the return value of func.
pub fn call(
func: anytype,
args: anytype,
) ExtErrType(ReturnType(func)) {
const prev_ctx: ?*const Context = top_ctx;
var ctx: Context = std.mem.zeroes(Context);
getContext(&ctx);
if (top_ctx != prev_ctx) {
top_ctx = prev_ctx;
return error.Panic;
}
top_ctx = &ctx;
defer top_ctx = prev_ctx;
return @call(.auto, func, args);
}
// windows
const CONTEXT = std.os.windows.CONTEXT;
const EXCEPTION_RECORD = std.os.windows.EXCEPTION_RECORD;
const WINAPI = std.os.windows.WINAPI;
extern "ntdll" fn RtlRestoreContext(
ContextRecord: *const CONTEXT,
ExceptionRecord: ?*const EXCEPTION_RECORD,
) callconv(WINAPI) noreturn;
// darwin, bsd, gnu linux
extern "c" fn setcontext(ucp: *const std.c.ucontext_t) noreturn;
// linux musl
const musl = struct {
const jmp_buf = @cImport(@cInclude("setjmp.h")).jmp_buf;
extern fn setjmp(env: *jmp_buf) c_int;
extern fn longjmp(env: *const jmp_buf, val: c_int) noreturn;
};
inline fn getContext(ctx: *Context) void {
if (builtin.os.tag == .windows) {
std.os.windows.ntdll.RtlCaptureContext(ctx);
} else if (builtin.os.tag == .linux and builtin.abi == .musl) {
_ = musl.setjmp(ctx);
} else {
_ = std.debug.getContext(ctx);
}
}
inline fn setContext(ctx: *const Context) noreturn {
if (builtin.os.tag == .windows) {
RtlRestoreContext(ctx, null);
} else if (builtin.os.tag == .linux and builtin.abi == .musl) {
musl.longjmp(ctx, 1);
} else {
setcontext(ctx);
}
}
/// Panic handler that if there is a recover call in current thread continues
/// from recover call. Otherwise calls the default panic.
/// Install at root source file as `pub const panic = @import("recover").panic;`
pub const panic: type = std.debug.FullPanic(
struct {
pub fn panic(
msg: []const u8,
first_trace_addr: ?usize,
) noreturn {
panicked();
std.debug.defaultPanic(msg, first_trace_addr);
}
}.panic,
);

View File

@@ -1,157 +0,0 @@
const std = @import("std");
const string = []const u8;
const RED = "\x1b[31;1m";
const GREEN = "\x1b[32;1m";
const CYAN = "\x1b[36;1m";
const WHITE = "\x1b[37;1m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
pub const Tester = struct {
pass: std.ArrayList(Expectation),
fail: std.ArrayList(Expectation),
allocator: std.mem.Allocator,
pub fn t(allocator: std.mem.Allocator) Tester {
return Tester{
.allocator = allocator,
.pass = std.ArrayList(Expectation).init(allocator),
.fail = std.ArrayList(Expectation).init(allocator),
};
}
pub const Expectation = struct {
expected: string,
result: string,
source: std.builtin.SourceLocation,
pub fn init(expected: string, result: string, src: std.builtin.SourceLocation) Expectation {
return Expectation{
.expected = expected,
.result = result,
.source = src,
};
}
const PADDING = 0;
pub fn print(self: *const @This()) void {
const pad = &([_]u8{' '} ** PADDING);
var stderr = std.io.getStdErr();
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll(pad) catch unreachable;
stderr.writeAll(DIM) catch unreachable;
std.fmt.format(stderr.writer(), "{s}:{d}:{d}", .{ self.source.file, self.source.line, self.source.column }) catch unreachable;
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll("\n") catch unreachable;
stderr.writeAll(pad) catch unreachable;
stderr.writeAll("Expected: ") catch unreachable;
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll(GREEN) catch unreachable;
std.fmt.format(stderr.writer(), "\"{s}\"", .{self.expected}) catch unreachable;
stderr.writeAll(GREEN) catch unreachable;
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll("\n") catch unreachable;
stderr.writeAll(pad) catch unreachable;
stderr.writeAll("Received: ") catch unreachable;
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll(RED) catch unreachable;
std.fmt.format(stderr.writer(), "\"{s}\"", .{self.result}) catch unreachable;
stderr.writeAll(RED) catch unreachable;
stderr.writeAll(RESET) catch unreachable;
stderr.writeAll("\n") catch unreachable;
}
const strings = @import("../string_immutable.zig");
pub fn evaluate_outcome(self: *const @This()) Outcome {
if (strings.eql(self.expected, self.result)) {
return .pass;
} else {
return .fail;
}
}
};
pub const Outcome = enum {
pass,
fail,
};
pub inline fn expect(tester: *Tester, expected: string, result: string, src: std.builtin.SourceLocation) bool {
var expectation = Expectation.init(expected, result, src);
switch (expectation.evaluate_outcome()) {
.pass => {
tester.pass.append(expectation) catch unreachable;
return true;
},
.fail => {
tester.fail.append(expectation) catch unreachable;
return false;
},
}
}
const ReportType = enum {
none,
pass,
fail,
some_fail,
pub fn init(tester: *Tester) ReportType {
if (tester.fail.items.len == 0 and tester.pass.items.len == 0) {
return .none;
} else if (tester.fail.items.len == 0) {
return .pass;
} else if (tester.pass.items.len == 0) {
return .fail;
} else {
return .some_fail;
}
}
};
pub fn report(tester: *Tester, src: std.builtin.SourceLocation) void {
var stderr = std.io.getStdErr();
if (tester.fail.items.len > 0) {
std.fmt.format(stderr.writer(), "\n\n", .{}) catch unreachable;
}
for (tester.fail.items) |item| {
item.print();
std.fmt.format(stderr.writer(), "\n", .{}) catch unreachable;
}
switch (ReportType.init(tester)) {
.none => {
std.log.info("No expectations.\n\n", .{});
},
.pass => {
std.fmt.format(stderr.writer(), "{s}All {d} expectations passed.{s}\n", .{ GREEN, tester.pass.items.len, GREEN }) catch unreachable;
std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
std.testing.expect(true) catch std.debug.panic("Test failure", .{});
},
.fail => {
std.fmt.format(stderr.writer(), "{s}All {d} expectations failed.{s}\n\n", .{ RED, tester.fail.items.len, RED }) catch unreachable;
std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
std.testing.expect(false) catch std.debug.panic("Test failure", .{});
},
.some_fail => {
std.fmt.format(stderr.writer(), "{s}{d} failed{s} and {s}{d} passed{s} of {d} expectations{s}\n\n", .{
RED,
tester.fail.items.len,
RED ++ RESET,
GREEN,
tester.pass.items.len,
GREEN ++ RESET,
tester.fail.items.len + tester.pass.items.len,
RESET,
}) catch unreachable;
std.fmt.format(stderr.writer(), RESET, .{}) catch unreachable;
std.testing.expect(false) catch std.debug.panic("Test failure in {s}: {s}:{d}:{d}", .{ src.fn_name, src.file, src.line, src.column });
},
}
}
};

16
src/unit_test.zig Normal file
View File

@@ -0,0 +1,16 @@
const std = @import("std");
const bun = @import("root").bun;
const t = std.testing;
test {
_ = @import("shell/braces.zig");
_ = @import("bun.js/node/assert/myers_diff.zig");
}
test "basic string usage" {
var s = bun.String.createUTF8("hi");
defer s.deref();
try t.expect(s.tag != .Dead and s.tag != .Empty);
try t.expectEqual(s.length(), 2);
try t.expectEqualStrings(s.asUTF8().?, "hi");
}

View File

@@ -6,9 +6,9 @@ const words: Record<string, { reason: string; limit?: number; regex?: boolean }>
" != undefined": { reason: "This is by definition Undefined Behavior." },
" == undefined": { reason: "This is by definition Undefined Behavior." },
'@import("root").bun.': { reason: "Only import 'bun' once" },
"std.debug.assert": { reason: "Use bun.assert instead", limit: 25 },
"std.debug.assert": { reason: "Use bun.assert instead", limit: 26 },
"std.debug.dumpStackTrace": { reason: "Use bun.handleErrorReturnTrace or bun.crash_handler.dumpStackTrace instead" },
"std.debug.print": { reason: "Don't let this be committed", limit: 2 },
"std.debug.print": { reason: "Don't let this be committed", limit: 0 },
"std.mem.indexOfAny(u8": { reason: "Use bun.strings.indexOfAny", limit: 3 },
"undefined != ": { reason: "This is by definition Undefined Behavior." },
"undefined == ": { reason: "This is by definition Undefined Behavior." },