Arrays.test.js 8.3 KB

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