Skip to content

Instantly share code, notes, and snippets.

@mhofman
Last active April 3, 2019 01:55
Show Gist options
  • Save mhofman/9824ef03535412caac194a57631a93b2 to your computer and use it in GitHub Desktop.
Save mhofman/9824ef03535412caac194a57631a93b2 to your computer and use it in GitHub Desktop.
WeakRef and FinalizationGroup tests

Test for WeakRef and FinalizationGroup as specified by https://weakrefs.netlify.com

Make sure to run Chrome 74 with --js-flags="--expose-gc --harmony-weak-refs"

function taskTurn() {
return new Promise(resolve => setTimeout(resolve, 0));
}
function makeGcOf(gc, FinalizationGroup) {
return async function gcOf(target) {
// Avoid creating a closure which may capture target
let resolve;
const collected = new Promise(r => (resolve = r));
const finalizationGroup = new FinalizationGroup(resolve);
finalizationGroup.register(target || {}, 0);
target = undefined;
await taskTurn();
gc();
await collected;
return true;
};
}
function expectThrowIfNotObject(functionToTest) {
it("undefined", async function() {
expect(() => functionToTest(undefined)).to.throw();
});
it("null", async function() {
expect(() => functionToTest(null)).to.throw();
});
it("boolean", async function() {
expect(() => functionToTest(true)).to.throw();
});
it("string", async function() {
expect(() => functionToTest("string")).to.throw();
});
it("symbol", async function() {
expect(() => functionToTest(Symbol())).to.throw();
});
it("number", async function() {
expect(() => functionToTest(42)).to.throw();
});
}
if (typeof WeakRef == "function") {
describe("WeakRef", function() {
let gcOf;
beforeEach(function() {
if (
typeof gc == "function" &&
typeof FinalizationGroup == "function"
) {
gcOf = makeGcOf(gc, FinalizationGroup);
} else {
gcOf = () => this.skip();
}
});
it("should have the right species", async function() {
expect(WeakRef[Symbol.species]).to.be.equal(WeakRef);
});
describe("constructor", function() {
it("should throw when constructor called without new", async function() {
const constructorFn = WeakRef;
expect(() => constructorFn({})).to.throw();
});
describe("should throw when constructed with non-object", function() {
expectThrowIfNotObject(value => new WeakRef(value));
});
it("should hold target on creation", async function() {
const ref = new WeakRef({});
expect(ref.deref()).to.be.ok;
});
});
describe("deref", function() {
describe("should throw when function invoked on non-object", function() {
expectThrowIfNotObject(value =>
WeakRef.prototype.deref.call(value)
);
});
it("should throw when method invoked with wrong this", async function() {
const weakRef = new WeakRef({});
expect(() => weakRef.deref.call({})).to.throw();
});
it("should hold target on deref", async function() {
let object = {};
const weakRef = new WeakRef(object);
await gcOf();
expect(weakRef.deref()).to.be.ok;
object = undefined;
if (typeof gc == "function") gc(); // No await since we need to stay in same turn
expect(weakRef.deref()).to.be.ok;
});
it("should return undefined if target collected (direct observe)", async function() {
let object = {};
const weakRef = new WeakRef(object);
const collected = gcOf(object);
object = undefined;
await collected;
expect(weakRef.deref()).to.be.equal(undefined);
});
it("should return undefined if target collected (indirect observe)", async function() {
const weakRef = new WeakRef({});
await gcOf();
expect(weakRef.deref()).to.be.equal(undefined);
});
});
it("should allow being subclassed", async function() {
class SemiWeakRef extends WeakRef {
constructor(target) {
super(target);
this.target = target;
}
deref() {
return this.target || super.deref();
}
release() {
this.target = undefined;
}
}
const semiWeakRef = new SemiWeakRef({});
semiWeakRef.release();
expect(semiWeakRef.deref()).to.be.ok;
});
});
} else {
it("WeakRef not available");
}
if (typeof FinalizationGroup == "function") {
let unregisterReturnsBool = true;
let workingCleanupSome = true;
describe("FinalizationGroup", function() {
const gcOf =
typeof gc == "function"
? makeGcOf(gc, FinalizationGroup)
: undefined;
it("should have the right species", async function() {
expect(FinalizationGroup[Symbol.species]).to.be.equal(
FinalizationGroup
);
});
describe("register", function() {
describe("should throw when function invoked on non-object", function() {
expectThrowIfNotObject(value =>
FinalizationGroup.prototype.register.call(value, {}, 0)
);
});
it("should throw when method invoked with wrong this", async function() {
expect(() =>
FinalizationGroup.prototype.register.call({}, {}, 0)
).to.throw();
});
describe("should throw when method invoked with non-object target", function() {
let finalizationGroup;
beforeEach(function() {
finalizationGroup = new FinalizationGroup(() => {});
});
expectThrowIfNotObject(value =>
finalizationGroup.register(value, 0)
);
});
it("should return undefined", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
expect(finalizationGroup.register({}, 0)).to.be.equal(
undefined
);
});
it("should allow registering the same object multiple times", async function() {
const object = {};
const finalizationGroup = new FinalizationGroup(() => {});
finalizationGroup.register(object, 0);
expect(() =>
finalizationGroup.register(object, 1)
).not.to.throw();
});
if (gcOf) {
describe("collection behavior", function() {
it("calls callback on collected object", async function() {
// Calls FinalizationGroup with marker object
// and only completes if callback called
await gcOf();
});
it("calls callback on multiple FinalizationGroup for same object", async function() {
let object = {};
const callback = chai.spy();
const finalizationGroup = new FinalizationGroup(
callback
);
finalizationGroup.register(object, 42);
const collected = gcOf(object);
object = undefined;
await collected;
expect(callback).to.have.been.called();
});
it("calls callback with multiple holdings for same object - different values", async function() {
let object = {};
let holdings = new Set();
const finalizationGroup = new FinalizationGroup(
items =>
(holdings = new Set([...holdings, ...items]))
);
finalizationGroup.register(object, 42);
finalizationGroup.register(object, 5);
const collected = gcOf(object);
object = undefined;
await collected;
expect(holdings).to.contain(5);
expect(holdings).to.contain(42);
expect(holdings).to.have.lengthOf(2);
});
it("calls callback with multiple holdings for same object - same values", async function() {
let object = {};
let holdings = new Array();
const finalizationGroup = new FinalizationGroup(items =>
holdings.push(...items)
);
finalizationGroup.register(object, 42);
finalizationGroup.register(object, 42);
const collected = gcOf(object);
object = undefined;
await collected;
expect(holdings[0]).to.be.equal(42);
expect(holdings[1]).to.be.equal(42);
expect(holdings).to.have.lengthOf(2);
});
});
} else {
it("collection behavior test disabled: no gc method");
}
});
describe("unregister", function() {
describe("should throw when function invoked on non-object", function() {
expectThrowIfNotObject(value =>
FinalizationGroup.prototype.unregister.call(value)
);
});
it("should throw when method invoked with wrong this", async function() {
expect(() =>
FinalizationGroup.prototype.unregister.call({})
).to.throw();
});
it("should return boolean", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
const ret = finalizationGroup.unregister(42);
unregisterReturnsBool = ret === false;
expect(ret).to.be.false;
});
it("should unregister cell with specific token", async function() {
if (!unregisterReturnsBool) this.skip();
const finalizationGroup = new FinalizationGroup(() => {});
finalizationGroup.register({}, 42, 42);
expect(finalizationGroup.unregister(42)).to.be.true;
});
it("should not unregister cell with token not strictly equal", async function() {
if (!unregisterReturnsBool) this.skip();
const finalizationGroup = new FinalizationGroup(() => {});
finalizationGroup.register({}, 42, 42);
expect(finalizationGroup.unregister("42")).to.be.false;
});
if (gcOf) {
describe("collection behavior", function() {
let finalizationGroup;
let holdings;
before(function() {
holdings = new Set();
finalizationGroup = new FinalizationGroup(
items =>
(holdings = new Set([...holdings, ...items]))
);
});
it("callback doesn't contain unregistered holdings - different objects", async function() {
let objects = [{}, {}];
finalizationGroup.register(objects[0], 5, 0);
finalizationGroup.register(objects[1], 42, 1);
const unregisterExpectation = expect(
finalizationGroup.unregister(0)
);
if (unregisterReturnsBool)
unregisterExpectation.to.be.true;
const collected = [gcOf(objects[0]), gcOf(objects[1])];
objects = undefined;
await Promise.all(collected);
expect(holdings).to.contain(42);
expect(holdings).to.have.lengthOf(1);
});
it("callback doesn't contain unregistered holdings - same object", async function() {
let object = {};
finalizationGroup.register(object, 5, 0);
finalizationGroup.register(object, 42, 1);
const unregisterExpectation = expect(
finalizationGroup.unregister(0)
);
if (unregisterReturnsBool)
unregisterExpectation.to.be.true;
const collected = gcOf(object);
object = undefined;
await collected;
expect(holdings).to.contain(42);
expect(holdings).to.have.lengthOf(1);
});
it("callback doesn't contain unregistered holdings - undefined token value", async function() {
let object = {};
finalizationGroup.register(object, 5, undefined);
finalizationGroup.register(object, 42, 1);
const unregisterExpectation = expect(
finalizationGroup.unregister(undefined)
);
if (unregisterReturnsBool)
unregisterExpectation.to.be.true;
const collected = gcOf(object);
object = undefined;
await collected;
expect(holdings).to.contain(42);
expect(holdings).to.have.lengthOf(1);
});
it("can unregister a finalized but not spent holding", async function() {
const holdings = [5, 42];
let notIterated = 0;
const finalizationGroup = new FinalizationGroup(
items => {
for (const item of items) {
if (item === notIterated)
expect.fail(
"Iterated on unregistered holding"
);
notIterated = item;
expect(holdings).to.contain(notIterated);
holdings.forEach(holding =>
expect(
finalizationGroup.unregister(
holding
)
).to.be.equal(holding == notIterated)
);
}
}
);
let object = {};
finalizationGroup.register(
object,
holdings[0],
holdings[1]
);
finalizationGroup.register(
object,
holdings[1],
holdings[0]
);
const collected = gcOf(object);
object = undefined;
await collected;
expect(notIterated).to.be.not.equal(0);
});
});
} else {
it("collection behavior test disabled: no gc method");
}
});
describe("cleanupSome", function() {
describe("should throw when function invoked on non-object", function() {
expectThrowIfNotObject(value =>
FinalizationGroup.prototype.cleanupSome.call(value)
);
});
it("should throw when method invoked with wrong this", async function() {
expect(() =>
FinalizationGroup.prototype.cleanupSome.call({})
).to.throw();
});
it("should call callback even when no empty cell", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
workingCleanupSome = false;
finalizationGroup.cleanupSome(
() => (workingCleanupSome = true)
);
expect(workingCleanupSome).to.be.true;
});
it("should throw when called with non-callable", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
// No point checking this if cleanupSome does nothing
if (!workingCleanupSome) this.skip();
expect(() => finalizationGroup.cleanupSome({})).to.throw();
});
it("should return undefined", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
expect(finalizationGroup.cleanupSome(() => {})).to.be.equal(
undefined
);
});
it("should not call the callback given at constructor if one is provided", async function() {
const callback = chai.spy();
const constructorCallback = chai.spy();
const finalizationGroup = new FinalizationGroup(
constructorCallback
);
finalizationGroup.cleanupSome(callback);
expect(constructorCallback).to.not.have.been.called();
if (workingCleanupSome) expect(callback).to.have.been.called();
});
if (gcOf) {
describe("collection behavior", function() {
it("should yield previously finalized cells", async function() {
let object = {};
const callback = chai.spy();
const finalizationGroup = new FinalizationGroup(
callback
);
finalizationGroup.register(object, 42);
const collected = gcOf(object);
object = undefined;
await collected;
expect(callback).to.have.been.called();
let holdings;
workingCleanupSome = false;
expect(
finalizationGroup.cleanupSome(items => {
workingCleanupSome = true;
holdings = [...items];
})
);
if (!workingCleanupSome) this.skip();
expect(holdings).to.contain(42);
expect(holdings).to.have.lengthOf(1);
});
});
} else {
it("collection behavior test disabled: no gc method");
}
});
describe("iterator", function() {
it("should have the correct toStringTag", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
let called = false;
finalizationGroup.cleanupSome(items => {
called = true;
expect(items[Symbol.toStringTag]).to.be.equal(
"FinalizationGroup Cleanup Iterator"
);
});
if (!called) this.skip();
});
it("should not yield any holding if nothing finalized", async function() {
const finalizationGroup = new FinalizationGroup(() => {});
if (!workingCleanupSome) this.skip();
finalizationGroup.cleanupSome(items => {
const result = items.next();
expect(result.done).to.be.true;
expect(result.value).to.be.equal(undefined);
});
});
if (gcOf) {
describe("collection behavior", function() {
it("doesn't remove cell if iterator not consumed", async function() {
let object = {};
const callback = chai.spy();
const finalizationGroup = new FinalizationGroup(
callback
);
finalizationGroup.register(object, 42, 42);
const collected = gcOf(object);
object = undefined;
await collected;
expect(callback).to.have.been.called();
if (unregisterReturnsBool) {
expect(finalizationGroup.unregister(42)).to.be.true;
} else if (workingCleanupSome) {
let notConsumed;
expect(
finalizationGroup.cleanupSome(items => {
notConsumed = [...items];
})
);
expect(notConsumed).to.contain(42);
expect(notConsumed).to.have.lengthOf(1);
} else {
this.skip();
}
});
it("doesn't remove cell if iterator is closed before", async function() {
const holdings = [5, 42];
let notIterated = 0;
const finalizationGroup = new FinalizationGroup(
items => {
for (const item of items) {
notIterated = item;
expect(holdings).to.contain(notIterated);
break;
}
}
);
let object = {};
finalizationGroup.register(
object,
holdings[0],
holdings[1]
);
finalizationGroup.register(
object,
holdings[1],
holdings[0]
);
const collected = gcOf(object);
object = undefined;
await collected;
expect(notIterated).to.be.not.equal(0);
if (unregisterReturnsBool) {
expect(finalizationGroup.unregister(notIterated)).to
.be.true;
} else if (workingCleanupSome) {
finalizationGroup.cleanupSome(items => {
for (const item of items)
expect(item).to.equal(notIterated);
notIterated = 0;
});
expect(notIterated).to.be.equal(0);
} else {
this.skip();
}
});
});
} else {
it("collection behavior test disabled: no gc method");
}
});
describe("constructor", function() {
it("should throw when constructor called without new", async function() {
const constructorFn = FinalizationGroup;
expect(() => constructorFn(() => {})).to.throw();
});
describe("should throw when constructed with non-object", function() {
expectThrowIfNotObject(value => new FinalizationGroup(value));
});
it("should throw when constructed with non-callable", async function() {
expect(() => new FinalizationGroup({})).to.throw();
});
it("should allow being subclassed", async function() {
class FinalizationGroupSubclass extends FinalizationGroup {
register(target, holding) {
const token = {};
super.register(target, holding, token);
return token;
}
}
const finalizationGroup = new FinalizationGroupSubclass(
() => {}
);
const token = finalizationGroup.register({}, 42);
expect(token).to.be.ok;
if (unregisterReturnsBool)
expect(finalizationGroup.unregister(token)).to.be.true;
});
if (gcOf) {
describe("collection behavior", function() {
it("doesn't call callback if has no empty cell", async function() {
let object = {};
const callback = chai.spy();
const finalizationGroup = new FinalizationGroup(
callback
);
finalizationGroup.register(object, 42);
await gcOf();
expect(callback).not.to.have.been.called();
});
});
} else {
it("collection behavior test disabled: no gc method");
}
});
});
} else {
it("FinalizationGroup not available");
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>weakrefs proposal tests</title>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script src="https://unpkg.com/[email protected]/mocha.js"></script>
<script src="https://unpkg.com/[email protected]/chai.js"></script>
<script src="https://unpkg.com/[email protected]/chai-spies.js"></script>
<script>
mocha.setup('bdd');
var expect = chai.expect;
</script>
<script src="./_weakrefs_tests.js"></script>
<script>
mocha.run();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment