Skip to content

Commit fbafb3a

Browse files
Move fail to core-utils (#23955)
## Description Now that assert tagging can support `fail`, and that's confirmed to be working in tree, it makes sense to broaden the availability of this utility function. ## Breaking Changes This will impact the call stack for asserts by adding "fail" to the top, and the call stacks of "fail" by adding "assert" to the top. Since such errors are mostly aggregated by their assert tag, and not their stack, this shouldn't be a big deal, but it could have some impact. ## Reviewer Guidance The review process is outlined on [this wiki page](https://github.com/microsoft/FluidFramework/wiki/PR-Guidelines#guidelines).
1 parent ba70b40 commit fbafb3a

File tree

5 files changed

+139
-27
lines changed

5 files changed

+139
-27
lines changed

packages/common/core-utils/src/assert.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,31 @@
3131
*/
3232
export function assert(condition: boolean, message: string | number): asserts condition {
3333
if (!condition) {
34-
throw new Error(
35-
typeof message === "number" ? `0x${message.toString(16).padStart(3, "0")}` : message,
36-
);
34+
fail(message);
3735
}
3836
}
3937

38+
/**
39+
* Throw an error with a constant message.
40+
* @remarks
41+
* Works like {@link assert}, but errors unconditionally instead of taking in a condition.
42+
*
43+
* Unlike `assert`, this `fail` is not "tagged" by the assert tagging too by default.
44+
* Use a `assertTagging.config.mjs` file to enable this and any other assert tagging customizations as needed.
45+
*
46+
* Returns `never` so it can be used inline as part of an expression, or as a return value.
47+
* @example
48+
* ```ts
49+
* const x: number = numbersMap.get("foo") ?? fail("foo missing from map");
50+
* ```
51+
* @internal
52+
*/
53+
export function fail(message: string | number): never {
54+
throw new Error(
55+
typeof message === "number" ? `0x${message.toString(16).padStart(3, "0")}` : message,
56+
);
57+
}
58+
4059
/**
4160
* Asserts that can be conditionally enabled in debug/development builds but will be optimized out of production builds.
4261
*

packages/common/core-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
export {
77
assert,
8+
fail,
89
debugAssert,
910
configureDebugAsserts,
1011
nonProductionConditionalsIncluded,

packages/dds/tree/src/util/utils.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,8 @@ export function asMutable<T>(readonly: T): Mutable<T> {
4747

4848
export const clone = structuredClone;
4949

50-
/**
51-
* Throw an error with a constant message.
52-
* @remarks
53-
* Works like {@link @fluidframework/core-utils/internal#assert}.
54-
*/
55-
export function fail(message: string | number): never {
56-
// Declaring this here aliased to a different name avoids the assert tagging objecting to the usages of `assert` below.
57-
// Since users of `fail` do the assert message tagging instead, suppressing tagging errors here makes sense.
58-
const assertNoTag: (condition: boolean, message: string | number) => asserts condition =
59-
assert;
60-
assertNoTag(false, message);
61-
}
50+
// TODO: update usages of this to use @fluidframework/core-utils/internal directly.
51+
export { fail } from "@fluidframework/core-utils/internal";
6252

6353
/**
6454
* Checks whether or not the given object is a `readonly` array.

packages/test/stochastic-test-utils/src/minification.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,18 +206,7 @@ export class FuzzTestMinimizer<TOperation extends BaseOperation> {
206206
return false;
207207
}
208208

209-
const stackLines = error.stack.split("\n").map((s) => s.trim());
210-
211-
const stackTop = stackLines.findIndex((s) => s.startsWith("at"));
212-
213-
const message = stackLines[stackTop].startsWith("at assert ")
214-
? // Reproduce based on the final two lines+col of the error if it is an assert error
215-
// This ensures the same assert is triggered by the minified test
216-
stackLines
217-
.slice(stackTop, stackTop + 2)
218-
.join("\n")
219-
: // Otherwise the final line is sufficient
220-
stackLines[stackTop];
209+
const message = extractMessage(error.stack);
221210

222211
if (this.initialError === undefined) {
223212
this.initialError = { message, op: lastOp };
@@ -230,3 +219,25 @@ export class FuzzTestMinimizer<TOperation extends BaseOperation> {
230219
}
231220
}
232221
}
222+
223+
/**
224+
* Collect relevant top portion of the stack.
225+
* Include enough lines that the error doesn't look the same as too many other errors,
226+
* but few enough that the stack doesn't include details not relevant to the error.
227+
*/
228+
export function extractMessage(stack: string): string {
229+
const stackLines = stack.split("\n").map((s) => s.trim());
230+
231+
const stackTop = stackLines.findIndex((s) => s.startsWith("at"));
232+
233+
const linesToKeep: string[] = [];
234+
for (const line of stackLines.slice(stackTop)) {
235+
linesToKeep.push(line);
236+
// Heuristically continue including lines if stack line matches this pattern:
237+
if (!line.match(/^at (assert|fail) /)) {
238+
break;
239+
}
240+
}
241+
242+
return linesToKeep.join("\n");
243+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { strict as assert } from "assert";
7+
8+
import { assert as coreAssert, fail } from "@fluidframework/core-utils/internal";
9+
10+
import { extractMessage } from "../minification.js";
11+
12+
function requireLines(s: string, count = 1): void {
13+
assert.equal(s.split("\n").length, count, `Expected ${count} lines in:\n${s}`);
14+
}
15+
16+
describe("Minification", () => {
17+
describe("extractMessage", () => {
18+
it("non assert", () => {
19+
function namedFunction() {
20+
return new Error("message");
21+
}
22+
const e = namedFunction();
23+
assert(e.stack !== undefined);
24+
const message = extractMessage(e.stack);
25+
requireLines(message);
26+
assert.match(message, /^at namedFunction .*minification\.spec\.ts/);
27+
});
28+
29+
it("node assert", () => {
30+
function namedFunction(): Error {
31+
try {
32+
assert(false);
33+
} catch (err: unknown) {
34+
return err as Error;
35+
}
36+
}
37+
const e = namedFunction();
38+
assert(e.stack !== undefined);
39+
const message = extractMessage(e.stack);
40+
requireLines(message);
41+
assert.match(message, /^at namedFunction .*minification\.spec\.ts/);
42+
});
43+
44+
it("core assert", () => {
45+
function namedFunction(): Error {
46+
try {
47+
coreAssert(false, "message");
48+
} catch (err: unknown) {
49+
return err as Error;
50+
}
51+
}
52+
const e = namedFunction();
53+
assert(e.stack !== undefined);
54+
const message = extractMessage(e.stack);
55+
requireLines(message, 3);
56+
assert.match(message, /\nat namedFunction .*minification\.spec\.ts/);
57+
});
58+
59+
it("node fail", () => {
60+
function namedFunction(): Error {
61+
try {
62+
assert.fail();
63+
} catch (err: unknown) {
64+
return err as Error;
65+
}
66+
}
67+
const e = namedFunction();
68+
assert(e.stack !== undefined);
69+
const message = extractMessage(e.stack);
70+
// Interestingly assert.fail() doesn't include itself in the stack.
71+
requireLines(message);
72+
assert.match(message, /^at namedFunction .*minification\.spec\.ts/);
73+
});
74+
75+
it("core fail", () => {
76+
function namedFunction(): Error {
77+
try {
78+
fail("message");
79+
} catch (err: unknown) {
80+
return err as Error;
81+
}
82+
}
83+
const e = namedFunction();
84+
assert(e.stack !== undefined);
85+
const message = extractMessage(e.stack);
86+
requireLines(message, 2);
87+
assert.match(message, /^at fail/);
88+
assert.match(message, /\nat namedFunction .*minification\.spec\.ts/);
89+
});
90+
});
91+
});

0 commit comments

Comments
 (0)