Arrays.test.js 7.6 KB

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