Arrays.test.js 7.2 KB

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