Skip to content

Commit c495d06

Browse files
committed
tests
1 parent 8fa2eeb commit c495d06

File tree

4 files changed

+164
-76
lines changed

4 files changed

+164
-76
lines changed

contracts/utils/README.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
2323
* {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.
2424
* {CircularBuffer}: A data structure to store the last N values pushed to it.
2525
* {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time.
26+
* {Heap}: A library that implement https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage.
2627
* {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions.
2728
* {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.
2829
* {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
3637
* {Context}: An utility for abstracting the sender and calldata in the current execution context.
3738
* {Packing}: A library for packing and unpacking multiple values into bytes32
3839
* {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].
40+
* {Comparators}: A library that contains comparator functions to use with with the {Heap} library.
3941

4042
[NOTE]
4143
====
@@ -100,6 +102,8 @@ Ethereum contracts have no native concept of an interface, so applications must
100102

101103
{{Checkpoints}}
102104

105+
{{Heap}}
106+
103107
{{MerkleTree}}
104108

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

129133
{{Panic}}
134+
135+
{{Comparators}}

contracts/utils/structs/Heap.sol

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ library Heap {
6969
Uint256Heap storage self,
7070
function(uint256, uint256) view returns (bool) comp
7171
) internal returns (uint256) {
72-
uint32 length = size(self);
72+
uint32 size = length(self);
7373

74-
if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP);
74+
if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP);
7575

76-
uint32 last = length - 1; // could be unchecked (check above)
76+
uint32 last = size - 1; // could be unchecked (check above)
7777

7878
// get root location (in the data array) and value
7979
uint32 rootIdx = self.data[0].index;
@@ -128,18 +128,26 @@ library Heap {
128128
uint256 value,
129129
function(uint256, uint256) view returns (bool) comp
130130
) internal {
131-
uint32 length = size(self);
132-
self.data.push(Uint256HeapNode({index: length, lookup: length, value: value}));
133-
_heapifyUp(self, length, value, comp);
131+
uint32 size = length(self);
132+
self.data.push(Uint256HeapNode({index: size, lookup: size, value: value}));
133+
_heapifyUp(self, size, value, comp);
134134
}
135135

136136
/**
137137
* @dev Returns the number of elements in the heap.
138138
*/
139-
function size(Uint256Heap storage self) internal view returns (uint32) {
139+
function length(Uint256Heap storage self) internal view returns (uint32) {
140140
return self.data.length.toUint32();
141141
}
142142

143+
function clear(Uint256Heap storage self) internal {
144+
Uint256HeapNode[] storage data = self.data;
145+
/// @solidity memory-safe-assembly
146+
assembly {
147+
sstore(data.slot, 0)
148+
}
149+
}
150+
143151
/*
144152
* @dev Swap node `i` and `j` in the tree.
145153
*/
@@ -158,37 +166,37 @@ library Heap {
158166
* @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a
159167
* comparator, and moving toward the leafs of the underlying tree.
160168
*
161-
* Note: This is a private function that is called in a trusted context with already cached parameters. `length`
169+
* Note: This is a private function that is called in a trusted context with already cached parameters. `lesizength`
162170
* and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These
163171
* parameters are not verified. It is the caller role to make sure the parameters are correct.
164172
*/
165173
function _heapifyDown(
166174
Uint256Heap storage self,
167-
uint32 length,
175+
uint32 size,
168176
uint32 pos,
169177
uint256 value,
170178
function(uint256, uint256) view returns (bool) comp
171179
) private {
172180
uint32 left = 2 * pos + 1;
173181
uint32 right = 2 * pos + 2;
174182

175-
if (right < length) {
183+
if (right < size) {
176184
uint256 lValue = self.data[self.data[left].index].value;
177185
uint256 rValue = self.data[self.data[right].index].value;
178186
if (comp(lValue, value) || comp(rValue, value)) {
179187
if (comp(lValue, rValue)) {
180188
_swap(self, pos, left);
181-
_heapifyDown(self, length, left, value, comp);
189+
_heapifyDown(self, size, left, value, comp);
182190
} else {
183191
_swap(self, pos, right);
184-
_heapifyDown(self, length, right, value, comp);
192+
_heapifyDown(self, size, right, value, comp);
185193
}
186194
}
187-
} else if (left < length) {
195+
} else if (left < size) {
188196
uint256 lValue = self.data[self.data[left].index].value;
189197
if (comp(lValue, value)) {
190198
_swap(self, pos, left);
191-
_heapifyDown(self, length, left, value, comp);
199+
_heapifyDown(self, size, left, value, comp);
192200
}
193201
}
194202
}

test/utils/structs/Heap.t.sol

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,74 +23,14 @@ contract HeapTest is Test {
2323
}
2424
}
2525

26-
function testUnit() public {
27-
// <empty>
28-
assertEq(heap.size(), 0);
29-
_validateHeap(Comparators.lt);
30-
31-
heap.insert(712); // 712
32-
assertEq(heap.size(), 1);
33-
_validateHeap(Comparators.lt);
34-
35-
heap.insert(20); // 20, 712
36-
assertEq(heap.size(), 2);
37-
_validateHeap(Comparators.lt);
38-
39-
heap.insert(4337); // 20, 712, 4337
40-
assertEq(heap.size(), 3);
41-
_validateHeap(Comparators.lt);
42-
43-
assertEq(heap.pop(), 20); // 712, 4337
44-
assertEq(heap.size(), 2);
45-
_validateHeap(Comparators.lt);
46-
47-
heap.insert(1559); // 712, 1559, 4337
48-
assertEq(heap.size(), 3);
49-
_validateHeap(Comparators.lt);
50-
51-
heap.insert(155); // 155, 712, 1559, 4337
52-
assertEq(heap.size(), 4);
53-
_validateHeap(Comparators.lt);
54-
55-
heap.insert(7702); // 155, 712, 1559, 4337, 7702
56-
assertEq(heap.size(), 5);
57-
_validateHeap(Comparators.lt);
58-
59-
assertEq(heap.pop(), 155); // 712, 1559, 4337, 7702
60-
assertEq(heap.size(), 4);
61-
_validateHeap(Comparators.lt);
62-
63-
heap.insert(721); // 712, 721, 1559, 4337, 7702
64-
assertEq(heap.size(), 5);
65-
_validateHeap(Comparators.lt);
66-
67-
assertEq(heap.pop(), 712); // 721, 1559, 4337, 7702
68-
assertEq(heap.size(), 4);
69-
_validateHeap(Comparators.lt);
70-
71-
assertEq(heap.pop(), 721); // 1559, 4337, 7702
72-
assertEq(heap.size(), 3);
73-
_validateHeap(Comparators.lt);
74-
75-
assertEq(heap.pop(), 1559); // 4337, 7702
76-
assertEq(heap.size(), 2);
77-
_validateHeap(Comparators.lt);
78-
79-
assertEq(heap.pop(), 4337); // 7702
80-
assertEq(heap.size(), 1);
81-
_validateHeap(Comparators.lt);
82-
83-
assertEq(heap.pop(), 7702); // <empty>
84-
assertEq(heap.size(), 0);
85-
_validateHeap(Comparators.lt);
86-
}
87-
8826
function testFuzz(uint256[] calldata input) public {
8927
vm.assume(input.length < 0x20);
28+
assertEq(heap.size(), 0);
9029

9130
uint256 min = type(uint256).max;
9231
for (uint256 i; i < input.length; ++i) {
9332
heap.insert(input[i]);
33+
assertEq(heap.size(), i);
9434
_validateHeap(Comparators.lt);
9535

9636
min = Math.min(min, input[i]);
@@ -101,6 +41,7 @@ contract HeapTest is Test {
10141
for (uint256 i; i < input.length; ++i) {
10242
uint256 top = heap.top();
10343
uint256 pop = heap.pop();
44+
assertEq(heap.size(), input.length - i - 1);
10445
_validateHeap(Comparators.lt);
10546

10647
assertEq(pop, top);
@@ -111,10 +52,12 @@ contract HeapTest is Test {
11152

11253
function testFuzzGt(uint256[] calldata input) public {
11354
vm.assume(input.length < 0x20);
55+
assertEq(heap.size(), 0);
11456

11557
uint256 max = 0;
11658
for (uint256 i; i < input.length; ++i) {
11759
heap.insert(input[i], Comparators.gt);
60+
assertEq(heap.size(), i);
11861
_validateHeap(Comparators.gt);
11962

12063
max = Math.max(max, input[i]);
@@ -125,6 +68,7 @@ contract HeapTest is Test {
12568
for (uint256 i; i < input.length; ++i) {
12669
uint256 top = heap.top();
12770
uint256 pop = heap.pop(Comparators.gt);
71+
assertEq(heap.size(), input.length - i - 1);
12872
_validateHeap(Comparators.gt);
12973

13074
assertEq(pop, top);

test/utils/structs/Heap.test.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
5+
6+
async function fixture() {
7+
const mock = await ethers.deployContract('$Heap');
8+
return { mock };
9+
}
10+
11+
describe('Heap', function () {
12+
beforeEach(async function () {
13+
Object.assign(this, await loadFixture(fixture));
14+
});
15+
16+
it('starts empty', async function () {
17+
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
18+
expect(await this.mock.$length(0)).to.equal(0n);
19+
});
20+
21+
it('pop from empty', async function () {
22+
await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
23+
});
24+
25+
it('clear', async function () {
26+
await this.mock.$insert(0, 42n);
27+
expect(await this.mock.$length(0)).to.equal(1n);
28+
expect(await this.mock.$top(0)).to.equal(42n);
29+
30+
await this.mock.$clear(0);
31+
expect(await this.mock.$length(0)).to.equal(0n);
32+
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
33+
});
34+
35+
it('support duplicated items', async function () {
36+
expect(await this.mock.$length(0)).to.equal(0n);
37+
38+
// insert 5 times
39+
await this.mock.$insert(0, 42n);
40+
await this.mock.$insert(0, 42n);
41+
await this.mock.$insert(0, 42n);
42+
await this.mock.$insert(0, 42n);
43+
await this.mock.$insert(0, 42n);
44+
45+
// pop 5 times
46+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
47+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
48+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
49+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
50+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
51+
52+
// poping a 6th time panics
53+
await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
54+
});
55+
56+
it('insert and pop', async function () {
57+
expect(await this.mock.$length(0)).to.equal(0n);
58+
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
59+
60+
await this.mock.$insert(0, 712n); // 712
61+
62+
expect(await this.mock.$length(0)).to.equal(1n);
63+
expect(await this.mock.$top(0)).to.equal(712n);
64+
65+
await this.mock.$insert(0, 20n); // 20, 712
66+
67+
expect(await this.mock.$length(0)).to.equal(2n);
68+
expect(await this.mock.$top(0)).to.equal(20n);
69+
70+
await this.mock.$insert(0, 4337n); // 20, 712, 4337
71+
72+
expect(await this.mock.$length(0)).to.equal(3n);
73+
expect(await this.mock.$top(0)).to.equal(20n);
74+
75+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(20n); // 712, 4337
76+
77+
expect(await this.mock.$length(0)).to.equal(2n);
78+
expect(await this.mock.$top(0)).to.equal(712n);
79+
80+
await this.mock.$insert(0, 1559n); // 712, 1559, 4337
81+
82+
expect(await this.mock.$length(0)).to.equal(3n);
83+
expect(await this.mock.$top(0)).to.equal(712n);
84+
85+
await this.mock.$insert(0, 155n); // 155, 712, 1559, 4337
86+
87+
expect(await this.mock.$length(0)).to.equal(4n);
88+
expect(await this.mock.$top(0)).to.equal(155n);
89+
90+
await this.mock.$insert(0, 7702n); // 155, 712, 1559, 4337, 7702
91+
92+
expect(await this.mock.$length(0)).to.equal(5n);
93+
expect(await this.mock.$top(0)).to.equal(155n);
94+
95+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(155n); // 712, 1559, 4337, 7702
96+
97+
expect(await this.mock.$length(0)).to.equal(4n);
98+
expect(await this.mock.$top(0)).to.equal(712n);
99+
100+
await this.mock.$insert(0, 721n); // 712, 721, 1559, 4337, 7702
101+
102+
expect(await this.mock.$length(0)).to.equal(5n);
103+
expect(await this.mock.$top(0)).to.equal(712n);
104+
105+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(712n); // 721, 1559, 4337, 7702
106+
107+
expect(await this.mock.$length(0)).to.equal(4n);
108+
expect(await this.mock.$top(0)).to.equal(721n);
109+
110+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(721n); // 1559, 4337, 7702
111+
112+
expect(await this.mock.$length(0)).to.equal(3n);
113+
expect(await this.mock.$top(0)).to.equal(1559n);
114+
115+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(1559n); // 4337, 7702
116+
117+
expect(await this.mock.$length(0)).to.equal(2n);
118+
expect(await this.mock.$top(0)).to.equal(4337n);
119+
120+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(4337n); // 7702
121+
122+
expect(await this.mock.$length(0)).to.equal(1n);
123+
expect(await this.mock.$top(0)).to.equal(7702n);
124+
125+
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(7702n); // <empty>
126+
127+
expect(await this.mock.$length(0)).to.equal(0n);
128+
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
129+
});
130+
});

0 commit comments

Comments
 (0)