Repository: denoland/deno
Pull request: fix(ext/node): improve assert.fail

This PR improves `assert.fail` compatibility and enables `parallel/test-assert-fail-deprecation.js` test case.

towards #29801 

Changed files:
filename: ext/node/polyfills/assert.ts
patch: @@ -420,17 +420,56 @@ function notDeepStrictEqual(
   );
 }
 
-function fail(message?: string | Error): never {
-  if (typeof message === "string" || message == null) {
-    throw createAssertionError({
-      message: message ?? "Failed",
-      operator: "fail",
-      generatedMessage: message == null,
-    });
+let warned = false;
+
+function fail(
+  actual?: string | Error,
+  expected?: unknown,
+  message?: string | Error,
+  operator?: string,
+  stackStartFn?: Function,
+): never {
+  const argsLen = arguments.length;
+
+  let internalMessage = false;
+  if (actual == null && argsLen <= 1) {
+    internalMessage = true;
+    message = "Failed";
+  } else if (argsLen === 1) {
+    message = actual;
+    actual = undefined;
   } else {
-    throw message;
+    if (warned === false) {
+      warned = true;
+      // deno-lint-ignore no-process-global
+      process.emitWarning(
+        "assert.fail() with more than one argument is deprecated. " +
+          "Please use assert.strictEqual() instead or only pass a message.",
+        "DeprecationWarning",
+        "DEP0094",
+      );
+    }
+    if (argsLen === 2) {
+      operator = "!=";
+    }
+  }
+
+  if (message instanceof Error) throw message;
+
+  const errArgs = {
+    actual,
+    expected,
+    operator: operator === undefined ? "fail" : operator,
+    stackStartFn: stackStartFn || fail,
+    message,
+  };
+  const err = new AssertionError(errArgs);
+  if (internalMessage) {
+    err.generatedMessage = true;
   }
+  throw err;
 }
+
 function match(actual: string, regexp: RegExp, message?: string | Error) {
   if (arguments.length < 2) {
     throw new ERR_MISSING_ARGS("actual", "regexp");

filename: tests/node_compat/config.jsonc
patch: @@ -247,6 +247,7 @@
       "test-assert-async.js",
       "test-assert-builtins-not-read-from-filesystem.js",
       "test-assert-checktag.js",
+      "test-assert-fail-deprecation.js",
       "test-assert-fail.js",
       "test-assert-if-error.js",
       "test-assert.js",

filename: tests/node_compat/runner/TODO.md
patch: @@ -227,7 +227,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
 - [parallel/test-assert-deep-with-error.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-deep-with-error.js)
 - [parallel/test-assert-deep.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-deep.js)
 - [parallel/test-assert-esm-cjs-message-verify.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-esm-cjs-message-verify.js)
-- [parallel/test-assert-fail-deprecation.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-fail-deprecation.js)
 - [parallel/test-assert-first-line.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-first-line.js)
 - [parallel/test-assert-objects.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-objects.js)
 - [parallel/test-assert-typedarray-deepequal.js](https://github.com/nodejs/node/tree/v23.9.0/test/parallel/test-assert-typedarray-deepequal.js)

filename: tests/node_compat/test/parallel/test-assert-fail-deprecation.js
patch: @@ -0,0 +1,77 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 23.9.0
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
+
+// Flags: --no-warnings
+'use strict';
+
+const { expectWarning } = require('../common');
+const assert = require('assert');
+const { test } = require('node:test');
+
+expectWarning(
+  'DeprecationWarning',
+  'assert.fail() with more than one argument is deprecated. ' +
+    'Please use assert.strictEqual() instead or only pass a message.',
+  'DEP0094'
+);
+
+test('Two args only, operator defaults to "!="', () => {
+  assert.throws(() => {
+    assert.fail('first', 'second');
+  }, {
+    code: 'ERR_ASSERTION',
+    name: 'AssertionError',
+    message: '\'first\' != \'second\'',
+    operator: '!=',
+    actual: 'first',
+    expected: 'second',
+    generatedMessage: true
+  });
+});
+
+test('Three args', () => {
+  assert.throws(() => {
+    assert.fail('ignored', 'ignored', 'another custom message');
+  }, {
+    code: 'ERR_ASSERTION',
+    name: 'AssertionError',
+    message: 'another custom message',
+    operator: 'fail',
+    actual: 'ignored',
+    expected: 'ignored',
+    generatedMessage: false
+  });
+});
+
+test('Three args with custom Error', () => {
+  assert.throws(() => {
+    assert.fail(typeof 1, 'object', new TypeError('another custom message'));
+  }, {
+    name: 'TypeError',
+    message: 'another custom message'
+  });
+});
+
+test('No third arg (but a fourth arg)', () => {
+  assert.throws(() => {
+    assert.fail('first', 'second', undefined, 'operator');
+  }, {
+    code: 'ERR_ASSERTION',
+    name: 'AssertionError',
+    message: '\'first\' operator \'second\'',
+    operator: 'operator',
+    actual: 'first',
+    expected: 'second'
+  });
+});
+
+test('The stackFrameFunction should exclude the foo frame', () => {
+  assert.throws(
+    function foo() { assert.fail('first', 'second', 'message', '!==', foo); },
+    (err) => !/^\s*at\sfoo\b/m.test(err.stack)
+  );
+});