disposable.test.ts (5350B)
1 /** 2 * @license 3 * Copyright 2025 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import {describe, it, beforeEach} from 'node:test'; 8 9 import expect from 'expect'; 10 import sinon from 'sinon'; 11 12 import { 13 DisposableStack, 14 disposeSymbol, 15 AsyncDisposableStack, 16 asyncDisposeSymbol, 17 SuppressedError, 18 } from './disposable.js'; 19 20 describe('DisposableStack', () => { 21 let stack: DisposableStack; 22 23 beforeEach(() => { 24 stack = new DisposableStack(); 25 }); 26 27 it('should dispose resources in LIFO order', () => { 28 const dispose1 = sinon.spy(); 29 const dispose2 = sinon.spy(); 30 stack.adopt({}, dispose1); 31 stack.adopt({}, dispose2); 32 stack.dispose(); 33 expect(dispose2.calledBefore(dispose1)).toBeTruthy(); 34 }); 35 36 it('should not dispose resources if already disposed', () => { 37 const dispose = sinon.spy(); 38 stack.adopt({}, dispose); 39 stack.dispose(); 40 stack.dispose(); 41 expect(dispose.calledOnce).toBeTruthy(); 42 }); 43 44 it('should use disposable resources', () => { 45 const resource = { 46 [disposeSymbol]: sinon.spy(), 47 }; 48 stack.use(resource); 49 stack.dispose(); 50 expect(resource[disposeSymbol].calledOnce).toBeTruthy(); 51 }); 52 53 it('should defer disposal callbacks', () => { 54 const onDispose = sinon.spy(); 55 stack.defer(onDispose); 56 stack.dispose(); 57 expect(onDispose.calledOnce).toBeTruthy(); 58 }); 59 60 it('should move resources to a new stack', () => { 61 const dispose = sinon.spy(); 62 stack.adopt({}, dispose); 63 const newStack = stack.move(); 64 expect(stack.disposed).toBeTruthy(); 65 expect(newStack.disposed).toBeFalsy(); 66 newStack.dispose(); 67 expect(dispose.calledOnce).toBeTruthy(); 68 }); 69 70 it('should throw error if moving a disposed stack', () => { 71 stack.dispose(); 72 expect(() => { 73 return stack.move(); 74 }).toThrow(ReferenceError); 75 }); 76 77 it('should collect errors from disposals', async () => { 78 const dispose1 = sinon.stub().throws(new Error('dispose1')); 79 const dispose2 = sinon.stub().throws(new Error('dispose2')); 80 const dispose3 = sinon.stub().throws(new Error('dispose3')); 81 stack.adopt({}, dispose1); 82 stack.adopt({}, dispose2); 83 stack.adopt({}, dispose3); 84 let error; 85 try { 86 stack.dispose(); 87 } catch (e) { 88 error = e; 89 } 90 91 expect(error instanceof SuppressedError).toBeTruthy(); 92 expect((error as SuppressedError).name).toEqual('SuppressedError'); 93 expect((error as SuppressedError).message).toEqual( 94 'An error was suppressed during disposal', 95 ); 96 expect((error as SuppressedError).error).toEqual(new Error('dispose3')); 97 expect((error as SuppressedError).suppressed).toEqual( 98 new SuppressedError('dispose2', new Error('dispose1')), 99 ); 100 }); 101 }); 102 103 describe('AsyncDisposableStack', () => { 104 let stack: AsyncDisposableStack; 105 106 beforeEach(() => { 107 stack = new AsyncDisposableStack(); 108 }); 109 110 it('should dispose resources in LIFO order', async () => { 111 const dispose1 = sinon.stub().resolves(); 112 const dispose2 = sinon.stub().resolves(); 113 stack.adopt({}, dispose1); 114 stack.adopt({}, dispose2); 115 await stack.dispose(); 116 expect(dispose2.calledBefore(dispose1)).toBeTruthy(); 117 }); 118 119 it('should not dispose resources if already disposed', async () => { 120 const dispose = sinon.stub().resolves(); 121 stack.adopt({}, dispose); 122 await stack.dispose(); 123 await stack.dispose(); 124 expect(dispose.calledOnce).toBeTruthy(); 125 }); 126 127 it('should use async disposable resources', async () => { 128 const resource = { 129 [asyncDisposeSymbol]: sinon.stub().resolves(), 130 }; 131 stack.use(resource); 132 await stack.dispose(); 133 expect(resource[asyncDisposeSymbol].calledOnce).toBeTruthy(); 134 }); 135 136 it('should defer async disposal callbacks', async () => { 137 const onDispose = sinon.stub().resolves(); 138 stack.defer(onDispose); 139 await stack.dispose(); 140 expect(onDispose.calledOnce).toBeTruthy(); 141 }); 142 143 it('should move resources to a new stack', async () => { 144 const dispose = sinon.stub().resolves(); 145 stack.adopt({}, dispose); 146 const newStack = stack.move(); 147 expect(stack.disposed).toBeTruthy(); 148 expect(newStack.disposed).toBeFalsy(); 149 await newStack.dispose(); 150 expect(dispose.calledOnce).toBeTruthy(); 151 }); 152 153 it('should throw error if moving a disposed stack', () => { 154 stack.dispose(); 155 expect(() => { 156 return stack.move(); 157 }).toThrow(ReferenceError); 158 }); 159 160 it('should collect errors from async disposals', async () => { 161 const dispose1 = sinon.stub().rejects(new Error('dispose1')); 162 const dispose2 = sinon.stub().rejects(new Error('dispose2')); 163 const dispose3 = sinon.stub().rejects(new Error('dispose3')); 164 stack.adopt({}, dispose1); 165 stack.adopt({}, dispose2); 166 stack.adopt({}, dispose3); 167 let error; 168 try { 169 await stack.dispose(); 170 } catch (e) { 171 error = e; 172 } 173 174 expect(error instanceof SuppressedError).toBeTruthy(); 175 expect((error as SuppressedError).name).toEqual('SuppressedError'); 176 expect((error as SuppressedError).message).toEqual( 177 'An error was suppressed during disposal', 178 ); 179 expect((error as SuppressedError).error).toEqual(new Error('dispose3')); 180 expect((error as SuppressedError).suppressed).toEqual( 181 new SuppressedError('dispose2', new Error('dispose1')), 182 ); 183 }); 184 });