Arrays.test.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { generators } = require('../helpers/random');
  5. // See https://en.cppreference.com/w/cpp/algorithm/lower_bound
  6. const lowerBound = (array, value) => {
  7. const i = array.findIndex(element => value <= element);
  8. return i == -1 ? array.length : i;
  9. };
  10. // See https://en.cppreference.com/w/cpp/algorithm/upper_bound
  11. const upperBound = (array, value) => {
  12. const i = array.findIndex(element => value < element);
  13. return i == -1 ? array.length : i;
  14. };
  15. const bigintSign = x => (x > 0n ? 1 : x < 0n ? -1 : 0);
  16. const comparator = (a, b) => bigintSign(ethers.toBigInt(a) - ethers.toBigInt(b));
  17. const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i);
  18. describe('Arrays', function () {
  19. const fixture = async () => {
  20. return { mock: await ethers.deployContract('$Arrays') };
  21. };
  22. beforeEach(async function () {
  23. Object.assign(this, await loadFixture(fixture));
  24. });
  25. describe('search', function () {
  26. for (const [title, { array, tests }] of Object.entries({
  27. 'Even number of elements': {
  28. array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n],
  29. tests: {
  30. 'basic case': 16n,
  31. 'first element': 11n,
  32. 'last element': 20n,
  33. 'searched value is over the upper boundary': 32n,
  34. 'searched value is under the lower boundary': 2n,
  35. },
  36. },
  37. 'Odd number of elements': {
  38. array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n, 21n],
  39. tests: {
  40. 'basic case': 16n,
  41. 'first element': 11n,
  42. 'last element': 21n,
  43. 'searched value is over the upper boundary': 32n,
  44. 'searched value is under the lower boundary': 2n,
  45. },
  46. },
  47. 'Array with gap': {
  48. array: [11n, 12n, 13n, 14n, 15n, 20n, 21n, 22n, 23n, 24n],
  49. tests: {
  50. 'search value in gap': 17n,
  51. },
  52. },
  53. 'Array with duplicated elements': {
  54. array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
  55. tests: {
  56. 'search value is duplicated': 10n,
  57. },
  58. },
  59. 'Array with duplicated first element': {
  60. array: [10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
  61. tests: {
  62. 'search value is duplicated first element': 10n,
  63. },
  64. },
  65. 'Array with duplicated last element': {
  66. array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n],
  67. tests: {
  68. 'search value is duplicated last element': 10n,
  69. },
  70. },
  71. 'Empty array': {
  72. array: [],
  73. tests: {
  74. 'always returns 0 for empty array': 10n,
  75. },
  76. },
  77. })) {
  78. describe(title, function () {
  79. const fixture = async () => {
  80. return { instance: await ethers.deployContract('Uint256ArraysMock', [array]) };
  81. };
  82. beforeEach(async function () {
  83. Object.assign(this, await loadFixture(fixture));
  84. });
  85. for (const [name, input] of Object.entries(tests)) {
  86. describe(name, function () {
  87. it('[deprecated] findUpperBound', async function () {
  88. // findUpperBound does not support duplicated
  89. if (hasDuplicates(array)) {
  90. expect(await this.instance.findUpperBound(input)).to.equal(upperBound(array, input) - 1);
  91. } else {
  92. expect(await this.instance.findUpperBound(input)).to.equal(lowerBound(array, input));
  93. }
  94. });
  95. it('lowerBound', async function () {
  96. expect(await this.instance.lowerBound(input)).to.equal(lowerBound(array, input));
  97. expect(await this.instance.lowerBoundMemory(array, input)).to.equal(lowerBound(array, input));
  98. });
  99. it('upperBound', async function () {
  100. expect(await this.instance.upperBound(input)).to.equal(upperBound(array, input));
  101. expect(await this.instance.upperBoundMemory(array, input)).to.equal(upperBound(array, input));
  102. });
  103. });
  104. }
  105. });
  106. }
  107. });
  108. for (const [type, { artifact, format }] of Object.entries({
  109. address: {
  110. artifact: 'AddressArraysMock',
  111. format: x => ethers.getAddress(ethers.toBeHex(x, 20)),
  112. },
  113. bytes32: {
  114. artifact: 'Bytes32ArraysMock',
  115. format: x => ethers.toBeHex(x, 32),
  116. },
  117. uint256: {
  118. artifact: 'Uint256ArraysMock',
  119. format: x => ethers.toBigInt(x),
  120. },
  121. })) {
  122. const elements = Array.from({ length: 10 }, generators[type]);
  123. describe(type, function () {
  124. const fixture = async () => {
  125. return { instance: await ethers.deployContract(artifact, [elements]) };
  126. };
  127. beforeEach(async function () {
  128. Object.assign(this, await loadFixture(fixture));
  129. });
  130. describe('sort', function () {
  131. for (const length of [0, 1, 2, 8, 32, 128]) {
  132. describe(`${type}[] of length ${length}`, function () {
  133. beforeEach(async function () {
  134. this.array = Array.from({ length }, generators[type]);
  135. });
  136. afterEach(async function () {
  137. const expected = Array.from(this.array).sort(comparator);
  138. const reversed = Array.from(expected).reverse();
  139. expect(await this.instance.sort(this.array)).to.deep.equal(expected);
  140. expect(await this.instance.sortReverse(this.array)).to.deep.equal(reversed);
  141. });
  142. it('sort array', async function () {
  143. // nothing to do here, beforeEach and afterEach already take care of everything.
  144. });
  145. if (length > 1) {
  146. it('sort array for identical elements', async function () {
  147. // duplicate the first value to all elements
  148. this.array.fill(this.array.at(0));
  149. });
  150. it('sort already sorted array', async function () {
  151. // pre-sort the elements
  152. this.array.sort(comparator);
  153. });
  154. it('sort reversed array', async function () {
  155. // pre-sort in reverse order
  156. this.array.sort(comparator).reverse();
  157. });
  158. it('sort almost sorted array', async function () {
  159. // pre-sort + rotate (move the last element to the front) for an almost sorted effect
  160. this.array.sort(comparator);
  161. this.array.unshift(this.array.pop());
  162. });
  163. }
  164. });
  165. }
  166. });
  167. describe('unsafeAccess', function () {
  168. describe('storage', function () {
  169. for (const i in elements) {
  170. it(`unsafeAccess within bounds #${i}`, async function () {
  171. expect(await this.instance.unsafeAccess(i)).to.equal(elements[i]);
  172. });
  173. }
  174. it('unsafeAccess outside bounds', async function () {
  175. await expect(this.instance.unsafeAccess(elements.length)).to.not.be.rejected;
  176. });
  177. it('unsafeSetLength changes the length or the array', async function () {
  178. const newLength = generators.uint256();
  179. expect(await this.instance.length()).to.equal(elements.length);
  180. await expect(this.instance.unsafeSetLength(newLength)).to.not.be.rejected;
  181. expect(await this.instance.length()).to.equal(newLength);
  182. });
  183. });
  184. describe('memory', function () {
  185. const fragment = `$unsafeMemoryAccess(${type}[] arr, uint256 pos)`;
  186. for (const i in elements) {
  187. it(`unsafeMemoryAccess within bounds #${i}`, async function () {
  188. expect(await this.mock[fragment](elements, i)).to.equal(elements[i]);
  189. });
  190. }
  191. it('unsafeMemoryAccess outside bounds', async function () {
  192. await expect(this.mock[fragment](elements, elements.length)).to.not.be.rejected;
  193. });
  194. it('unsafeMemoryAccess loop around', async function () {
  195. for (let i = 251n; i < 256n; ++i) {
  196. expect(await this.mock[fragment](elements, 2n ** i - 1n)).to.equal(format(elements.length));
  197. expect(await this.mock[fragment](elements, 2n ** i + 0n)).to.equal(elements[0]);
  198. expect(await this.mock[fragment](elements, 2n ** i + 1n)).to.equal(elements[1]);
  199. }
  200. });
  201. });
  202. });
  203. });
  204. }
  205. });