Enable breaking_changes_1_3 (#23308)

Breaking changes:

- bun:test: disallow creating snapshots or using .only() in ci
- for users: hopefully this should only reveal existing bugs in tests,
not cause failures.
- general: enable calling unhandled rejection handlers for
ErrorBuilder.reject()
- for users: this might reveal some unhandled rejections that were not
visible before.
This commit is contained in:
pfg
2025-10-07 12:07:29 -07:00
committed by GitHub
parent bcbba97807
commit 5e8feca98b
13 changed files with 115 additions and 296 deletions

View File

@@ -278,7 +278,6 @@ fn genericExtend(this: *ScopeFunctions, globalThis: *JSGlobalObject, cfg: bun_te
}
fn errorInCI(globalThis: *jsc.JSGlobalObject, signature: []const u8) bun.JSError!void {
if (!bun.FeatureFlags.breaking_changes_1_3) return; // this is a breaking change for version 1.3
if (bun.detectCI()) |_| {
return globalThis.throwPretty("{s} is not allowed in CI environments.\nIf this is not a CI environment, set the environment variable CI=false to force allow.", .{signature});
}

View File

@@ -744,12 +744,10 @@ pub const Expect = struct {
}
if (needs_write) {
if (bun.FeatureFlags.breaking_changes_1_3) {
if (bun.detectCI()) |_| {
if (!update) {
const signature = comptime getSignature(fn_name, "", false);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Inline snapshot updates are not allowed in CI environments unless --update-snapshots is used\nIf this is not a CI environment, set the environment variable CI=false to force allow.", .{});
}
if (bun.detectCI()) |_| {
if (!update) {
const signature = comptime getSignature(fn_name, "", false);
return this.throw(globalThis, signature, "\n\n<b>Matcher error<r>: Inline snapshot updates are not allowed in CI environments unless --update-snapshots is used\nIf this is not a CI environment, set the environment variable CI=false to force allow.", .{});
}
}
var buntest_strong = this.bunTest() orelse {

View File

@@ -484,7 +484,6 @@ pub fn captureTestLineNumber(callframe: *jsc.CallFrame, globalThis: *JSGlobalObj
}
pub fn errorInCI(globalObject: *jsc.JSGlobalObject, message: []const u8) bun.JSError!void {
if (!bun.FeatureFlags.breaking_changes_1_3) return; // this is a breaking change for version 1.3
if (bun.detectCI()) |_| {
return globalObject.throwPretty("{s}\nIf this is not a CI environment, set the environment variable CI=false to force allow.", .{message});
}

View File

@@ -85,11 +85,9 @@ pub const Snapshots = struct {
// doesn't exist. append to file bytes and add to hashmap.
// Prevent snapshot creation in CI environments unless --update-snapshots is used
if (bun.FeatureFlags.breaking_changes_1_3) {
if (bun.detectCI()) |_| {
if (!this.update_snapshots) {
return error.SnapshotCreationNotAllowedInCI;
}
if (bun.detectCI()) |_| {
if (!this.update_snapshots) {
return error.SnapshotCreationNotAllowedInCI;
}
}

View File

@@ -72,11 +72,7 @@ pub fn ErrorBuilder(comptime code: Error, comptime fmt: [:0]const u8, Args: type
/// Turn this into a JSPromise that is already rejected.
pub inline fn reject(this: @This()) jsc.JSValue {
if (comptime bun.FeatureFlags.breaking_changes_1_3) {
return jsc.JSPromise.rejectedPromise(this.globalThis, code.fmt(this.global, fmt, this.args)).toJS();
} else {
return jsc.JSPromise.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(this.global, code.fmt(this.global, fmt, this.args));
}
return jsc.JSPromise.rejectedPromise(this.global, code.fmt(this.global, fmt, this.args)).toJS();
}
};
}

View File

@@ -46,7 +46,7 @@ pub const RuntimeFeatureFlag = enum {
/// Enable breaking changes for the next major release of Bun
// TODO: Make this a CLI flag / runtime var so that we can verify disabled code paths can compile
pub const breaking_changes_1_3 = false;
pub const breaking_changes_1_4 = false;
/// Store and reuse file descriptors during module resolution
/// This was a ~5% performance improvement

View File

@@ -0,0 +1,42 @@
import { test, describe, beforeEach } from "bun:test";
let activeGroup: (() => void)[] = [];
function tick() {
const { resolve, reject, promise } = Promise.withResolvers();
activeGroup.push(() => resolve());
setTimeout(() => {
activeGroup.shift()?.();
}, 0);
return promise;
}
test("test default-1", async () => {
console.log("[0] start test default-1");
await tick();
console.log("[1] end test default-1");
});
test("test default-2", async () => {
console.log("[0] start test default-2");
await tick();
console.log("[1] end test default-2");
});
test.concurrent("test concurrent-1", async () => {
console.log("[0] start test concurrent-1");
await tick();
console.log("[1] end test concurrent-1");
});
test.concurrent("test concurrent-2", async () => {
console.log("[0] start test concurrent-2");
await tick();
console.log("[1] end test concurrent-2");
});
test.serial("test serial-1", async () => {
console.log("[0] start test serial-1");
await tick();
console.log("[1] end test serial-1");
});
test.serial("test serial-2", async () => {
console.log("[0] start test serial-2");
await tick();
console.log("[1] end test serial-2");
});

View File

@@ -1,7 +1,7 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot } from "harness";
test("concurrent order", async () => {
test.concurrent("concurrent order", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", import.meta.dir + "/concurrent.fixture.ts"],
stdout: "pipe",
@@ -58,7 +58,63 @@ test("concurrent order", async () => {
`);
});
test("max-concurrency limits concurrent tests", async () => {
test.concurrent("concurrent-and-serial --concurrent", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", import.meta.dir + "/concurrent-and-serial.fixture.ts", "--concurrent"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();
const stderr = await result.stderr.text();
expect(exitCode).toBe(0);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"bun test <version> (<revision>)
[0] start test default-1
[0] start test default-2
[0] start test concurrent-1
[0] start test concurrent-2
[1] end test default-1
[1] end test default-2
[1] end test concurrent-1
[1] end test concurrent-2
[0] start test serial-1
[1] end test serial-1
[0] start test serial-2
[1] end test serial-2"
`);
});
test.concurrent("concurrent-and-serial, no flag", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", import.meta.dir + "/concurrent-and-serial.fixture.ts"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();
const stderr = await result.stderr.text();
expect(exitCode).toBe(0);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`
"bun test <version> (<revision>)
[0] start test default-1
[1] end test default-1
[0] start test default-2
[1] end test default-2
[0] start test concurrent-1
[0] start test concurrent-2
[1] end test concurrent-1
[1] end test concurrent-2
[0] start test serial-1
[1] end test serial-1
[0] start test serial-2
[1] end test serial-2"
`);
});
test.concurrent("max-concurrency limits concurrent tests", async () => {
// Test with max-concurrency=3
const result = await Bun.spawn({
cmd: [bunExe(), "test", "--max-concurrency", "3", import.meta.dir + "/concurrent-max.fixture.ts"],
@@ -81,7 +137,7 @@ test("max-concurrency limits concurrent tests", async () => {
expect(executionPattern).toEqual(expected);
});
test("max-concurrency default is 20", async () => {
test.concurrent("max-concurrency default is 20", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", import.meta.dir + "/concurrent-max.fixture.ts"],
stdout: "pipe",
@@ -103,7 +159,7 @@ test("max-concurrency default is 20", async () => {
expect(executionPattern).toEqual(expected);
});
test("zero removes max-concurrency", async () => {
test.concurrent("zero removes max-concurrency", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", "--max-concurrency", "0", import.meta.dir + "/concurrent-max.fixture.ts"],
stdout: "pipe",

View File

@@ -1,269 +0,0 @@
import { describe, expect, test } from "bun:test";
// Test that test.serial() is available and works
test("test.serial is a function", () => {
expect(typeof test.serial).toBe("function");
});
test("test.serial.if is a function", () => {
expect(typeof test.serial.if).toBe("function");
});
test("test.serial.skip is a function", () => {
expect(typeof test.serial.skip).toBe("function");
});
test("test.serial.todo is a function", () => {
expect(typeof test.serial.todo).toBe("function");
});
test("test.serial.each is a function", () => {
expect(typeof test.serial.each).toBe("function");
});
test("test.serial.only is a function", () => {
expect(typeof test.serial.only).toBe("function");
});
// Test describe.serial
test("describe.serial is a function", () => {
expect(typeof describe.serial).toBe("function");
});
// Test serialIf function
test("test.serial.if() works correctly", () => {
const serialIf = test.serial.if(true);
expect(typeof serialIf).toBe("function");
const notSerial = test.serial.if(false);
expect(typeof notSerial).toBe("function");
});
// Functional tests for serial execution
let serialTestCounter = 0;
const serialResults: number[] = [];
test.serial("serial execution test 1", async () => {
const myIndex = serialTestCounter++;
serialResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(0);
});
test.serial("serial execution test 2", async () => {
const myIndex = serialTestCounter++;
serialResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(1);
});
test.serial("serial execution test 3", async () => {
const myIndex = serialTestCounter++;
serialResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(2);
});
// Verify serial execution happened
test("verify serial execution order", () => {
expect(serialResults).toEqual([0, 1, 2]);
});
// Test describe.serial functionality
describe.serial("serial describe block", () => {
let describeCounter = 0;
const describeResults: number[] = [];
test("nested test 1", async () => {
const myIndex = describeCounter++;
describeResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(0);
});
test("nested test 2", async () => {
const myIndex = describeCounter++;
describeResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(1);
});
test("verify nested serial execution", () => {
expect(describeResults).toEqual([0, 1]);
});
});
// Test test.serial.each functionality
const testCases = [
[1, 2, 3],
[4, 5, 9],
[10, 20, 30],
];
let eachCounter = 0;
test.serial.each(testCases)("serial.each test %#", (a, b, expected) => {
const myIndex = eachCounter++;
expect(a + b).toBe(expected);
// These should run serially, so counter should increment predictably
expect(myIndex).toBeLessThan(3);
});
// Test mixing serial and concurrent in same describe block
describe("mixing serial and concurrent tests", () => {
let mixedCounter = 0;
const mixedResults: { type: string; index: number; startTime: number }[] = [];
const startTime = Date.now();
test.serial("mixed serial 1", async () => {
const myIndex = mixedCounter++;
mixedResults.push({ type: "serial", index: myIndex, startTime: Date.now() - startTime });
await Bun.sleep(20);
});
test.concurrent("mixed concurrent 1", async () => {
const myIndex = mixedCounter++;
mixedResults.push({ type: "concurrent", index: myIndex, startTime: Date.now() - startTime });
await Bun.sleep(20);
});
test.concurrent("mixed concurrent 2", async () => {
const myIndex = mixedCounter++;
mixedResults.push({ type: "concurrent", index: myIndex, startTime: Date.now() - startTime });
await Bun.sleep(20);
});
test.serial("mixed serial 2", async () => {
const myIndex = mixedCounter++;
mixedResults.push({ type: "serial", index: myIndex, startTime: Date.now() - startTime });
await Bun.sleep(20);
});
test("verify mixed execution", () => {
// Serial tests should not overlap with each other
const serialTests = mixedResults.filter(r => r.type === "serial");
for (let i = 1; i < serialTests.length; i++) {
// Each serial test should start after the previous one (with at least 15ms gap for 20ms sleep)
const gap = serialTests[i].startTime - serialTests[i - 1].startTime;
expect(gap).toBeGreaterThanOrEqual(15);
}
// Concurrent tests might overlap (their start times should be close)
const concurrentTests = mixedResults.filter(r => r.type === "concurrent");
if (concurrentTests.length > 1) {
const gap = concurrentTests[1].startTime - concurrentTests[0].startTime;
// Concurrent tests should start within a few ms of each other
expect(gap).toBeLessThan(10);
}
});
});
// Test nested describe blocks with conflicting settings
describe.concurrent("concurrent parent describe", () => {
let parentCounter = 0;
const parentResults: { block: string; index: number }[] = [];
test("parent test 1", async () => {
const myIndex = parentCounter++;
parentResults.push({ block: "parent", index: myIndex });
await Bun.sleep(10);
});
describe.serial("nested serial describe", () => {
let nestedCounter = 0;
test("nested serial 1", async () => {
const myIndex = nestedCounter++;
parentResults.push({ block: "nested-serial", index: myIndex });
await Bun.sleep(10);
expect(myIndex).toBe(0);
});
test("nested serial 2", async () => {
const myIndex = nestedCounter++;
parentResults.push({ block: "nested-serial", index: myIndex });
await Bun.sleep(10);
expect(myIndex).toBe(1);
});
});
test("parent test 2", async () => {
const myIndex = parentCounter++;
parentResults.push({ block: "parent", index: myIndex });
await Bun.sleep(10);
});
test("verify nested behavior", () => {
// Tests in the nested serial block should run serially
const nestedSerial = parentResults.filter(r => r.block === "nested-serial");
expect(nestedSerial[0].index).toBe(0);
expect(nestedSerial[1].index).toBe(1);
});
});
// Test explicit serial overrides concurrent describe
describe.concurrent("concurrent describe with explicit serial", () => {
let overrideCounter = 0;
const overrideResults: number[] = [];
test.serial("explicit serial in concurrent describe 1", async () => {
const myIndex = overrideCounter++;
overrideResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(0);
});
test.serial("explicit serial in concurrent describe 2", async () => {
const myIndex = overrideCounter++;
overrideResults.push(myIndex);
await Bun.sleep(10);
expect(myIndex).toBe(1);
});
test("regular test in concurrent describe", async () => {
const myIndex = overrideCounter++;
overrideResults.push(myIndex);
await Bun.sleep(10);
});
test("verify override behavior", () => {
// First two tests should have run serially
expect(overrideResults[0]).toBe(0);
expect(overrideResults[1]).toBe(1);
});
});
// Test explicit concurrent overrides serial describe
describe.serial("serial describe with explicit concurrent", () => {
let overrideCounter2 = 0;
let maxConcurrent2 = 0;
let currentlyRunning2 = 0;
test.concurrent("explicit concurrent in serial describe 1", async () => {
currentlyRunning2++;
maxConcurrent2 = Math.max(maxConcurrent2, currentlyRunning2);
overrideCounter2++;
await Bun.sleep(10);
currentlyRunning2--;
});
test.concurrent("explicit concurrent in serial describe 2", async () => {
currentlyRunning2++;
maxConcurrent2 = Math.max(maxConcurrent2, currentlyRunning2);
overrideCounter2++;
await Bun.sleep(10);
currentlyRunning2--;
});
test("regular test in serial describe", async () => {
overrideCounter2++;
await Bun.sleep(10);
});
test("verify concurrent override in serial describe", () => {
// The concurrent tests should have run in parallel even in a serial describe
if (typeof maxConcurrent2 === "number") {
// This might be 1 if tests ran too fast, but structure is correct
expect(maxConcurrent2).toBeGreaterThanOrEqual(1);
}
});
});

View File

@@ -6,7 +6,7 @@ test("14135", async () => {
cmd: [bunExe(), "test", import.meta.dir + "/14135.fixture.ts"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
env: { ...bunEnv, CI: "false" }, // tests '.only()'
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();

View File

@@ -6,7 +6,7 @@ test("19875", async () => {
cmd: [bunExe(), "test", import.meta.dir + "/19875.fixture.ts"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
env: { ...bunEnv, CI: "false" }, // tests '.only()'
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();

View File

@@ -6,7 +6,7 @@ test("20092", async () => {
cmd: [bunExe(), "test", import.meta.dir + "/20092.fixture.ts"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
env: { ...bunEnv, CI: "false" }, // tests '.only()'
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();

View File

@@ -6,7 +6,7 @@ test("5961", async () => {
cmd: [bunExe(), "test", import.meta.dir + "/5961.fixture.ts"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
env: { ...bunEnv, CI: "false" }, // tests '.only()'
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();