From c495d06f7bd259f0710b3021d0f16d2761ae3bad Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sun, 16 Jun 2024 23:04:47 +0200 Subject: [PATCH] tests --- contracts/utils/README.adoc | 6 ++ contracts/utils/structs/Heap.sol | 36 +++++---- test/utils/structs/Heap.t.sol | 68 ++-------------- test/utils/structs/Heap.test.js | 130 +++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 76 deletions(-) create mode 100644 test/utils/structs/Heap.test.js diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 74281b3c59..dc0978589e 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -23,6 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures. * {CircularBuffer}: A data structure to store the last N values pushed to it. * {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time. + * {Heap}: A library that implement https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage. * {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions. * {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly. * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. @@ -36,6 +37,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Context}: An utility for abstracting the sender and calldata in the current execution context. * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. + * {Comparators}: A library that contains comparator functions to use with with the {Heap} library. [NOTE] ==== @@ -100,6 +102,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{Checkpoints}} +{{Heap}} + {{MerkleTree}} == Libraries @@ -127,3 +131,5 @@ Ethereum contracts have no native concept of an interface, so applications must {{Packing}} {{Panic}} + +{{Comparators}} diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 8dd7049020..2fa386721a 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -69,11 +69,11 @@ library Heap { Uint256Heap storage self, function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { - uint32 length = size(self); + uint32 size = length(self); - if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); + if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - uint32 last = length - 1; // could be unchecked (check above) + uint32 last = size - 1; // could be unchecked (check above) // get root location (in the data array) and value uint32 rootIdx = self.data[0].index; @@ -128,18 +128,26 @@ library Heap { uint256 value, function(uint256, uint256) view returns (bool) comp ) internal { - uint32 length = size(self); - self.data.push(Uint256HeapNode({index: length, lookup: length, value: value})); - _heapifyUp(self, length, value, comp); + uint32 size = length(self); + self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); + _heapifyUp(self, size, value, comp); } /** * @dev Returns the number of elements in the heap. */ - function size(Uint256Heap storage self) internal view returns (uint32) { + function length(Uint256Heap storage self) internal view returns (uint32) { return self.data.length.toUint32(); } + function clear(Uint256Heap storage self) internal { + Uint256HeapNode[] storage data = self.data; + /// @solidity memory-safe-assembly + assembly { + sstore(data.slot, 0) + } + } + /* * @dev Swap node `i` and `j` in the tree. */ @@ -158,13 +166,13 @@ library Heap { * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a * comparator, and moving toward the leafs of the underlying tree. * - * Note: This is a private function that is called in a trusted context with already cached parameters. `length` + * Note: This is a private function that is called in a trusted context with already cached parameters. `lesizength` * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ function _heapifyDown( Uint256Heap storage self, - uint32 length, + uint32 size, uint32 pos, uint256 value, function(uint256, uint256) view returns (bool) comp @@ -172,23 +180,23 @@ library Heap { uint32 left = 2 * pos + 1; uint32 right = 2 * pos + 2; - if (right < length) { + if (right < size) { uint256 lValue = self.data[self.data[left].index].value; uint256 rValue = self.data[self.data[right].index].value; if (comp(lValue, value) || comp(rValue, value)) { if (comp(lValue, rValue)) { _swap(self, pos, left); - _heapifyDown(self, length, left, value, comp); + _heapifyDown(self, size, left, value, comp); } else { _swap(self, pos, right); - _heapifyDown(self, length, right, value, comp); + _heapifyDown(self, size, right, value, comp); } } - } else if (left < length) { + } else if (left < size) { uint256 lValue = self.data[self.data[left].index].value; if (comp(lValue, value)) { _swap(self, pos, left); - _heapifyDown(self, length, left, value, comp); + _heapifyDown(self, size, left, value, comp); } } } diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index 7fb289f566..4940a37757 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -23,74 +23,14 @@ contract HeapTest is Test { } } - function testUnit() public { - // - assertEq(heap.size(), 0); - _validateHeap(Comparators.lt); - - heap.insert(712); // 712 - assertEq(heap.size(), 1); - _validateHeap(Comparators.lt); - - heap.insert(20); // 20, 712 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - heap.insert(4337); // 20, 712, 4337 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 20); // 712, 4337 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - heap.insert(1559); // 712, 1559, 4337 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - heap.insert(155); // 155, 712, 1559, 4337 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - heap.insert(7702); // 155, 712, 1559, 4337, 7702 - assertEq(heap.size(), 5); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 155); // 712, 1559, 4337, 7702 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - heap.insert(721); // 712, 721, 1559, 4337, 7702 - assertEq(heap.size(), 5); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 712); // 721, 1559, 4337, 7702 - assertEq(heap.size(), 4); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 721); // 1559, 4337, 7702 - assertEq(heap.size(), 3); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 1559); // 4337, 7702 - assertEq(heap.size(), 2); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 4337); // 7702 - assertEq(heap.size(), 1); - _validateHeap(Comparators.lt); - - assertEq(heap.pop(), 7702); // - assertEq(heap.size(), 0); - _validateHeap(Comparators.lt); - } - function testFuzz(uint256[] calldata input) public { vm.assume(input.length < 0x20); + assertEq(heap.size(), 0); uint256 min = type(uint256).max; for (uint256 i; i < input.length; ++i) { heap.insert(input[i]); + assertEq(heap.size(), i); _validateHeap(Comparators.lt); min = Math.min(min, input[i]); @@ -101,6 +41,7 @@ contract HeapTest is Test { for (uint256 i; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(); + assertEq(heap.size(), input.length - i - 1); _validateHeap(Comparators.lt); assertEq(pop, top); @@ -111,10 +52,12 @@ contract HeapTest is Test { function testFuzzGt(uint256[] calldata input) public { vm.assume(input.length < 0x20); + assertEq(heap.size(), 0); uint256 max = 0; for (uint256 i; i < input.length; ++i) { heap.insert(input[i], Comparators.gt); + assertEq(heap.size(), i); _validateHeap(Comparators.gt); max = Math.max(max, input[i]); @@ -125,6 +68,7 @@ contract HeapTest is Test { for (uint256 i; i < input.length; ++i) { uint256 top = heap.top(); uint256 pop = heap.pop(Comparators.gt); + assertEq(heap.size(), input.length - i - 1); _validateHeap(Comparators.gt); assertEq(pop, top); diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js new file mode 100644 index 0000000000..4dcf268f2b --- /dev/null +++ b/test/utils/structs/Heap.test.js @@ -0,0 +1,130 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); + +async function fixture() { + const mock = await ethers.deployContract('$Heap'); + return { mock }; +} + +describe('Heap', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('starts empty', async function () { + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + expect(await this.mock.$length(0)).to.equal(0n); + }); + + it('pop from empty', async function () { + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); + + it('clear', async function () { + await this.mock.$insert(0, 42n); + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(42n); + + await this.mock.$clear(0); + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + + it('support duplicated items', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + + // insert 5 times + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + + // pop 5 times + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + + // poping a 6th time panics + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); + + it('insert and pop', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + + await this.mock.$insert(0, 712n); // 712 + + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 20n); // 20, 712 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(20n); + + await this.mock.$insert(0, 4337n); // 20, 712, 4337 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(20n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(20n); // 712, 4337 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 1559n); // 712, 1559, 4337 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 155n); // 155, 712, 1559, 4337 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(155n); + + await this.mock.$insert(0, 7702n); // 155, 712, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(5n); + expect(await this.mock.$top(0)).to.equal(155n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(155n); // 712, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(712n); + + await this.mock.$insert(0, 721n); // 712, 721, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(5n); + expect(await this.mock.$top(0)).to.equal(712n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(712n); // 721, 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(4n); + expect(await this.mock.$top(0)).to.equal(721n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(721n); // 1559, 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(3n); + expect(await this.mock.$top(0)).to.equal(1559n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(1559n); // 4337, 7702 + + expect(await this.mock.$length(0)).to.equal(2n); + expect(await this.mock.$top(0)).to.equal(4337n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(4337n); // 7702 + + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$top(0)).to.equal(7702n); + + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(7702n); // + + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); +});