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
5 changes: 5 additions & 0 deletions .nx/version-plans/fix-diagnostics-lingering-process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

Fixes a hang where the harness process kept running after tests finished whenever diagnostics were enabled (via the `diagnostics` config option or `RN_HARNESS_DIAGNOSTICS`). Diagnostics instrumentation replaced subprocess handles returned by adb/simctl with plain promises, so teardown could no longer kill the background `adb logcat` / app-launch processes and they kept the harness alive. Instrumented calls now return the original subprocess handles untouched, so runs with diagnostics enabled terminate cleanly.
29 changes: 29 additions & 0 deletions packages/tools/src/__tests__/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,35 @@ describe('instrumented()', () => {
expect(listeners.size).toBe(0);
});

it('returns hybrid thenables (e.g. nano-spawn Subprocess) untouched, not collapsed to a plain Promise', async () => {
const diagnostics = createDiagnostics({ enabled: true });
const nodeChildProcess = Promise.resolve({ kill: vi.fn() });
const hybrid = Object.assign(Promise.resolve('done'), {
nodeChildProcess,
[Symbol.asyncIterator]: async function* () {
yield 'line-1';
},
});
const target = { startLogcat: () => hybrid };

const wrapped = instrumented(target, 'platform.android.adb', diagnostics);
const result = wrapped.startLogcat();

expect(result).toBe(hybrid);
expect((result as typeof hybrid).nodeChildProcess).toBe(nodeChildProcess);

const lines: string[] = [];
for await (const line of result as AsyncIterable<string>) {
lines.push(line);
}
expect(lines).toEqual(['line-1']);

await wait(5);
const entries = diagnostics.flush();
expect(entries).toHaveLength(1);
expect(entries[0]?.status).toBe('ok');
});

it('spy-based check that fn is invoked once per call', () => {
const diagnostics = createDiagnostics({ enabled: true });
const spy = vi.fn(() => 'ok');
Expand Down
19 changes: 10 additions & 9 deletions packages/tools/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,16 +258,17 @@ export const instrumented = <T extends object>(
'then' in result &&
typeof (result as { then: unknown }).then === 'function'
) {
return (result as Promise<unknown>).then(
(resolved) => {
span.end();
return resolved;
},
(error: unknown) => {
span.fail(error);
throw error;
}
// Some thenables (e.g. nano-spawn's Subprocess) carry extra
// properties/behavior beyond `.then()` (async iteration, a handle
// to the underlying child process, etc). Calling `.then()` and
// returning its result would collapse them into a plain Promise
// and silently drop that shape, so track the span as a side
// effect and always return the original object untouched.
(result as Promise<unknown>).then(
() => span.end(),
(error: unknown) => span.fail(error)
);
return result;
}

span.end();
Expand Down
Loading