1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 05:27:45 +00:00

LibJS: Add DisposableStack{, Prototype, Constructor}

Since the async parts of the spec are not stage 3 at this point we don't
add AsyncDisposableStack.
This commit is contained in:
davidot 2022-12-21 11:48:14 +01:00 committed by Linus Groh
parent bff038411a
commit 6255ca4a42
22 changed files with 868 additions and 0 deletions

View file

@ -0,0 +1,18 @@
test("constructor properties", () => {
expect(DisposableStack).toHaveLength(0);
expect(DisposableStack.name).toBe("DisposableStack");
});
describe("errors", () => {
test("called without new", () => {
expect(() => {
DisposableStack();
}).toThrowWithMessage(TypeError, "DisposableStack constructor must be called with 'new'");
});
});
describe("normal behavior", () => {
test("typeof", () => {
expect(typeof new DisposableStack()).toBe("object");
});
});

View file

@ -0,0 +1,19 @@
test("length is 0", () => {
expect(DisposableStack.prototype[Symbol.dispose]).toHaveLength(0);
});
test("is the same as dispose", () => {
expect(DisposableStack.prototype[Symbol.dispose]).toBe(DisposableStack.prototype.dispose);
});
describe("used in using functionality", () => {
test("make the stack marked as disposed", () => {
let innerStack;
{
using stack = new DisposableStack();
innerStack = stack;
expect(stack.disposed).toBeFalse();
}
expect(innerStack.disposed).toBeTrue();
});
});

View file

@ -0,0 +1,3 @@
test("basic functionality", () => {
expect(DisposableStack.prototype[Symbol.toStringTag]).toBe("DisposableStack");
});

View file

