From 454d69f9dfadf0b48daeee05846243b26abba479 Mon Sep 17 00:00:00 2001 From: leeheonseung Date: Mon, 2 Mar 2020 16:09:40 +0900 Subject: [PATCH 1/2] IS-1021: DictDB iteration causes infinite loop officially we do not support iteration on DictDB so add raise Exception code references https://www.python.org/dev/peps/pep-0234/ https://docs.python.org/3/reference/datamodel.html#object.__getitem__ --- iconservice/iconscore/icon_container_db.py | 3 ++ tests/icon_score/test_icon_container_db.py | 30 +++++++++++++ .../sample_scores/sample_dict_db/package.json | 5 +++ .../sample_dict_db/sample_dict_db.py | 33 ++++++++++++++ ...y => test_integrate_container_db_patch.py} | 43 +++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 tests/integrate_test/samples/sample_scores/sample_dict_db/package.json create mode 100644 tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py rename tests/integrate_test/{test_integrate_array_db_patch.py => test_integrate_container_db_patch.py} (72%) diff --git a/iconservice/iconscore/icon_container_db.py b/iconservice/iconscore/icon_container_db.py index 4b2be31c1..36e6d5d23 100644 --- a/iconservice/iconscore/icon_container_db.py +++ b/iconservice/iconscore/icon_container_db.py @@ -216,6 +216,9 @@ def __remove(self, key: K) -> None: raise InvalidContainerAccessException('DictDB depth mismatch') self._db.delete(get_encoded_key(key)) + def __iter__(self): + raise InvalidContainerAccessException("Not Supported on DictDB") + class ArrayDB(object): """ diff --git a/tests/icon_score/test_icon_container_db.py b/tests/icon_score/test_icon_container_db.py index 7ddbdbb6f..3ade384f8 100644 --- a/tests/icon_score/test_icon_container_db.py +++ b/tests/icon_score/test_icon_container_db.py @@ -339,3 +339,33 @@ def test_container_util(self): with self.assertRaises(InvalidParamsException): prefix: bytes = ContainerUtil.create_db_prefix(VarDB, 'vardb') + +class TestOnlyGetItemObj: + def __init__(self, limit): + self._limit = limit + + def __getitem__(self, key): + if key > self._limit: + raise IndexError(key) + + +class TestIterObj(TestOnlyGetItemObj): + def __iter__(self): + pass + + +class TestContainerDB(unittest.TestCase): + def test_getitem(self): + limit = 100 + datas = TestOnlyGetItemObj(limit) + index = 0 + for index, e in enumerate(datas): + pass + self.assertEqual(index, limit) + + def test_iter(self): + limit = 100 + datas = TestIterObj(limit) + with self.assertRaises(TypeError) as e: + for e in datas: + pass diff --git a/tests/integrate_test/samples/sample_scores/sample_dict_db/package.json b/tests/integrate_test/samples/sample_scores/sample_dict_db/package.json new file mode 100644 index 000000000..6cc62a78a --- /dev/null +++ b/tests/integrate_test/samples/sample_scores/sample_dict_db/package.json @@ -0,0 +1,5 @@ +{ + "version": "0.0.1", + "main_file": "sample_dict_db", + "main_score": "IterableDictDB" +} \ No newline at end of file diff --git a/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py b/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py new file mode 100644 index 000000000..9bfd3b355 --- /dev/null +++ b/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from iconservice import * +TAG = 'IterableDictDB' + + +class IterableDictDB(IconScoreBase): + """ IterableDictDB SCORE Base implementation """ + + # ================================================ + # Initialization + # ================================================ + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + self._dict = DictDB('DICT', db, value_type=int) + + def on_install(self) -> None: + super().on_install() + + def on_update(self) -> None: + super().on_update() + + @external + def create_item(self, key: str, value: int) -> None: + self._dict[key] = value + + @external(readonly=True) + def get_items(self) -> list: + items = [] + for item in self._dict: + print("11") + items.append(item) + return items diff --git a/tests/integrate_test/test_integrate_array_db_patch.py b/tests/integrate_test/test_integrate_container_db_patch.py similarity index 72% rename from tests/integrate_test/test_integrate_array_db_patch.py rename to tests/integrate_test/test_integrate_container_db_patch.py index a6ddf2529..c958138e9 100644 --- a/tests/integrate_test/test_integrate_array_db_patch.py +++ b/tests/integrate_test/test_integrate_container_db_patch.py @@ -21,6 +21,7 @@ from iconservice.base.address import GOVERNANCE_SCORE_ADDRESS from iconservice.icon_constant import Revision +from iconservice.base.exception import InvalidContainerAccessException from tests.integrate_test.test_integrate_base import TestIntegrateBase if TYPE_CHECKING: @@ -112,3 +113,45 @@ def test_array_db_patch(self): ) self.assertEqual(len(response), 2) + + +class TestIntegrateDictDBPatch(TestIntegrateBase): + def test_dict_db_defective(self): + self.update_governance("0_0_4") + + expected_status = { + "code": Revision.TWO.value, + "name": "1.1.0" + } + + query_request = { + "version": self._version, + "from": self._accounts[0], + "to": GOVERNANCE_SCORE_ADDRESS, + "dataType": "call", + "data": { + "method": "getRevision", + "params": {} + } + } + response = self._query(query_request) + self.assertEqual(expected_status, response) + + tx_results: List['TransactionResult'] = self.deploy_score("sample_scores", + "sample_dict_db", + self._accounts[0]) + score_address: 'Address' = tx_results[0].score_address + + self.score_call(self._accounts[0], score_address, "create_item", params={"key": "a", "value": hex(1)}) + + with self.assertRaises(InvalidContainerAccessException) as e: + self._query( + { + 'to': score_address, + 'dataType': 'call', + 'data': { + 'method': 'get_items' + } + } + ) + self.assertEqual(e.exception.message, "Not Supported on DictDB") From 16f850f6e2fd5837f0c8763eeac13e605225f63d Mon Sep 17 00:00:00 2001 From: leeheonseung Date: Thu, 5 Mar 2020 09:50:04 +0900 Subject: [PATCH 2/2] apply feedback add comment, append detail log message --- iconservice/iconscore/icon_container_db.py | 2 +- tests/icon_score/test_icon_container_db.py | 26 ++++++++++++++++--- .../sample_dict_db/sample_dict_db.py | 8 +++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/iconservice/iconscore/icon_container_db.py b/iconservice/iconscore/icon_container_db.py index 36e6d5d23..f226cd0c7 100644 --- a/iconservice/iconscore/icon_container_db.py +++ b/iconservice/iconscore/icon_container_db.py @@ -217,7 +217,7 @@ def __remove(self, key: K) -> None: self._db.delete(get_encoded_key(key)) def __iter__(self): - raise InvalidContainerAccessException("Not Supported on DictDB") + raise InvalidContainerAccessException("Not Supported iter function on DictDB") class ArrayDB(object): diff --git a/tests/icon_score/test_icon_container_db.py b/tests/icon_score/test_icon_container_db.py index 3ade384f8..c42afbe1a 100644 --- a/tests/icon_score/test_icon_container_db.py +++ b/tests/icon_score/test_icon_container_db.py @@ -340,7 +340,20 @@ def test_container_util(self): prefix: bytes = ContainerUtil.create_db_prefix(VarDB, 'vardb') +""" +Dict DB infinity infinite case +implement __getitem__ function means can be iter +https://www.python.org/dev/peps/pep-0234 +https://docs.python.org/3/reference/datamodel.html#object.__getitem__ +""" + + class TestOnlyGetItemObj: + """ + Override getitem function + If you don't consider this case, this logic will always have an infinite loop. + https://stackoverflow.com/questions/926574/why-does-defining-getitem-on-a-class-make-it-iterable-in-python + """ def __init__(self, limit): self._limit = limit @@ -350,13 +363,19 @@ def __getitem__(self, key): class TestIterObj(TestOnlyGetItemObj): + """ + Override the iter function. + This logic prevents the above infinite loop situation. + https://stackoverflow.com/questions/926574/why-does-defining-getitem-on-a-class-make-it-iterable-in-python + """ def __iter__(self): pass -class TestContainerDB(unittest.TestCase): +class TestIsAvailableToIterator(unittest.TestCase): def test_getitem(self): - limit = 100 + # it is possible to use foreach by using getitem + limit = 100_000 datas = TestOnlyGetItemObj(limit) index = 0 for index, e in enumerate(datas): @@ -364,7 +383,8 @@ def test_getitem(self): self.assertEqual(index, limit) def test_iter(self): - limit = 100 + # prevent infinite loop by using empty __iter function + limit = 100_000 datas = TestIterObj(limit) with self.assertRaises(TypeError) as e: for e in datas: diff --git a/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py b/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py index 9bfd3b355..a304df4c5 100644 --- a/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py +++ b/tests/integrate_test/samples/sample_scores/sample_dict_db/sample_dict_db.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- +""" +Example + +Problem with DictDB : cannot iterate +https://forum.icon.community/t/problem-with-dictdb-cannot-iterate/484 +""" + from iconservice import * TAG = 'IterableDictDB' @@ -28,6 +35,5 @@ def create_item(self, key: str, value: int) -> None: def get_items(self) -> list: items = [] for item in self._dict: - print("11") items.append(item) return items