Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred Sumner
1d1edf7cfc Add hi test 2024-07-06 01:34:25 -07:00
Jarred Sumner
6ca649a88d leaky test 2024-07-06 01:31:56 -07:00
6 changed files with 172 additions and 82 deletions

View File

@@ -2527,6 +2527,7 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb
}
extern "C" JSC::EncodedJSValue CryptoObject__create(JSGlobalObject*);
extern "C" bool is_allowed_to_use_internal_testing_apis;
void GlobalObject::finishCreation(VM& vm)
{
@@ -2571,15 +2572,33 @@ void GlobalObject::finishCreation(VM& vm)
JSC::JSGlobalObject* globalObject = init.owner;
JSValue result = JSValue::decode(Bun__Jest__createTestModuleObject(globalObject));
init.set(result.toObject(globalObject));
auto* object = result.toObject(globalObject);
// Add a copy of the "test" function we can use to add extra Bun test-suite only methods
if (is_allowed_to_use_internal_testing_apis) {
const auto test = Identifier::fromString(init.vm, "test"_s);
auto* testFn = object->getDirect(init.vm, test).getObject();
testFn->putDirect(init.vm, init.vm.propertyNames->region, testFn);
}
init.set(object);
});
m_lazyPreloadTestModuleObject.initLater(
[](const Initializer<JSObject>& init) {
JSC::JSGlobalObject* globalObject = init.owner;
Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(init.owner);
JSValue result = JSValue::decode(Bun__Jest__createTestPreloadObject(globalObject));
init.set(result.toObject(globalObject));
auto* preload = result.toObject(globalObject);
// Add a copy of the "test" function we can use to add extra Bun test-suite only methods
if (is_allowed_to_use_internal_testing_apis) {
const auto test = Identifier::fromString(init.vm, "test"_s);
JSObject* testFn = jsCast<JSObject*>(globalObject->lazyTestModuleObject()->getDirect(init.vm, test));
preload->getDirect(init.vm, test).getObject()->putDirect(init.vm, init.vm.propertyNames->region, testFn);
}
init.set(preload);
});
m_testMatcherUtilsObject.initLater(

View File

@@ -746,7 +746,7 @@ pub const ModuleLoader = struct {
transpile_source_code_arena: ?*bun.ArenaAllocator = null,
eval_source: ?*logger.Source = null,
pub var is_allowed_to_use_internal_testing_apis = false;
pub export var is_allowed_to_use_internal_testing_apis = false;
/// This must be called after calling transpileSourceCode
pub fn resetArena(this: *ModuleLoader, jsc_vm: *VirtualMachine) void {

8
test/bunfig.toml Normal file
View File

@@ -0,0 +1,8 @@
[test]
# Large monorepos (like Bun) may want to specify the test directory more specifically
# By default, `bun test` scans every single folder recursively which, if you
# have a gigantic submodule (like WebKit), requires lots of directory
# traversals
#
# Instead, we can only scan the test directory for Bun's runtime tests
preload = "./preload.ts"

View File

@@ -1,10 +1,9 @@
/// Be careful when adding to this file
/// This file is loaded in every single test.
/// Every time you make this file longer, you make our entire test suite slower.
import { gc as bunGC, unsafe, which } from "bun";
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
import { readlink, readFile, writeFile } from "fs/promises";
import { isAbsolute, join, dirname } from "path";
import fs, { openSync, closeSync } from "node:fs";
import os from "node:os";
import { heapStats } from "bun:jsc";
type Awaitable<T> = T | Promise<T>;
@@ -136,6 +135,8 @@ type DirectoryTree = {
};
export function tempDirWithFiles(basename: string, files: DirectoryTree): string {
const fs = require("fs");
const os = require("os");
async function makeTree(base: string, tree: DirectoryTree) {
for (const [name, raw_contents] of Object.entries(tree)) {
const contents = typeof raw_contents === "function" ? await raw_contents({ root: base }) : raw_contents;
@@ -214,32 +215,6 @@ export function bunRunAsScript(
};
}
export function randomLoneSurrogate() {
const n = randomRange(0, 2);
if (n === 0) return randomLoneHighSurrogate();
return randomLoneLowSurrogate();
}
export function randomInvalidSurrogatePair() {
const low = randomLoneLowSurrogate();
const high = randomLoneHighSurrogate();
return `${low}${high}`;
}
// Generates a random lone high surrogate (from the range D800-DBFF)
export function randomLoneHighSurrogate() {
return String.fromCharCode(randomRange(0xd800, 0xdbff));
}
// Generates a random lone high surrogate (from the range DC00-DFFF)
export function randomLoneLowSurrogate() {
return String.fromCharCode(randomRange(0xdc00, 0xdfff));
}
function randomRange(low: number, high: number): number {
return low + Math.floor(Math.random() * (high - low));
}
export function runWithError(cb: () => unknown): Error | undefined {
try {
cb();
@@ -399,6 +374,7 @@ export function ospath(path: string) {
* non-npm packages (links, folders, git dependencies, etc.)
*/
export async function toMatchNodeModulesAt(lockfile: any, root: string) {
const fs = require("fs");
function shouldSkip(pkg: any, dep: any): boolean {
return (
!pkg ||
@@ -529,6 +505,7 @@ export async function toHaveBins(actual: string[], expectedBins: string[]) {
export async function toBeValidBin(actual: string, expectedLinkPath: string) {
const message = () => `Expected ${actual} to be a link to ${expectedLinkPath}`;
const { readFile, readlink } = require("fs/promises");
if (isWindows) {
const contents = await readFile(actual + ".bunx", "utf16le");
@@ -553,10 +530,11 @@ export async function toBeWorkspaceLink(actual: string, expectedLinkPath: string
}
export function getMaxFD(): number {
const { openSync, readdirSync, closeSync } = require("fs");
if (isMacOS || isLinux) {
let max = -1;
// https://github.com/python/cpython/commit/e21a7a976a7e3368dc1eba0895e15c47cb06c810
for (let entry of fs.readdirSync(isMacOS ? "/dev/fd" : "/proc/self/fd")) {
for (let entry of readdirSync(isMacOS ? "/dev/fd" : "/proc/self/fd")) {
const fd = parseInt(entry.trim(), 10);
if (Number.isSafeInteger(fd) && fd >= 0) {
max = Math.max(max, fd);
@@ -757,7 +735,7 @@ function failTestsOnBlockingWriteCall() {
failTestsOnBlockingWriteCall();
export function dumpStats() {
const stats = heapStats();
const stats = require("bun:jsc").heapStats();
const { objectTypeCounts, protectedObjectTypeCounts } = stats;
console.log({
objects: Object.fromEntries(Object.entries(objectTypeCounts).sort()),
@@ -823,6 +801,7 @@ const shebang_windows = (program: string) => `0</* :{
`;
export function writeShebangScript(path: string, program: string, data: string) {
const { writeFile } = require("fs/promises");
if (!isWindows) {
return writeFile(path, shebang_posix(program) + "\n" + data, { mode: 0o777 });
} else {
@@ -897,6 +876,8 @@ export function mergeWindowEnvs(envs: Record<string, string | undefined>[]) {
}
export function tmpdirSync(pattern: string = "bun.test.") {
const fs = require("fs");
const os = require("os");
return fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), pattern));
}
@@ -1055,7 +1036,7 @@ let networkInterfaces: any;
function isIP(type: "IPv4" | "IPv6") {
if (!networkInterfaces) {
networkInterfaces = os.networkInterfaces();
networkInterfaces = require("os").networkInterfaces();
}
for (const networkInterface of Object.values(networkInterfaces)) {
for (const { family } of networkInterface as any[]) {
@@ -1127,3 +1108,107 @@ export function isMacOSVersionAtLeast(minVersion: number): boolean {
}
return parseFloat(macOSVersion) >= minVersion;
}
const NS_PER_MS = 1e6;
let lazyLoadedNumberFmt;
export async function leakTest(
fn: () => void | Promise<void>,
{ label, minimumMilliseocnds = 5, repeatCount = 50, verbose = isDebug, maxWarmup = 100 } = {},
) {
maxWarmup *= NS_PER_MS;
lazyLoadedNumberFmt ||= new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
const initialStart = Bun.nanoseconds();
let i = 0;
let last = initialStart;
Bun.gc(true);
const log = verbose ? console.log : (...args: any[]) => {};
let threshold = minimumMilliseocnds * NS_PER_MS;
while (i < 100) {
while ((last = Bun.nanoseconds()) - initialStart < threshold) {
i++;
await fn();
}
if (i < 100) {
threshold *= 2;
}
if (last - initialStart > maxWarmup) {
break;
}
}
log(`[${label}]`, "Running", i, "iterations", "x", repeatCount);
Bun.gc(true);
const baseline = (process.memoryUsage.rss() / 1024 / 1024) | 0;
for (let iter = 0; iter < repeatCount; iter++) {
for (let j = 0; j < i; j++) {
await fn();
}
Bun.gc();
}
const rss = (process.memoryUsage.rss() / 1024 / 1024) | 0;
const delta = rss - baseline;
const leak = delta >= 1 ? ((delta / (i * repeatCount)) * 1024 * 1024) | 0 : 0;
log(`[${label}]`, "Ran", i, "iterations", "x", repeatCount, {
delta,
leak: `${lazyLoadedNumberFmt.format(leak)} bytes per iteration`,
rss,
});
return {
delta,
leak,
rss,
i,
baseline,
repeatCount,
};
}
/* @ts-expect-error */
const secretTestFn = Bun.jest(import.meta.path).test?.region!;
if (secretTestFn) {
secretTestFn.leak = function (label, fn, options = {}) {
const test = this;
if (!options) {
options = {};
}
// Default delta is 20 MB.
const delta = options?.delta ?? 20;
options.label ??= label;
test(
label,
async () => {
const result = await leakTest(fn, options);
expect(result.delta).toBeLessThan(delta);
},
options.timeout ?? undefined,
);
};
}
declare module "bun:test" {
interface LeakTestOptions extends TestOptions {
repeatCount?: number;
minimumMilliseocnds?: number;
verbose?: boolean;
maxWarmup?: number;
/**
* Default delta is 20 MB.
*/
delta?: number;
}
interface Test {
leak(label: string, fn: () => void | Promise<void>, options?: LeakTestOptions): void;
}
}

View File

@@ -0,0 +1,17 @@
import { test, expect } from "bun:test";
test.leak(
"echo hi",
async () => {
await Bun.$`echo hi`.quiet();
},
{ delta: 5, repeatCount: 500 },
);
test.leak(
"echo hi text",
async () => {
await Bun.$`echo hi`.text();
},
{ delta: 5, repeatCount: 500 },
);

View File

@@ -50,54 +50,15 @@ const constructorArgs = [
},
},
],
];
] as const;
for (let i = 0; i < constructorArgs.length; i++) {
const args = constructorArgs[i];
test("new Request(test #" + i + ")", () => {
Bun.gc(true);
for (let i = 0; i < 1000; i++) {
new Request(...args);
}
Bun.gc(true);
const baseline = (process.memoryUsage.rss() / 1024 / 1024) | 0;
for (let i = 0; i < 2000; i++) {
for (let j = 0; j < 500; j++) {
new Request(...args);
}
Bun.gc();
}
Bun.gc(true);
const memory = (process.memoryUsage.rss() / 1024 / 1024) | 0;
const delta = Math.max(memory, baseline) - Math.min(baseline, memory);
console.log("RSS delta: ", delta, "MB");
expect(delta).toBeLessThan(30);
test.leak("new Request(test #" + i + ")", async () => {
new Request(...args);
});
test("request.clone(test #" + i + ")", () => {
Bun.gc(true);
for (let i = 0; i < 1000; i++) {
const request = new Request(...args);
request.clone();
}
Bun.gc(true);
const baseline = (process.memoryUsage.rss() / 1024 / 1024) | 0;
for (let i = 0; i < 2000; i++) {
for (let j = 0; j < 500; j++) {
const request = new Request(...args);
request.clone();
}
Bun.gc();
}
Bun.gc(true);
const memory = (process.memoryUsage.rss() / 1024 / 1024) | 0;
const delta = Math.max(memory, baseline) - Math.min(baseline, memory);
console.log("RSS delta: ", delta, "MB");
expect(delta).toBeLessThan(30);
test.leak("request.clone(test #" + i + ")", async () => {
const request = new Request(...args);
request.clone();
});
}