Skip to content

Commit

Permalink
tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Jun 16, 2024
1 parent 8fa2eeb commit c495d06
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 76 deletions.
6 changes: 6 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
====
Expand Down Expand Up @@ -100,6 +102,8 @@ Ethereum contracts have no native concept of an interface, so applications must

{{Checkpoints}}

{{Heap}}

{{MerkleTree}}

== Libraries
Expand Down Expand Up @@ -127,3 +131,5 @@ Ethereum contracts have no native concept of an interface, so applications must
{{Packing}}

{{Panic}}

{{Comparators}}
36 changes: 22 additions & 14 deletions contracts/utils/structs/Heap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand All @@ -158,37 +166,37 @@ 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
) private {
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);
}
}
}
Expand Down
68 changes: 6 additions & 62 deletions test/utils/structs/Heap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,74 +23,14 @@ contract HeapTest is Test {
}
}

function testUnit() public {
// <empty>
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); // <empty>
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]);
Expand All @@ -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);
Expand All @@ -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]);
Expand All @@ -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);
Expand Down
130 changes: 130 additions & 0 deletions test/utils/structs/Heap.test.js
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 52 in test/utils/structs/Heap.test.js

View workflow job for this annotation

GitHub Actions / codespell

poping ==> popping, pooping
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); // <empty>

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);
});
});

0 comments on commit c495d06

Please sign in to comment.