Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions core/src/assert/adapters/exprAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
* Licensed under the MIT license.
*/

import { arrForEach, arrSlice, dumpObj, fnApply, isArray, isFunction, isNumber, isString, objDefine, objKeys, strEndsWith, strIndexOf, strLeft, strStartsWith } from "@nevware21/ts-utils";
import {
arrForEach, arrSlice, dumpObj, fnApply, isArray, isFunction, isNumber, isString,
objDefine, objKeys, strEndsWith, strIndexOf, strLeft, strStartsWith
} from "@nevware21/ts-utils";
import { IAssertScope } from "../interface/IAssertScope";
import { IScopeContext } from "../interface/IScopeContext";
import { IScopeFn } from "../interface/IScopeFuncs";
Expand Down Expand Up @@ -136,7 +139,7 @@ function _runExpr(theAssert: any, scope: IAssertScope, steps: IStepDef[], scopeF

if (!(step.name in scope.that)) {
throw new AssertionError(
"" + idx + " Invalid step: " + step.name + " for [" + steps.map((s: any) => s.name).join("->") + "] available steps: [" + objKeys(scope.that).join(";") + "] - " + _formatValue(scope.context.opts, scope.that),
"" + idx + " Invalid step: " + step.name + " for [" + steps.map((s: any) => s.name).join("->") + "] available steps: [" + objKeys(scope.that).join(";") + "] - " + _formatValue(scope.context, scope.that),
{
expected: step.name,
actual: objKeys(scope.that).join(";")
Expand Down Expand Up @@ -191,7 +194,7 @@ function _processFn(scope: IAssertScope, scopeFn: IScopeFn, theArgs: any[], theR
// The last step is a function, so we call it
theResult = scope.exec(theResult, theArgs);
if (scope.context.opts.isVerbose) {
scope.context.setOp("=>[[r:" + _formatValue(scope.context.opts, theResult) + "]]");
scope.context.setOp("=>[[r:" + _formatValue(scope.context, theResult) + "]]");
}
}

Expand Down
146 changes: 106 additions & 40 deletions core/src/assert/assertClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { IPromise } from "@nevware21/ts-async";
import { IAssertClass } from "./interface/IAssertClass";
import { IAssertInst } from "./interface/IAssertInst";
import { MsgSource } from "./type/MsgSource";
import { isBooleanFunc, isFunctionFunc, isNumberFunc, isNullFunc, isObjectFunc, isPlainObjectFunc, isStrictFalseFunc, isStrictTrueFunc, isUndefinedFunc } from "./funcs/is";
import {
isBooleanFunc, isFunctionFunc, isNumberFunc, isNullFunc, isObjectFunc, isPlainObjectFunc, isStrictFalseFunc, isStrictTrueFunc, isUndefinedFunc
} from "./funcs/is";
import { isEmptyFunc } from "./funcs/isEmpty";
import { IAssertClassDef } from "./interface/IAssertClassDef";
import { matchFunc } from "./funcs/match";
Expand Down Expand Up @@ -487,29 +489,6 @@ function _createLazyInstHandler(target: any, propName: string, argDef: IAssertCl
});
}

function _extractInitMsg(theArgs: any[], numArgs?: number, mIdx?: number): string {
// Extract the message if its present to be passed to the scope context
let msg: string;
if (!isUndefined(mIdx)) {
if (mIdx >= 0) {
// Positive index
if (theArgs.length > mIdx) {
msg = theArgs[mIdx];
theArgs.splice(mIdx, 1);
}
} else {
// Negative index
let idx = theArgs.length + mIdx;
if (idx >= 0 && idx < theArgs.length && numArgs >= 0 && numArgs < theArgs.length) {
msg = theArgs[idx];
theArgs.splice(idx, 1);
}
}
}

return msg;
}

function _createAliasFunc(alias: string) {
return function _aliasProxyFunc(): IAssertInst | IPromise<IAssertInst> {
let _this = this;
Expand All @@ -525,6 +504,106 @@ function _createAliasFunc(alias: string) {
};
}

/**
* @internal
* @ignore
* Extracts the actual value, message and scope arguments from the provided arguments based on the definition of the assertion function.
* @param theArgs - The arguments passed to the assertion function, this is expected to be an array of arguments.
* @param mIdx - The index of the message argument in the arguments array, this can be a positive index (0-based)
* or a negative index (from the end of the array), or undefined if no message extraction is needed.
* @param numArgs - The number of arguments expected by the assertion function.
* @returns An object containing the extracted message, actual value, and remaining arguments.
*/
export function _extractArgs(theArgs: any[], mIdx: number | undefined, numArgs: number): { msg?: string, act?: any, args: any[], orgArgs: any[], m?: number } {
const orgArgsLen = theArgs.length;
let initMsg: string;
let actualValue: any;
let scopeArgs: any[];
let startArgsIdx: number = 0;
let mode: number;

// -------------------------------------------------------------------------
// Extract the init message if its present to be passed to the scope context
// -------------------------------------------------------------------------
let initMsgIdx: number = -1;
if (mIdx >= 0) {
// Positive index
if (orgArgsLen > mIdx) {
initMsg = theArgs[mIdx];
initMsgIdx = mIdx;
if (initMsgIdx === 0) {
// If the message index is the first argument, then we can start extracting the actual value and scope args from the second argument
startArgsIdx = 1;
}
}
} else if (!isUndefined(mIdx)) {
// Negative index - replicates: if (idx >= 0 && idx < theArgs.length && numArgs >= 0 && numArgs < theArgs.length)
let idx = orgArgsLen + mIdx;
if (idx >= 0 && idx < orgArgsLen && numArgs >= 0 && numArgs < orgArgsLen) {
initMsg = theArgs[idx];
initMsgIdx = idx;
if (initMsgIdx === 0) {
// If the message index is the first argument, then we can start extracting the actual value and scope args from the second argument
startArgsIdx = 1;
}
}
}

if (numArgs > 0 && startArgsIdx < orgArgsLen && initMsgIdx !== startArgsIdx) {
actualValue = theArgs[startArgsIdx];
startArgsIdx++;
}

// -------------------------------------------------------------------------
// Extract the scope arguments, we need to simulate the array as if the message argument
// was removed (if it was present) to correctly extract the scope arguments
// -------------------------------------------------------------------------
if (initMsgIdx < startArgsIdx) {
// If we don't have a message argument, then we can just take the remaining arguments as scope arguments
if (startArgsIdx === 0) {
// Just return the original arguments as scope arguments if we don't have a message argument and we are not skipping any arguments
mode = 0;
scopeArgs = theArgs;
} else if (startArgsIdx < orgArgsLen) {
mode = 1;
scopeArgs = arrSlice(theArgs, startArgsIdx);
} else {
// Just return an empty array as scope arguments if we don't have any arguments after the actual value argument
mode = 2;
scopeArgs = [];
}
} else {
// If we have a message argument, then we need to skip it when extracting the scope arguments
if (initMsgIdx === orgArgsLen - 1) {
// If the message argument is the last argument, then we can just take the remaining arguments as scope arguments without the need to skip any arguments
if (startArgsIdx < orgArgsLen - 1) {
mode = 11;
scopeArgs = arrSlice(theArgs, startArgsIdx, orgArgsLen - 1);
} else {
// Just return an empty array as scope arguments if we don't have any arguments after the message argument
mode = 12;
scopeArgs = [];
}
} else {
mode = 10;
scopeArgs = [];
for (let i = startArgsIdx; i < orgArgsLen; i++) {
if (i !== initMsgIdx) {
scopeArgs.push(theArgs[i]);
}
}
}
}

return {
msg: initMsg,
act: actualValue,
args: scopeArgs,
orgArgs: theArgs,
m: mode
};
}

function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAssertClassDef, internalErrorStackStart: Function, responseHandler: (result: any) => any): (...args: any[]) => void | IAssertInst | IPromise<IAssertInst> {
// let steps: IStepDef[];
let scopeFn: IScopeFn = def.scopeFn;
Expand All @@ -546,24 +625,11 @@ function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAss
}

return function _assertFunc(): void | IAssertInst | IPromise<IAssertInst> {
let theArgs = arrSlice(arguments, 0);
let orgArgs = arrSlice(theArgs);

// Extract the initial message from the passed arguments (if present)
let initMsg = _extractInitMsg(theArgs, numArgs, mIdx);
let scopeArgs: any[];

// Extract the actual value from the arguments
let actualValue: any;
if (theArgs.length > 0 && numArgs > 0) {
// Get the actual "value" from the first argument
actualValue = theArgs[0];
scopeArgs = arrSlice(theArgs, 1);
}
let theArgs = _extractArgs(arrSlice(arguments), mIdx, numArgs);

// Create the initial scope `expect(value, initMsg)` and run any defined steps
// Using either the current alias entry point or the current function
let newScope = createAssertScope(createContext(actualValue, initMsg, _aliasStackStart || _assertFunc, orgArgs));
let newScope = createAssertScope(createContext(theArgs.act, theArgs.msg, _aliasStackStart || _assertFunc, theArgs.orgArgs));
newScope.context._$stackFn.push(_aliasStackStart || _assertFunc);

newScope.context.setOp(assertName + "()");
Expand All @@ -572,7 +638,7 @@ function _createProxyFunc(theAssert: IAssertClass, assertName: string, def: IAss
v: newScope.context
});

let theResult = newScope.exec(scopeFn, scopeArgs || theArgs, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous") as void | IAssertInst | IPromise<IAssertInst>;
let theResult = newScope.exec(scopeFn, theArgs.args, scopeFn.name || (scopeFn as any)["displayName"] || "anonymous") as void | IAssertInst | IPromise<IAssertInst>;

return responseHandler ? responseHandler(theResult) : theResult;
};
Expand Down
25 changes: 13 additions & 12 deletions core/src/assert/assertScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* @nevware21/tripwire
* https://github.com/nevware21/tripwire
*
* Copyright (c) 2024-2025 NevWare21 Solutions LLC
* Copyright (c) 2024-2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

import { arrSlice, objDefineProps } from "@nevware21/ts-utils";
import { arrSlice, objDefine } from "@nevware21/ts-utils";
import { IScopeContext, IScopeContextOverrides } from "./interface/IScopeContext";
import { AssertScopeFuncDefs } from "./interface/IAssertInst";
import { MsgSource } from "./type/MsgSource";
Expand Down Expand Up @@ -47,16 +47,11 @@ export function createAssertScope(context: IScopeContext, handlerCreator?: Asser
createOperation: createOperation
};

let _that: any = null; // Initialize with the `that` the same as the IAssertInst

objDefineProps(theScope, {
context: {
g: () => _context
},
"that": {
g: () => _that as any,
s: (v: any) => _that = v
}
// Define context as a value property (not getter) - fast reads, controlled writes
// w: false makes it readonly to external callers
objDefine(theScope, "context", {
v: _context,
w: false
});

function newScope<V>(value?: V): IAssertScope {
Expand Down Expand Up @@ -105,6 +100,12 @@ export function createAssertScope(context: IScopeContext, handlerCreator?: Asser
function updateCtx<T>(value: T, overrides?: IScopeContextOverrides): IAssertScope {
if (value !== _context.value || overrides) {
_context = _context.new(value, overrides);
// Update the context property value directly to avoid the overhead of redefining
// the property and to preserve the readonly nature of the property for external callers.
objDefine(theScope, "context", {
v: _context,
w: false
});
}

return theScope;
Expand Down
6 changes: 3 additions & 3 deletions core/src/assert/assertionError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ function _formatProps(ctx: IScopeContext | null, props: any): string {
thePath = props.opPath.join("->") + "->" + lastOp;
}
} else {
thePath = _formatValue(ctx.opts, props.opPath) + "->" + lastOp;
thePath = _formatValue(ctx, props.opPath) + "->" + lastOp;
}
} else {
thePath = _formatValue(ctx.opts, props.opPath);
thePath = _formatValue(ctx, props.opPath);
}

let parts: string[] = [formatted, "running \"", thePath, "\""];
if (props.actual) {
parts.push(" with (", _formatValue(ctx.opts, props.actual), ")");
parts.push(" with (", _formatValue(ctx, props.actual), ")");
}
let leftOver: any = {};
objForEachKey(props, (key, value) => {
Expand Down
2 changes: 1 addition & 1 deletion core/src/assert/funcs/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ function _isVisiting<T>(value: any, options: IEqualOptions, cb: () => T): T {
visitCount++;
if (visitCount > 10) {
// Early exit on excessive visits
let errorMsg = "Unresolvable Circular reference detected for " + _formatValue(options.context.opts, visiting[0]) + " @ depth " + depth + " reference count: " + visitCount;
let errorMsg = "Unresolvable Circular reference detected for " + _formatValue(options.context, visiting[0]) + " @ depth " + depth + " reference count: " + visitCount;
options.context.fatal(errorMsg);
}
}
Expand Down
Loading