@ -0,0 +1,95 @@
test("length is 2", () => {
expect(DisposableStack.prototype.adopt).toHaveLength(2);
});
describe("basic functionality", () => {
test("adopted dispose method gets called when stack is disposed", () => {
const stack = new DisposableStack();
let disposedCalled = 0;
let disposeArgument = undefined;
expect(disposedCalled).toBe(0);
const result = stack.adopt(null, arg => {
disposeArgument = arg;
++disposedCalled;
});
expect(result).toBeNull();
expect(disposedCalled).toBe(0);
stack.dispose();
expect(disposedCalled).toBe(1);
expect(disposeArgument).toBeNull();
stack.dispose();
expect(disposedCalled).toBe(1);
});
test("can adopt any value", () => {
const stack = new DisposableStack();
const disposed = [];
function dispose(value) {
disposed.push(value);
}
const values = [null, undefined, 1, "a", Symbol.dispose, () => {}, new WeakMap(), [], {}];
values.forEach(value => {
stack.adopt(value, dispose);
});
stack.dispose();
expect(disposed).toEqual(values.reverse());
});
test("adopted stack is already disposed", () => {
const stack = new DisposableStack();
stack.adopt(stack, value => {
expect(stack).toBe(value);
expect(stack.disposed).toBeTrue();
});
stack.dispose();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new DisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.adopt(null, value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("adopt throws if stack is already disposed (over type errors)", () => {
const stack = new DisposableStack();
stack.dispose();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.adopt(value, () => {})).toThrowWithMessage(
ReferenceError,
"DisposableStack already disposed values"
);
expect(() => stack.adopt(null, value)).toThrowWithMessage(
ReferenceError,
"DisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,70 @@
test("length is 1", () => {
expect(DisposableStack.prototype.defer).toHaveLength(1);
});
describe("basic functionality", () => {
test("deferred function gets called when stack is disposed", () => {
const stack = new DisposableStack();
let disposedCalled = 0;
expect(disposedCalled).toBe(0);
const result = stack.defer((...args) => {
expect(args.length).toBe(0);
++disposedCalled;
});
expect(result).toBeUndefined();
expect(disposedCalled).toBe(0);
stack.dispose();
expect(disposedCalled).toBe(1);
stack.dispose();
expect(disposedCalled).toBe(1);
});
test("deferred stack is already disposed", () => {
const stack = new DisposableStack();
stack.defer(() => {
expect(stack.disposed).toBeTrue();
});
stack.dispose();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new DisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("defer throws if stack is already disposed (over type errors)", () => {
const stack = new DisposableStack();
stack.dispose();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(
ReferenceError,
"DisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,83 @@
test("length is 0", () => {
expect(DisposableStack.prototype.dispose).toHaveLength(0);
});
describe("basic functionality", () => {
test("make the stack marked as disposed", () => {
const stack = new DisposableStack();
const result = stack.dispose();
expect(stack.disposed).toBeTrue();
expect(result).toBeUndefined();
});
test("call dispose on objects in stack when called", () => {
const stack = new DisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.dispose]() {
disposedCalled = true;
},
});
expect(disposedCalled).toBeFalse();
const result = stack.dispose();
expect(disposedCalled).toBeTrue();
expect(result).toBeUndefined();
});
test("disposed the objects added to the stack in reverse order", () => {
const disposed = [];
const stack = new DisposableStack();
stack.use({
[Symbol.dispose]() {
disposed.push("a");
},
});
stack.use({
[Symbol.dispose]() {
disposed.push("b");
},
});
expect(disposed).toEqual([]);
const result = stack.dispose();
expect(disposed).toEqual(["b", "a"]);
expect(result).toBeUndefined();
});
test("does not dispose anything if already disposed", () => {
const disposed = [];
const stack = new DisposableStack();
stack.use({
[Symbol.dispose]() {
disposed.push("a");
},
});
expect(stack.disposed).toBeFalse();
expect(disposed).toEqual([]);
expect(stack.dispose()).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
expect(stack.dispose()).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
});
test("throws if dispose method throws", () => {
const stack = new DisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.dispose]() {
disposedCalled = true;
expect().fail("fail in dispose");
},
});
expect(() => stack.dispose()).toThrowWithMessage(ExpectationError, "fail in dispose");
});
});

View file

@ -0,0 +1,24 @@
test("is getter without setter", () => {
const property = Object.getOwnPropertyDescriptor(DisposableStack.prototype, "disposed");
expect(property.get).not.toBeUndefined();
expect(property.set).toBeUndefined();
expect(property.value).toBeUndefined();
});
describe("basic functionality", () => {
test("is not a property on the object itself", () => {
const stack = new DisposableStack();
expect(Object.hasOwn(stack, "disposed")).toBeFalse();
});
test("starts off as false", () => {
const stack = new DisposableStack();
expect(stack.disposed).toBeFalse();
});
test("becomes true after being disposed", () => {
const stack = new DisposableStack();
stack.dispose();
expect(stack.disposed).toBeTrue();
});
});

View file

@ -0,0 +1,62 @@
test("length is 0", () => {
expect(DisposableStack.prototype.move).toHaveLength(0);
});
describe("basic functionality", () => {
test("stack is disposed after moving", () => {
const stack = new DisposableStack();
const newStack = stack.move();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
});
test("move does not dispose resource but only move them", () => {
const stack = new DisposableStack();
let disposeCalled = false;
stack.defer(() => {
disposeCalled = true;
});
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeFalse();
const newStack = stack.move();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
stack.dispose();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
newStack.dispose();
expect(disposeCalled).toBeTrue();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeTrue();
});
test("can add stack to itself", () => {
const stack = new DisposableStack();
stack.move(stack);
stack.dispose();
});
});
describe("throws errors", () => {
test("move throws if stack is already disposed (over type errors)", () => {
const stack = new DisposableStack();
stack.dispose();
expect(stack.disposed).toBeTrue();
expect(() => stack.move()).toThrowWithMessage(
ReferenceError,
"DisposableStack already disposed values"
);
});
});

View file

@ -0,0 +1,96 @@
test("length is 1", () => {
expect(DisposableStack.prototype.use).toHaveLength(1);
});
describe("basic functionality", () => {
test("added objects dispose method gets when stack is disposed", () => {
const stack = new DisposableStack();
let disposedCalled = 0;
const obj = {
[Symbol.dispose]() {
++disposedCalled;
},
};
expect(disposedCalled).toBe(0);
const result = stack.use(obj);
expect(result).toBe(obj);
expect(disposedCalled).toBe(0);
stack.dispose();
expect(disposedCalled).toBe(1);
stack.dispose();
expect(disposedCalled).toBe(1);
});
test("can add null and undefined", () => {
const stack = new DisposableStack();
expect(stack.use(null)).toBeNull();
expect(stack.use(undefined)).toBeUndefined();
expect(stack.disposed).toBeFalse();
stack.dispose();
expect(stack.disposed).toBeTrue();
});
test("can add stack to itself", () => {
const stack = new DisposableStack();
stack.use(stack);
stack.dispose();
});
});
describe("throws errors", () => {
test("if added value is not an object or null or undefined throws type error", () => {
const stack = new DisposableStack();
[1, 1n, "a", Symbol.dispose, NaN, 0].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "not an object");
});
expect(stack.disposed).toBeFalse();
});
test("if added object does not have a dispose method throws type error", () => {
const stack = new DisposableStack();
[{}, [], { f() {} }].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
TypeError,
"does not have dispose method"
);
});
expect(stack.disposed).toBeFalse();
});
test("if added object has non function dispose method it throws type error", () => {
const stack = new DisposableStack();
let calledGetter = false;
[
{ [Symbol.dispose]: 1 },
{
get [Symbol.dispose]() {
calledGetter = true;
return 1;
},
},
].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "is not a function");
});
expect(stack.disposed).toBeFalse();
expect(calledGetter).toBeTrue();
});
test("use throws if stack is already disposed (over type errors)", () => {
const stack = new DisposableStack();
stack.dispose();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
ReferenceError,
"DisposableStack already disposed values"
);
});
});
});