Repository: denoland/deno
Pull request: fix(test): allow test functions to override reported location

## Summary

Fixes #20047.

In `cli/js/40_test.js`, `testInner` now checks whether the registered test function carries a `Symbol.for("Deno.test.location")` property before falling back to `core.currentUserCallSite()`. If the symbol is set, its string value (format: `"fileName:lineNumber:columnNumber"`) is parsed and used as the test location. This allows test-helper libraries like `@std/testing/bdd` to declare the user's source location instead of the library's own call site.

---

## Root Cause

In `cli/js/40_test.js:302` (before this fix):

```js
testDesc.location = core.currentUserCallSite();
```

`core.currentUserCallSite()` captures the stack frame where `Deno.test()` is called. When a BDD library such as `@std/testing/bdd` wraps user tests and calls `Deno.test()` internally, the captured location is inside the library file (e.g. `https://jsr.io/@std/testing/.../bdd.ts`), not the user's test file.

The JUnit reporter uses `description.location.file_name` to both name the `<testsuite>` element and populate each `<testcase classname="...">`. With the wrong location, every test registered via `bdd.ts` is grouped under the library URL:

```xml
<!-- Before fix -->
<testsuite name="https://jsr.io/@std/testing/1.0.0/bdd.ts" ...>
```

This breaks CI test-reporting tools (e.g. `dorny/test-reporter`, `EnricoMi/publish-unit-test-result-action`) that rely on the suite name to map results back to source files.

---

## Solution

Before reading `core.currentUserCallSite()`, check for a well-known symbol on the test function:

```js
const locationOverride = testDesc.fn[SymbolFor("Deno.test.location")];
testDesc.location = locationOverride
  ? parseTestLocation(locationOverride)
  : core.currentUserCallSite();
```

`parseTestLocation` splits `"fileName:lineNumber:columnNumber"` from the **right**, so URL schemes containing `:` (e.g. `file:///…`, `https://…`) are handled correctly.

A follow-up change to `@std/testing/bdd` (in the `std` repo) will set `Symbol.for("Deno.test.location")` on each wrapped test function to point at the user's source file, completing the fix end-to-end.

---

## Testing

- Added `tests/specs/test/junit_location_override/` — a new spec test that simulates a library-registered test (by setting `Symbol.for("Deno.test.location")` on the function) alongside an ordinary test, and asserts that the JUnit XML groups them into separate `<testsuite>` elements with the correct names and line/column numbers.
- All six existing JUnit spec tests continue to pass unchanged.

```
test specs::test::hide_stacktraces::reporter_junit ... ok
test specs::test::junit                           ... ok
test specs::test::junit_console_formatting        ... ok
test specs::test::junit_location_override         ... ok  ← new
test specs::test::junit_multiple_test_files       ... ok
test specs::test::junit_nested                    ... ok
```

---

## Checklist

- [x] Fixes the root cause (not just the symptom) — the location is captured correctly at registration time
- [x] New spec test covers the exact scenario from the issue report
- [x] All existing JUnit spec tests pass
- [x] No unrelated changes
- [x] Code style matches surrounding primordials pattern (`StringPrototypeLastIndexOf`, `StringPrototypeSlice`, `SymbolFor`)
- [x] Read CONTRIBUTING.md and followed its requirements

Changed files:
filename: cli/js/40_test.js
patch: @@ -20,9 +20,13 @@ const {
   DateNow,
   Error,
   Map,
+  NumberIsNaN,
   MapPrototypeGet,
   MapPrototypeSet,
   SafeArrayIterator,
+  StringPrototypeLastIndexOf,
+  StringPrototypeSlice,
+  SymbolFor,
   SymbolToStringTag,
   TypeError,
 } = primordials;
@@ -81,6 +85,51 @@ const DenoNs = globalThis.Deno;
 /** @type {Map<number, TestState | TestStepState>} */
 const testStates = new Map();
 
