|
@@ -20,7 +20,7 @@ let root: string
|
|
|
let ws: string
|
|
let ws: string
|
|
|
let ctx: Context
|
|
let ctx: Context
|
|
|
let fs: LocalFileSystem
|
|
let fs: LocalFileSystem
|
|
|
-let live: LspInstance[] = []
|
|
|
|
|
|
|
+const live: LspInstance[] = []
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
beforeEach(async () => {
|
|
|
root = await realpath(await mkdtemp(join(tmpdir(), 'lsp-inst-')))
|
|
root = await realpath(await mkdtemp(join(tmpdir(), 'lsp-inst-')))
|
|
@@ -33,10 +33,12 @@ beforeEach(async () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
afterEach(async () => {
|
|
|
- for (const instance of live) await instance.dispose()
|
|
|
|
|
- live = []
|
|
|
|
|
- await ctx.fiber.dispose()
|
|
|
|
|
- await rm(root, { recursive: true, force: true })
|
|
|
|
|
|
|
+ const instances = live.splice(0)
|
|
|
|
|
+ const ownedContext = ctx
|
|
|
|
|
+ const directory = root
|
|
|
|
|
+ for (const instance of instances) await instance.dispose()
|
|
|
|
|
+ await ownedContext.fiber.dispose()
|
|
|
|
|
+ await rm(directory, { recursive: true, force: true })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
function makeInstance(
|
|
function makeInstance(
|
|
@@ -202,25 +204,40 @@ describe('LspInstance query and abort', () => {
|
|
|
await instance.dispose()
|
|
await instance.dispose()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('terminates when abort interrupts a backpressured didOpen write', async () => {
|
|
|
|
|
|
|
+ it('terminates when abort interrupts a backpressured didOpen write', async ({ task, signal }) => {
|
|
|
// The fixture consumes initialized, then stops reading. A document larger than the stdio pipe
|
|
// The fixture consumes initialized, then stops reading. A document larger than the stdio pipe
|
|
|
// keeps didOpen's write callback pending until cancellation forces bounded process teardown.
|
|
// keeps didOpen's write callback pending until cancellation forces bounded process teardown.
|
|
|
await writeFile(join(ws, 'a.ts'), 'x'.repeat(2_000_000))
|
|
await writeFile(join(ws, 'a.ts'), 'x'.repeat(2_000_000))
|
|
|
const marker = join(root, 'initialized.log')
|
|
const marker = join(root, 'initialized.log')
|
|
|
|
|
+ const didOpenStarted = Promise.withResolvers<undefined>()
|
|
|
|
|
+ let didOpenFinished = false
|
|
|
const instance = makeInstance({
|
|
const instance = makeInstance({
|
|
|
LSP_FAKE_INITIALIZED_MARKER: marker,
|
|
LSP_FAKE_INITIALIZED_MARKER: marker,
|
|
|
LSP_FAKE_PAUSE_STDIN_AFTER_INITIALIZED: '1',
|
|
LSP_FAKE_PAUSE_STDIN_AFTER_INITIALIZED: '1',
|
|
|
}, {
|
|
}, {
|
|
|
shutdownTimeoutMs: 100,
|
|
shutdownTimeoutMs: 100,
|
|
|
killGraceMs: 100,
|
|
killGraceMs: 100,
|
|
|
|
|
+ }, (stdin, message, done) => {
|
|
|
|
|
+ if ((message as { method?: unknown }).method !== 'textDocument/didOpen') {
|
|
|
|
|
+ stdin.write(encodeMessage(message), done)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ stdin.write(encodeMessage(message), (error) => {
|
|
|
|
|
+ didOpenFinished = true
|
|
|
|
|
+ done(error)
|
|
|
|
|
+ })
|
|
|
|
|
+ didOpenStarted.resolve(undefined)
|
|
|
})
|
|
})
|
|
|
const controller = new AbortController()
|
|
const controller = new AbortController()
|
|
|
- const pending = run(instance, 'goToDefinition', controller.signal)
|
|
|
|
|
- await waitForFile(marker)
|
|
|
|
|
- // Let the client enter the large didOpen write after the fixture has paused stdin.
|
|
|
|
|
- await new Promise<void>(resolve => setTimeout(resolve, 100))
|
|
|
|
|
|
|
+ const outcome = run(instance, 'goToDefinition', controller.signal)
|
|
|
|
|
+ .then(() => undefined, (error: unknown) => error)
|
|
|
|
|
+ await waitForFile(marker, task.timeout, signal)
|
|
|
|
|
+ await didOpenStarted.promise
|
|
|
|
|
+ signal.throwIfAborted()
|
|
|
|
|
+ expect(didOpenFinished).toBe(false)
|
|
|
controller.abort(new Error('didOpen-abort'))
|
|
controller.abort(new Error('didOpen-abort'))
|
|
|
- await expect(pending).rejects.toThrow(/didOpen-abort/)
|
|
|
|
|
|
|
+ const failure = await outcome
|
|
|
|
|
+ expect(() => { throw failure }).toThrow(/didOpen-abort/)
|
|
|
expect(instance.dead).toBe(true)
|
|
expect(instance.dead).toBe(true)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -382,9 +399,10 @@ function failingWriter(method: string, failure = new Error(`fixture ${method} fa
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Wait until a fixture marker exists, bounded so a broken handshake cannot hang the test. */
|
|
/** Wait until a fixture marker exists, bounded so a broken handshake cannot hang the test. */
|
|
|
-async function waitForFile(path: string, timeoutMs = 3000): Promise<void> {
|
|
|
|
|
|
|
+async function waitForFile(path: string, timeoutMs: number, signal: AbortSignal): Promise<void> {
|
|
|
const started = Date.now()
|
|
const started = Date.now()
|
|
|
for (;;) {
|
|
for (;;) {
|
|
|
|
|
+ signal.throwIfAborted()
|
|
|
try {
|
|
try {
|
|
|
await readFile(path)
|
|
await readFile(path)
|
|
|
return
|
|
return
|