From b13bdb02492cca68091d56c31072b60f10e6142e Mon Sep 17 00:00:00 2001 From: Wias Liaw Date: Tue, 22 Mar 2022 23:36:29 +0800 Subject: [PATCH] Add bytes32 to bytes32 enumerable map (#3192) * feat(enumerablemap): add bytes32 to bytes32 map * chore(changelog): edit CHANGELOG * feat(enumerable map): edit struct visibility --- CHANGELOG.md | 1 + contracts/mocks/EnumerableMapMock.sol | 46 +++++ contracts/utils/structs/EnumerableMap.sol | 81 ++++---- test/utils/structs/EnumerableMap.behavior.js | 181 ++++++++++++++++++ test/utils/structs/EnumerableMap.test.js | 58 ++++++ .../EnumerableMap/AddressToUintMap.test.js | 157 --------------- .../EnumerableMap/UintToAddressMap.test.js | 157 --------------- test/utils/structs/EnumerableMap/helpers.js | 31 --- 8 files changed, 334 insertions(+), 378 deletions(-) create mode 100644 test/utils/structs/EnumerableMap.behavior.js create mode 100644 test/utils/structs/EnumerableMap.test.js delete mode 100644 test/utils/structs/EnumerableMap/AddressToUintMap.test.js delete mode 100644 test/utils/structs/EnumerableMap/UintToAddressMap.test.js delete mode 100644 test/utils/structs/EnumerableMap/helpers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5d150f605..3144e206395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * `AccessControl`: add a virtual `_checkRole(bytes32)` function that can be overridden to alter the `onlyRole` modifier behavior. ([#3137](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3137)) * `EnumerableMap`: add new `AddressToUintMap` map type. ([#3150](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3150)) + * `EnumerableMap`: add new `Bytes32ToBytes32Map` map type. ([#3192](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3192)) * `ERC1155`: Add a `_afterTokenTransfer` hook for improved extensibility. ([#3166](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3166)) * `DoubleEndedQueue`: a new data structure that supports efficient push and pop to both front and back, useful for FIFO and LIFO queues. ([#3153](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3153)) * `Governor`: improved security of `onlyGovernance` modifier when using an external executor contract (e.g. a timelock) that can operate without necessarily going through the governance protocol. ([#3147](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3147)) diff --git a/contracts/mocks/EnumerableMapMock.sol b/contracts/mocks/EnumerableMapMock.sol index 21c1fe362d9..7cecd0d60d7 100644 --- a/contracts/mocks/EnumerableMapMock.sol +++ b/contracts/mocks/EnumerableMapMock.sol @@ -84,4 +84,50 @@ contract AddressToUintMapMock { function get(address key) public view returns (uint256) { return _map.get(key); } + + function getWithMessage(address key, string calldata errorMessage) public view returns (uint256) { + return _map.get(key, errorMessage); + } +} + +contract Bytes32ToBytes32MapMock { + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + + event OperationResult(bool result); + + EnumerableMap.Bytes32ToBytes32Map private _map; + + function contains(bytes32 key) public view returns (bool) { + return _map.contains(key); + } + + function set(bytes32 key, bytes32 value) public { + bool result = _map.set(key, value); + emit OperationResult(result); + } + + function remove(bytes32 key) public { + bool result = _map.remove(key); + emit OperationResult(result); + } + + function length() public view returns (uint256) { + return _map.length(); + } + + function at(uint256 index) public view returns (bytes32 key, bytes32 value) { + return _map.at(index); + } + + function tryGet(bytes32 key) public view returns (bool, bytes32) { + return _map.tryGet(key); + } + + function get(bytes32 key) public view returns (bytes32) { + return _map.get(key); + } + + function getWithMessage(bytes32 key, string calldata errorMessage) public view returns (bytes32) { + return _map.get(key, errorMessage); + } } diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 1c3f4357ecf..8717c697811 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -30,6 +30,7 @@ import "./EnumerableSet.sol"; * * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0 * - `address -> uint256` (`AddressToUintMap`) since v4.6.0 + * - `bytes32 -> bytes32` (`Bytes32ToBytes32`) since v4.6.0 */ library EnumerableMap { using EnumerableSet for EnumerableSet.Bytes32Set; @@ -43,7 +44,7 @@ library EnumerableMap { // This means that we can only create new EnumerableMaps for types that fit // in bytes32. - struct Map { + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; mapping(bytes32 => bytes32) _values; @@ -56,11 +57,11 @@ library EnumerableMap { * Returns true if the key was added to the map, that is if it was not * already present. */ - function _set( - Map storage map, + function set( + Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value - ) private returns (bool) { + ) internal returns (bool) { map._values[key] = value; return map._keys.add(key); } @@ -70,7 +71,7 @@ library EnumerableMap { * * Returns true if the key was removed from the map, that is if it was present. */ - function _remove(Map storage map, bytes32 key) private returns (bool) { + function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) { delete map._values[key]; return map._keys.remove(key); } @@ -78,14 +79,14 @@ library EnumerableMap { /** * @dev Returns true if the key is in the map. O(1). */ - function _contains(Map storage map, bytes32 key) private view returns (bool) { + function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) { return map._keys.contains(key); } /** * @dev Returns the number of key-value pairs in the map. O(1). */ - function _length(Map storage map) private view returns (uint256) { + function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) { return map._keys.length(); } @@ -99,7 +100,7 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function _at(Map storage map, uint256 index) private view returns (bytes32, bytes32) { + function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { bytes32 key = map._keys.at(index); return (key, map._values[key]); } @@ -108,10 +109,10 @@ library EnumerableMap { * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function _tryGet(Map storage map, bytes32 key) private view returns (bool, bytes32) { + function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { bytes32 value = map._values[key]; if (value == bytes32(0)) { - return (_contains(map, key), bytes32(0)); + return (contains(map, key), bytes32(0)); } else { return (true, value); } @@ -124,9 +125,9 @@ library EnumerableMap { * * - `key` must be in the map. */ - function _get(Map storage map, bytes32 key) private view returns (bytes32) { + function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || _contains(map, key), "EnumerableMap: nonexistent key"); + require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); return value; } @@ -136,20 +137,20 @@ library EnumerableMap { * CAUTION: This function is deprecated because it requires allocating memory for the error * message unnecessarily. For custom revert reasons use {_tryGet}. */ - function _get( - Map storage map, + function get( + Bytes32ToBytes32Map storage map, bytes32 key, string memory errorMessage - ) private view returns (bytes32) { + ) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || _contains(map, key), errorMessage); + require(value != 0 || contains(map, key), errorMessage); return value; } // UintToAddressMap struct UintToAddressMap { - Map _inner; + Bytes32ToBytes32Map _inner; } /** @@ -164,7 +165,7 @@ library EnumerableMap { uint256 key, address value ) internal returns (bool) { - return _set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); + return set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); } /** @@ -173,21 +174,21 @@ library EnumerableMap { * Returns true if the key was removed from the map, that is if it was present. */ function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) { - return _remove(map._inner, bytes32(key)); + return remove(map._inner, bytes32(key)); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) { - return _contains(map._inner, bytes32(key)); + return contains(map._inner, bytes32(key)); } /** * @dev Returns the number of elements in the map. O(1). */ function length(UintToAddressMap storage map) internal view returns (uint256) { - return _length(map._inner); + return length(map._inner); } /** @@ -200,7 +201,7 @@ library EnumerableMap { * - `index` must be strictly less than {length}. */ function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { - (bytes32 key, bytes32 value) = _at(map._inner, index); + (bytes32 key, bytes32 value) = at(map._inner, index); return (uint256(key), address(uint160(uint256(value)))); } @@ -211,7 +212,7 @@ library EnumerableMap { * _Available since v3.4._ */ function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { - (bool success, bytes32 value) = _tryGet(map._inner, bytes32(key)); + (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); return (success, address(uint160(uint256(value)))); } @@ -223,7 +224,7 @@ library EnumerableMap { * - `key` must be in the map. */ function get(UintToAddressMap storage map, uint256 key) internal view returns (address) { - return address(uint160(uint256(_get(map._inner, bytes32(key))))); + return address(uint160(uint256(get(map._inner, bytes32(key))))); } /** @@ -237,13 +238,13 @@ library EnumerableMap { uint256 key, string memory errorMessage ) internal view returns (address) { - return address(uint160(uint256(_get(map._inner, bytes32(key), errorMessage)))); + return address(uint160(uint256(get(map._inner, bytes32(key), errorMessage)))); } // AddressToUintMap struct AddressToUintMap { - Map _inner; + Bytes32ToBytes32Map _inner; } /** @@ -258,7 +259,7 @@ library EnumerableMap { address key, uint256 value ) internal returns (bool) { - return _set(map._inner, bytes32(uint256(uint160(key))), bytes32(value)); + return set(map._inner, bytes32(uint256(uint160(key))), bytes32(value)); } /** @@ -267,21 +268,21 @@ library EnumerableMap { * Returns true if the key was removed from the map, that is if it was present. */ function remove(AddressToUintMap storage map, address key) internal returns (bool) { - return _remove(map._inner, bytes32(uint256(uint160(key)))); + return remove(map._inner, bytes32(uint256(uint160(key)))); } /** * @dev Returns true if the key is in the map. O(1). */ function contains(AddressToUintMap storage map, address key) internal view returns (bool) { - return _contains(map._inner, bytes32(uint256(uint160(key)))); + return contains(map._inner, bytes32(uint256(uint160(key)))); } /** * @dev Returns the number of elements in the map. O(1). */ function length(AddressToUintMap storage map) internal view returns (uint256) { - return _length(map._inner); + return length(map._inner); } /** @@ -294,7 +295,7 @@ library EnumerableMap { * - `index` must be strictly less than {length}. */ function at(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) { - (bytes32 key, bytes32 value) = _at(map._inner, index); + (bytes32 key, bytes32 value) = at(map._inner, index); return (address(uint160(uint256(key))), uint256(value)); } @@ -305,7 +306,7 @@ library EnumerableMap { * _Available since v3.4._ */ function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { - (bool success, bytes32 value) = _tryGet(map._inner, bytes32(uint256(uint160(key)))); + (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); return (success, uint256(value)); } @@ -317,6 +318,20 @@ library EnumerableMap { * - `key` must be in the map. */ function get(AddressToUintMap storage map, address key) internal view returns (uint256) { - return uint256(_get(map._inner, bytes32(uint256(uint160(key))))); + return uint256(get(map._inner, bytes32(uint256(uint160(key))))); + } + + /** + * @dev Same as {get}, with a custom error message when `key` is not in the map. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryGet}. + */ + function get( + AddressToUintMap storage map, + address key, + string memory errorMessage + ) internal view returns (uint256) { + return uint256(get(map._inner, bytes32(uint256(uint160(key))), errorMessage)); } } diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js new file mode 100644 index 00000000000..b1d0d0dceb0 --- /dev/null +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -0,0 +1,181 @@ +const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const zip = require('lodash.zip'); + +function shouldBehaveLikeMap (keys, values, zeroValue) { + const [keyA, keyB, keyC] = keys; + const [valueA, valueB, valueC] = values; + + async function expectMembersMatch (map, keys, values) { + expect(keys.length).to.equal(values.length); + + await Promise.all(keys.map(async key => + expect(await map.contains(key)).to.equal(true), + )); + + expect(await map.length()).to.bignumber.equal(keys.length.toString()); + + expect( + (await Promise.all(keys.map(key => map.get(key)))).map(k => k.toString()), + ).to.have.same.members( + values.map(value => value.toString()), + ); + + // To compare key-value pairs, we zip keys and values, and convert BNs to + // strings to workaround Chai limitations when dealing with nested arrays + expect(await Promise.all([...Array(keys.length).keys()].map(async (index) => { + const entry = await map.at(index); + return [entry.key.toString(), entry.value.toString()]; + }))).to.have.same.deep.members( + zip(keys.map(k => k.toString()), values.map(v => v.toString())), + ); + } + + it('starts empty', async function () { + expect(await this.map.contains(keyA)).to.equal(false); + + await expectMembersMatch(this.map, [], []); + }); + + describe('set', function () { + it('adds a key', async function () { + const receipt = await this.map.set(keyA, valueA); + expectEvent(receipt, 'OperationResult', { result: true }); + + await expectMembersMatch(this.map, [keyA], [valueA]); + }); + + it('adds several keys', async function () { + await this.map.set(keyA, valueA); + await this.map.set(keyB, valueB); + + await expectMembersMatch(this.map, [keyA, keyB], [valueA, valueB]); + expect(await this.map.contains(keyC)).to.equal(false); + }); + + it('returns false when adding keys already in the set', async function () { + await this.map.set(keyA, valueA); + + const receipt = (await this.map.set(keyA, valueA)); + expectEvent(receipt, 'OperationResult', { result: false }); + + await expectMembersMatch(this.map, [keyA], [valueA]); + }); + + it('updates values for keys already in the set', async function () { + await this.map.set(keyA, valueA); + + await this.map.set(keyA, valueB); + + await expectMembersMatch(this.map, [keyA], [valueB]); + }); + }); + + describe('remove', function () { + it('removes added keys', async function () { + await this.map.set(keyA, valueA); + + const receipt = await this.map.remove(keyA); + expectEvent(receipt, 'OperationResult', { result: true }); + + expect(await this.map.contains(keyA)).to.equal(false); + await expectMembersMatch(this.map, [], []); + }); + + it('returns false when removing keys not in the set', async function () { + const receipt = await this.map.remove(keyA); + expectEvent(receipt, 'OperationResult', { result: false }); + + expect(await this.map.contains(keyA)).to.equal(false); + }); + + it('adds and removes multiple keys', async function () { + // [] + + await this.map.set(keyA, valueA); + await this.map.set(keyC, valueC); + + // [A, C] + + await this.map.remove(keyA); + await this.map.remove(keyB); + + // [C] + + await this.map.set(keyB, valueB); + + // [C, B] + + await this.map.set(keyA, valueA); + await this.map.remove(keyC); + + // [A, B] + + await this.map.set(keyA, valueA); + await this.map.set(keyB, valueB); + + // [A, B] + + await this.map.set(keyC, valueC); + await this.map.remove(keyA); + + // [B, C] + + await this.map.set(keyA, valueA); + await this.map.remove(keyB); + + // [A, C] + + await expectMembersMatch(this.map, [keyA, keyC], [valueA, valueC]); + + expect(await this.map.contains(keyB)).to.equal(false); + }); + }); + + describe('read', function () { + beforeEach(async function () { + await this.map.set(keyA, valueA); + }); + + describe('get', function () { + it('existing value', async function () { + expect( + (await this.map.get(keyA)).toString(), + ).to.be.equal(valueA.toString()); + }); + it('missing value', async function () { + await expectRevert(this.map.get(keyB), 'EnumerableMap: nonexistent key'); + }); + }); + + describe('get with message', function () { + it('existing value', async function () { + expect( + (await this.map.getWithMessage(keyA, 'custom error string')) + .toString(), + ).to.be.equal(valueA.toString()); + }); + it('missing value', async function () { + await expectRevert(this.map.getWithMessage(keyB, 'custom error string'), 'custom error string'); + }); + }); + + describe('tryGet', function () { + it('existing value', async function () { + const result = await this.map.tryGet(keyA); + expect(result['0']).to.be.equal(true); + expect(result['1'].toString()).to.be.equal(valueA.toString()); + }); + it('missing value', async function () { + const result = await this.map.tryGet(keyB); + expect(result['0']).to.be.equal(false); + expect(result['1'].toString()).to.be.equal(zeroValue.toString()); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeMap, +}; diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js new file mode 100644 index 00000000000..866ff64397a --- /dev/null +++ b/test/utils/structs/EnumerableMap.test.js @@ -0,0 +1,58 @@ +const { BN, constants } = require('@openzeppelin/test-helpers'); + +const AddressToUintMapMock = artifacts.require('AddressToUintMapMock'); +const UintToAddressMapMock = artifacts.require('UintToAddressMapMock'); +const Bytes32ToBytes32MapMock = artifacts.require('Bytes32ToBytes32MapMock'); + +const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); + +contract('EnumerableMap', function (accounts) { + const [ accountA, accountB, accountC ] = accounts; + + const keyA = new BN('7891'); + const keyB = new BN('451'); + const keyC = new BN('9592328'); + + const bytesA = '0xdeadbeef'.padEnd(66, '0'); + const bytesB = '0x0123456789'.padEnd(66, '0'); + const bytesC = '0x42424242'.padEnd(66, '0'); + + // AddressToUintMap + describe('AddressToUintMap', function () { + beforeEach(async function () { + this.map = await AddressToUintMapMock.new(); + }); + + shouldBehaveLikeMap( + [accountA, accountB, accountC], + [keyA, keyB, keyC], + new BN('0'), + ); + }); + + // UintToAddressMap + describe('UintToAddressMap', function () { + beforeEach(async function () { + this.map = await UintToAddressMapMock.new(); + }); + + shouldBehaveLikeMap( + [keyA, keyB, keyC], + [accountA, accountB, accountC], + constants.ZERO_ADDRESS, + ); + }); + + // Bytes32ToBytes32Map + describe('Bytes32ToBytes32Map', function () { + beforeEach(async function () { + this.map = await Bytes32ToBytes32MapMock.new(); + }); + + shouldBehaveLikeMap( + [keyA, keyB, keyC].map(k => ('0x' + k.toString(16)).padEnd(66, '0')), + [bytesA, bytesB, bytesC], + constants.ZERO_BYTES32, + ); + }); +}); diff --git a/test/utils/structs/EnumerableMap/AddressToUintMap.test.js b/test/utils/structs/EnumerableMap/AddressToUintMap.test.js deleted file mode 100644 index 0a79479323e..00000000000 --- a/test/utils/structs/EnumerableMap/AddressToUintMap.test.js +++ /dev/null @@ -1,157 +0,0 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { expectMembersMatch } = require('./helpers'); - -const AddressToUintMapMock = artifacts.require('AddressToUintMapMock'); - -contract('AddressToUintMap', function (accounts) { - const [accountA, accountB, accountC] = accounts; - - const valueA = new BN('7891'); - const valueB = new BN('451'); - const valueC = new BN('9592328'); - - beforeEach(async function () { - this.map = await AddressToUintMapMock.new(); - }); - - it('starts empty', async function () { - expect(await this.map.contains(accountA)).to.equal(false); - - await expectMembersMatch(this.map, [], []); - }); - - describe('set', function () { - it('adds a key', async function () { - const receipt = await this.map.set(accountA, valueA); - - expectEvent(receipt, 'OperationResult', { result: true }); - - await expectMembersMatch(this.map, [accountA], [valueA]); - }); - - it('adds several keys', async function () { - await this.map.set(accountA, valueA); - await this.map.set(accountB, valueB); - - await expectMembersMatch(this.map, [accountA, accountB], [valueA, valueB]); - - expect(await this.map.contains(accountC)).to.equal(false); - }); - - it('returns false when adding keys already in the set', async function () { - await this.map.set(accountA, valueA); - - const receipt = await this.map.set(accountA, valueA); - - expectEvent(receipt, 'OperationResult', { result: false }); - - await expectMembersMatch(this.map, [accountA], [valueA]); - }); - - it('updates values for keys already in the set', async function () { - await this.map.set(accountA, valueA); - await this.map.set(accountA, valueB); - - await expectMembersMatch(this.map, [accountA], [valueB]); - }); - }); - - describe('remove', function () { - it('removes added keys', async function () { - await this.map.set(accountA, valueA); - - const receipt = await this.map.remove(accountA); - - expectEvent(receipt, 'OperationResult', { result: true }); - - expect(await this.map.contains(accountA)).to.equal(false); - - await expectMembersMatch(this.map, [], []); - }); - - it('returns false when removing keys not in the set', async function () { - const receipt = await this.map.remove(accountA); - - expectEvent(receipt, 'OperationResult', { result: false }); - - expect(await this.map.contains(accountA)).to.equal(false); - }); - - it('adds and removes multiple keys', async function () { - // [] - - await this.map.set(accountA, valueA); - await this.map.set(accountC, valueC); - - // [A, C] - - await this.map.remove(accountA); - await this.map.remove(accountB); - - // [C] - - await this.map.set(accountB, valueB); - - // [C, B] - - await this.map.set(accountA, valueA); - await this.map.remove(accountC); - - // [A, B] - - await this.map.set(accountA, valueA); - await this.map.set(accountB, valueB); - - // [A, B] - - await this.map.set(accountC, valueC); - await this.map.remove(accountA); - - // [B, C] - - await this.map.set(accountA, valueA); - await this.map.remove(accountB); - - // [A, C] - - await expectMembersMatch(this.map, [accountA, accountC], [valueA, valueC]); - - expect(await this.map.contains(accountB)).to.equal(false); - }); - }); - - describe('read', function () { - beforeEach(async function () { - await this.map.set(accountA, valueA); - }); - - describe('get', function () { - it('existing value', async function () { - expect(await this.map.get(accountA)).to.bignumber.equal(valueA); - }); - - it('missing value', async function () { - await expectRevert(this.map.get(accountB), 'EnumerableMap: nonexistent key'); - }); - }); - - describe('tryGet', function () { - const stringifyTryGetValue = ({ 0: result, 1: value }) => ({ 0: result, 1: value.toString() }); - - it('existing value', async function () { - const actual = stringifyTryGetValue(await this.map.tryGet(accountA)); - const expected = stringifyTryGetValue({ 0: true, 1: valueA }); - - expect(actual).to.deep.equal(expected); - }); - - it('missing value', async function () { - const actual = stringifyTryGetValue(await this.map.tryGet(accountB)); - const expected = stringifyTryGetValue({ 0: false, 1: new BN('0') }); - - expect(actual).to.deep.equal(expected); - }); - }); - }); -}); diff --git a/test/utils/structs/EnumerableMap/UintToAddressMap.test.js b/test/utils/structs/EnumerableMap/UintToAddressMap.test.js deleted file mode 100644 index 560ed507701..00000000000 --- a/test/utils/structs/EnumerableMap/UintToAddressMap.test.js +++ /dev/null @@ -1,157 +0,0 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { expectMembersMatch } = require('./helpers'); - -const UintToAddressMapMock = artifacts.require('UintToAddressMapMock'); - -contract('UintToAddressMap', function (accounts) { - const [accountA, accountB, accountC] = accounts; - - const keyA = new BN('7891'); - const keyB = new BN('451'); - const keyC = new BN('9592328'); - - beforeEach(async function () { - this.map = await UintToAddressMapMock.new(); - }); - - it('starts empty', async function () { - expect(await this.map.contains(keyA)).to.equal(false); - - await expectMembersMatch(this.map, [], []); - }); - - describe('set', function () { - it('adds a key', async function () { - const receipt = await this.map.set(keyA, accountA); - expectEvent(receipt, 'OperationResult', { result: true }); - - await expectMembersMatch(this.map, [keyA], [accountA]); - }); - - it('adds several keys', async function () { - await this.map.set(keyA, accountA); - await this.map.set(keyB, accountB); - - await expectMembersMatch(this.map, [keyA, keyB], [accountA, accountB]); - expect(await this.map.contains(keyC)).to.equal(false); - }); - - it('returns false when adding keys already in the set', async function () { - await this.map.set(keyA, accountA); - - const receipt = await this.map.set(keyA, accountA); - expectEvent(receipt, 'OperationResult', { result: false }); - - await expectMembersMatch(this.map, [keyA], [accountA]); - }); - - it('updates values for keys already in the set', async function () { - await this.map.set(keyA, accountA); - - await this.map.set(keyA, accountB); - - await expectMembersMatch(this.map, [keyA], [accountB]); - }); - }); - - describe('remove', function () { - it('removes added keys', async function () { - await this.map.set(keyA, accountA); - - const receipt = await this.map.remove(keyA); - expectEvent(receipt, 'OperationResult', { result: true }); - - expect(await this.map.contains(keyA)).to.equal(false); - await expectMembersMatch(this.map, [], []); - }); - - it('returns false when removing keys not in the set', async function () { - const receipt = await this.map.remove(keyA); - expectEvent(receipt, 'OperationResult', { result: false }); - - expect(await this.map.contains(keyA)).to.equal(false); - }); - - it('adds and removes multiple keys', async function () { - // [] - - await this.map.set(keyA, accountA); - await this.map.set(keyC, accountC); - - // [A, C] - - await this.map.remove(keyA); - await this.map.remove(keyB); - - // [C] - - await this.map.set(keyB, accountB); - - // [C, B] - - await this.map.set(keyA, accountA); - await this.map.remove(keyC); - - // [A, B] - - await this.map.set(keyA, accountA); - await this.map.set(keyB, accountB); - - // [A, B] - - await this.map.set(keyC, accountC); - await this.map.remove(keyA); - - // [B, C] - - await this.map.set(keyA, accountA); - await this.map.remove(keyB); - - // [A, C] - - await expectMembersMatch(this.map, [keyA, keyC], [accountA, accountC]); - - expect(await this.map.contains(keyB)).to.equal(false); - }); - }); - - describe('read', function () { - beforeEach(async function () { - await this.map.set(keyA, accountA); - }); - - describe('get', function () { - it('existing value', async function () { - expect(await this.map.get(keyA)).to.be.equal(accountA); - }); - it('missing value', async function () { - await expectRevert(this.map.get(keyB), 'EnumerableMap: nonexistent key'); - }); - }); - - describe('get with message', function () { - it('existing value', async function () { - expect(await this.map.getWithMessage(keyA, 'custom error string')).to.be.equal(accountA); - }); - it('missing value', async function () { - await expectRevert(this.map.getWithMessage(keyB, 'custom error string'), 'custom error string'); - }); - }); - - describe('tryGet', function () { - it('existing value', async function () { - expect(await this.map.tryGet(keyA)).to.be.deep.equal({ - 0: true, - 1: accountA, - }); - }); - it('missing value', async function () { - expect(await this.map.tryGet(keyB)).to.be.deep.equal({ - 0: false, - 1: constants.ZERO_ADDRESS, - }); - }); - }); - }); -}); diff --git a/test/utils/structs/EnumerableMap/helpers.js b/test/utils/structs/EnumerableMap/helpers.js deleted file mode 100644 index 456cdf156f3..00000000000 --- a/test/utils/structs/EnumerableMap/helpers.js +++ /dev/null @@ -1,31 +0,0 @@ -const zip = require('lodash.zip'); - -const toStringArray = (array) => array.map((i) => i.toString()); - -async function expectMembersMatch (map, keys, values) { - const stringKeys = toStringArray(keys); - const stringValues = toStringArray(values); - - expect(keys.length).to.equal(values.length); - - await Promise.all(keys.map(async (key) => expect(await map.contains(key)).to.equal(true))); - - expect(await map.length()).to.bignumber.equal(keys.length.toString()); - - expect(toStringArray(await Promise.all(keys.map((key) => map.get(key))))).to.have.same.members(stringValues); - - // to compare key-value pairs, we zip keys and values - // we also stringify pairs because this helper is used for multiple types of maps - expect( - await Promise.all( - [...Array(keys.length).keys()].map(async (index) => { - const { key, value } = await map.at(index); - return [key.toString(), value.toString()]; - }), - ), - ).to.have.same.deep.members(zip(stringKeys, stringValues)); -} - -module.exports = { - expectMembersMatch, -};