+/**
+ * Symbol that test functions (or their wrappers) can carry to tell the test
+ * runner which source location to report for the test, instead of the call
+ * site of `Deno.test()` itself.
+ *
+ * The value must be a string in the format `"fileName:lineNumber:columnNumber"`,
+ * where `fileName` may be an absolute file URL or any remote URL.  Parsing
+ * works from the right so that URL schemes (which also contain `:`) are
+ * handled correctly.
+ *
+ * This is primarily intended for test-helper libraries (e.g. `@std/testing/bdd`)
+ * that call `Deno.test()` on behalf of the user: by setting this symbol on the
+ * wrapped function they can report the location in *user* code rather than the
+ * location inside the library itself.
+ */
+const TEST_LOCATION_SYMBOL = SymbolFor("Deno.test.location");
+
+/**
+ * Parse a location string of the form `"fileName:lineNumber:columnNumber"`.
+ * Parsing is done from the right so that file names that contain `:` (such as
+ * `file://` or `https://` URLs) are handled correctly.
+ *
+ * Returns `null` if the string is not a valid location.
+ *
+ * @param {string} str
+ * @returns {{ fileName: string, lineNumber: number, columnNumber: number } | null}
+ */
+function parseTestLocation(str) {
+  if (typeof str !== "string") return null;
+  const lastColon = StringPrototypeLastIndexOf(str, ":");
+  if (lastColon <= 0) return null;
+  const secondLastColon = StringPrototypeLastIndexOf(str, ":", lastColon - 1);
+  if (secondLastColon <= 0) return null;
+  const lineNumber = parseInt(
+    StringPrototypeSlice(str, secondLastColon + 1, lastColon),
+  );
+  const columnNumber = parseInt(StringPrototypeSlice(str, lastColon + 1));
+  if (NumberIsNaN(lineNumber) || NumberIsNaN(columnNumber)) return null;
+  return {
+    fileName: StringPrototypeSlice(str, 0, secondLastColon),
+    lineNumber,
+    columnNumber,
+  };
+}
+
 // Wrap test function in additional assertion that makes sure
 // that the test case does not accidentally exit prematurely.
 function assertExit(fn, isTest) {
@@ -299,7 +348,10 @@ function testInner(
     cachedOrigin = op_test_get_origin();
   }
 
-  testDesc.location = core.currentUserCallSite();
+  const locationOverride = parseTestLocation(
+    testDesc.fn[TEST_LOCATION_SYMBOL],
+  );
+  testDesc.location = locationOverride ?? core.currentUserCallSite();
   testDesc.fn = wrapTest(testDesc);
   testDesc.name = escapeName(testDesc.name);
 

filename: tests/specs/test/junit_location_override/__test__.jsonc
patch: @@ -0,0 +1,4 @@
+{
+  "args": "test --reporter junit main.ts",
+  "output": "main.out"
+}

filename: tests/specs/test/junit_location_override/main.out
patch: @@ -0,0 +1,12 @@
+Check [WILDCARD]main.ts
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites name="deno test" tests="2" failures="0" errors="0" time="[WILDCARD]">
+    <testsuite name="https://jsr.io/@std/testing/1.0.0/bdd.ts" tests="1" disabled="0" errors="0" failures="0">
+        <testcase name="library registered test" classname="https://jsr.io/@std/testing/1.0.0/bdd.ts" time="[WILDCARD]" line="100" col="10">
+        </testcase>
+    </testsuite>
+    <testsuite name="./main.ts" tests="1" disabled="0" errors="0" failures="0">
+        <testcase name="normal test" classname="./main.ts" time="[WILDCARD]" line="13" col="6">
+        </testcase>
+    </testsuite>
+</testsuites>

filename: tests/specs/test/junit_location_override/main.ts
patch: @@ -0,0 +1,13 @@
+// Simulates what a BDD library (e.g. @std/testing/bdd) does when it wraps
+// user tests: it calls Deno.test() internally but sets
+// Symbol.for("Deno.test.location") on the test function so the runner uses
+// the user's source location instead of the library's own file.
+
+// deno-lint-ignore no-explicit-any
+const libraryRegisteredFn: any = () => {};
+libraryRegisteredFn[Symbol.for("Deno.test.location")] =
+  "https://jsr.io/@std/testing/1.0.0/bdd.ts:100:10";
+Deno.test("library registered test", libraryRegisteredFn);
+
+// Ordinary test without a location override — location is this file.
+Deno.test("normal test", () => {});