From 14cd420cfb9cf2c5fdadc71f8b44b5a7f8242ce6 Mon Sep 17 00:00:00 2001 From: "S. Mahmudul Hasan" Date: Wed, 18 Oct 2023 01:45:18 -0400 Subject: [PATCH] Fixed some functionalities and added more tests --- src/collections.cpp | 92 +++++--- tests/70_collections.py | 472 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 512 insertions(+), 52 deletions(-) diff --git a/src/collections.cpp b/src/collections.cpp index 95af11aa..9422f8a1 100644 --- a/src/collections.cpp +++ b/src/collections.cpp @@ -12,6 +12,7 @@ namespace pkpy bool bounded = false; // if true, maxlen is not -1 void insertObj(bool front, bool back, int index, PyObject *item); // insert at index, used purely for internal purposes: append, appendleft, insert methods PyObject *popObj(bool front, bool back, PyObject *item, VM *vm); // pop at index, used purely for internal purposes: pop, popleft, remove methods + int findIndex(VM *vm, PyObject *obj, int start, int stop); // find the index of the given object in the deque std::stringstream getRepr(VM *vm); // get the string representation of the deque // Special methods static void _register(VM *vm, PyObject *mod, PyObject *type); // register the type @@ -235,8 +236,8 @@ namespace pkpy { if (vm->py_equals((*it), obj)) cnt++; - if (sz != self.dequeItems.size()) // mutating the deque during iteration is not allowed - vm->RuntimeError("deque mutated during iteration");//TODO replace this with RuntimeError + if (sz != self.dequeItems.size()) // mutating the deque during iteration is not allowed + vm->RuntimeError("deque mutated during iteration"); // TODO replace this with RuntimeError } return VAR(cnt); }); @@ -267,31 +268,26 @@ namespace pkpy start = CAST(int, args[2]); if (!vm->py_equals(args[3], vm->None)) stop = CAST(int, args[3]); - // the following code is special purpose normalization for this method, taken from CPython: _collectionsmodule.c file - if (start < 0) - { - start = self.dequeItems.size() + start; // try to fix for negative indices - if (start < 0) - start = 0; - } - if (stop < 0) - { - stop = self.dequeItems.size() + stop; // try to fix for negative indices - if (stop < 0) - stop = 0; - } - if (stop > self.dequeItems.size()) - stop = self.dequeItems.size(); - if (start > stop) - start = stop; // end of normalization - PK_ASSERT(start >= 0 && start <= self.dequeItems.size() && stop >= 0 && stop <= self.dequeItems.size() && start <= stop); // sanity check - int loopSize = std::min((int)self.dequeItems.size(), stop); - for (int i = start; i < loopSize; i++) - if (vm->py_equals(self.dequeItems[i], obj)) - return VAR(i); - vm->ValueError(_CAST(Str &, vm->py_repr(obj)) + " is not in deque"); + int index = self.findIndex(vm, obj, start, stop); + if (index != -1) + return VAR(index); + else + vm->ValueError(_CAST(Str &, vm->py_repr(obj)) + " is not in deque"); return vm->None; }); + // NEW: returns the index of the given object in the deque + vm->bind(type, "__contains__(self, obj) -> bool", + [](VM *vm, ArgsView args) + { + // Return the position of x in the deque (at or after index start and before index stop). Returns the first match or raises ValueError if not found. + PyDeque &self = _CAST(PyDeque &, args[0]); + PyObject *obj = args[1]; + int start = 0, stop = self.dequeItems.size(); // default values + int index = self.findIndex(vm, obj, start, stop); + if (index != -1) + return VAR(true); + return VAR(false); + }); // NEW: inserts the given object at the given index vm->bind(type, "insert(self, index, obj) -> None", [](VM *vm, ArgsView args) @@ -300,7 +296,7 @@ namespace pkpy int index = CAST(int, args[1]); PyObject *obj = args[2]; if (self.bounded && self.dequeItems.size() == self.maxlen) - vm->ValueError("deque already at its maximum size"); + vm->IndexError("deque already at its maximum size"); else self.insertObj(false, false, index, obj); // this index shouldn't be fixed using vm->normalized_index, pass as is return vm->None; @@ -338,7 +334,8 @@ namespace pkpy { PyDeque &self = _CAST(PyDeque &, args[0]); int n = CAST(int, args[1]); - if (n != 0) // trivial case + + if (n != 0 && !self.dequeItems.empty()) // trivial case { PyObject *tmp; // holds the object to be rotated int direction = n > 0 ? 1 : -1; @@ -400,6 +397,39 @@ namespace pkpy PyDeque::PyDeque(VM *vm, PyObject *iterable, PyObject *maxlen) { } + + int PyDeque::findIndex(VM *vm, PyObject *obj, int start, int stop) + { + // the following code is special purpose normalization for this method, taken from CPython: _collectionsmodule.c file + if (start < 0) + { + start = this->dequeItems.size() + start; // try to fix for negative indices + if (start < 0) + start = 0; + } + if (stop < 0) + { + stop = this->dequeItems.size() + stop; // try to fix for negative indices + if (stop < 0) + stop = 0; + } + if (stop > this->dequeItems.size()) + stop = this->dequeItems.size(); + if (start > stop) + start = stop; // end of normalization + PK_ASSERT(start >= 0 && start <= this->dequeItems.size() && stop >= 0 && stop <= this->dequeItems.size() && start <= stop); // sanity check + int loopSize = std::min((int)(this->dequeItems.size()), stop); + int sz = this->dequeItems.size(); + for (int i = start; i < loopSize; i++) + { + if (vm->py_equals(this->dequeItems[i], obj)) + return i; + if (sz != this->dequeItems.size()) // mutating the deque during iteration is not allowed + vm->RuntimeError("deque mutated during iteration"); // TODO replace this with RuntimeError + } + return -1; + } + /// @brief pops or removes an item from the deque /// @param front if true, pop from the front of the deque /// @param back if true, pop from the back of the deque @@ -432,13 +462,19 @@ namespace pkpy else { // front and back are not set, we care about item, this is a remove operation and we return the removed item or nullptr + int sz = this->dequeItems.size(); for (auto it = this->dequeItems.begin(); it != this->dequeItems.end(); ++it) - if (vm->py_equals((*it), item)) + { + bool found = vm->py_equals((*it), item); + if (sz != this->dequeItems.size()) // mutating the deque during iteration is not allowed + vm->IndexError("deque mutated during iteration"); + if (found) { PyObject *obj = *it; // keep a reference to the object for returning this->dequeItems.erase(it); return obj; } + } return nullptr; // not found } } diff --git a/tests/70_collections.py b/tests/70_collections.py index aee738e2..ee265cfc 100644 --- a/tests/70_collections.py +++ b/tests/70_collections.py @@ -17,9 +17,13 @@ assert q == deque([1, 2]) ## ADDING TESTS FROM CPYTHON's test_deque.py file -############TEST BASICS############### +############TEST basics############### def assertEqual(a, b): assert a == b +BIG = 100000 +def fail(): + raise SyntaxError + yield 1 d = deque(range(-5125, -5000)) d.__init__(range(200)) @@ -40,7 +44,7 @@ right.reverse() assertEqual(right, list(range(150, 400))) assertEqual(list(d), list(range(50, 150))) -#######TEST MAXLEN############### +#######TEST maxlen############### try: dq = deque() dq.maxlen = -1 @@ -73,7 +77,7 @@ assertEqual(repr(d)[-30:], ', 198, 199, [...]], maxlen=10)') d = deque(range(10), maxlen=None) assertEqual(repr(d), 'deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])') -#######TEST MAXLEN SIZE 0############### +#######TEST maxlen = 0############### it = iter(range(100)) deque(it, maxlen=0) assertEqual(list(it), []) @@ -89,7 +93,7 @@ d.extendleft(it) assertEqual(list(it), []) -#######TEST MAXLEN ATTRIBUTE############# +#######TEST maxlen attribute ############# assertEqual(deque().maxlen, None) assertEqual(deque('abc').maxlen, None) @@ -99,12 +103,12 @@ assertEqual(deque('abc', maxlen=0).maxlen, 0) try: d = deque('abc') d.maxlen = 10 - print("Failed Tests!!") + print("X Failed Tests!!") exit(1) except AttributeError: pass -######### TEST COUNT################# +######### TEST count()################# for s in ('', 'abracadabra', 'simsalabim'*500+'abc'): s = list(s) d = deque(s) @@ -112,14 +116,14 @@ for s in ('', 'abracadabra', 'simsalabim'*500+'abc'): assertEqual(s.count(letter), d.count(letter)) try: d.count() - print("Failed Tests!!") + print("X Failed Tests!!") exit(1) except TypeError: pass try: d.count(1,2) - print("Failed Tests!!") + print("X Failed Tests!!") exit(1) except TypeError: pass @@ -131,7 +135,7 @@ d = deque([1, 2, BadCompare(), 3]) try: d.count(2) - print("Failed Tests!!") + print("X Failed Tests!!") exit(1) except ArithmeticError: pass @@ -139,7 +143,7 @@ except ArithmeticError: d = deque([1, 2, 3]) try: d.count(BadCompare()) - print("Failed Tests!!") + print("X Failed Tests!!") exit(1) except ArithmeticError: pass @@ -154,7 +158,7 @@ m.d = d try: d.count(3) - print("Failed Tests!") + print("X Failed Tests!") exit(1) except RuntimeError: pass @@ -166,7 +170,7 @@ d.rotate(1) assertEqual(d.count(1), 0) assertEqual(d.count(None), 16) -#### TEST COMPARISONS##### +#### TEST comparisons == ##### d = deque('xabc') d.popleft() @@ -184,7 +188,9 @@ for x in args: # assertEqual(x > y, list(x) > list(y)) # not currently supported # assertEqual(x >= y, list(x) >= list(y)) # not currently supported -###############TEST CONTAINS################# + + +###############TEST contains()################# n = 200 @@ -202,23 +208,441 @@ class MutateCmp: self.deque.clear() return self.result -# Test detection of mutation during iteration -# d = deque(range(n)) -# d[n//2] = MutateCmp(d, False) -# print(n in d) -# with assertRaises(RuntimeError): -# n in d +# # Test detection of mutation during iteration +d = deque(range(n)) +d[n//2] = MutateCmp(d, False) +try: + n in d + print("X Failed Tests!") + exit(1) +except RuntimeError: + pass + +class BadCmp: + def __eq__(self, other): + raise RuntimeError # # Test detection of comparison exceptions -# d = deque(range(n)) -# d[n//2] = BadCmp() -# with assertRaises(RuntimeError): -# n in d +d = deque(range(n)) +d[n//2] = BadCmp() +try: + n in d + print("X Failed Tests!") + exit(1) +except RuntimeError: + pass + + +#####test_contains_count_stop_crashes##### + +class A: + def __eq__(self, other): + d.clear() + return NotImplemented +d = deque([A(), A()]) + +try: + _ = 3 in d + print("X Failed Tests!") + exit(1) +except RuntimeError: + pass + +d = deque([A(), A()]) +try: + _ = d.count(3) + print("X Failed Tests!") + exit(1) +except RuntimeError: + pass + + +########TEST extend()################ + + +d = deque('a') +try: + d.extend(1) + print("X Failed Tests!") + exit(1) +except TypeError: + pass +d.extend('bcd') +assertEqual(list(d), list('abcd')) +d.extend(d) +assertEqual(list(d), list('abcdabcd')) + +######TEST extend_left() ################ + +d = deque('a') +try: + d.extendleft(1) + print("X Failed Tests!") + exit(1) +except TypeError: + pass +d.extendleft('bcd') +assertEqual(list(d), list(reversed('abcd'))) +d.extendleft(d) +assertEqual(list(d), list('abcddcba')) +d = deque() +d.extendleft(range(1000)) +assertEqual(list(d), list(reversed(range(1000)))) +try: + d.extendleft(fail()) + print("X Failed Tests!") + exit(1) +except SyntaxError: + pass + +##### TEST get_item ################ + +n = 200 +d = deque(range(n)) +l = list(range(n)) +for i in range(n): + d.popleft() + l.pop(0) + if random.random() < 0.5: + d.append(i) + l.append(i) + for j in range(1-len(l), len(l)): + assert d[j] == l[j] + +d = deque('superman') +assertEqual(d[0], 's') +assertEqual(d[-1], 'n') +d = deque() +try: + d.__getitem__(0) + print("X Failed Tests!") + exit(1) +except IndexError: + pass +try: + d.__getitem__(-1) + print("X Failed Tests!") + exit(1) +except IndexError: + pass + + +#########TEST index()############### +for n in 1, 2, 30, 40, 200: + + d = deque(range(n)) + for i in range(n): + assertEqual(d.index(i), i) + + try: + d.index(n+1) + print("X Failed Tests!") + exit(1) + except ValueError: + pass + + # Test detection of mutation during iteration + d = deque(range(n)) + d[n//2] = MutateCmp(d, False) + + try: + d.index(n) + print("X Failed Tests!") + exit(1) + except RuntimeError: + pass + + # Test detection of comparison exceptions + d = deque(range(n)) + d[n//2] = BadCmp() + + try: + d.index(n) + print("X Failed Tests!") + exit(1) + except RuntimeError: + pass + + +# Test start and stop arguments behavior matches list.index() +#COMMENT: Current List behavior doesn't support start and stop arguments, so this test is not supported +# elements = 'ABCDEFGHI' +# nonelement = 'Z' +# d = deque(elements * 2) +# s = list(elements * 2) +# for start in range(-5 - len(s)*2, 5 + len(s) * 2): +# for stop in range(-5 - len(s)*2, 5 + len(s) * 2): +# for element in elements + 'Z': +# try: +# print(element, start, stop) +# target = s.index(element, start, stop) +# except ValueError: +# try: +# d.index(element, start, stop) +# print("X Failed Tests!") +# exit(1) +# except ValueError: +# continue +# # with assertRaises(ValueError): +# # d.index(element, start, stop) +# assertEqual(d.index(element, start, stop), target) + + +# Test large start argument +d = deque(range(0, 10000, 10)) +for step in range(100): + i = d.index(8500, 700) + assertEqual(d[i], 8500) + # Repeat test with a different internal offset + d.rotate() + +###########test_index_bug_24913############# +d = deque('A' * 3) +try: + d.index('A', 1, 0) + print("X Failed Tests!") + exit(1) +except ValueError: + pass + +###########test_insert############# + # Test to make sure insert behaves like lists +elements = 'ABCDEFGHI' +for i in range(-5 - len(elements)*2, 5 + len(elements) * 2): + d = deque('ABCDEFGHI') + s = list('ABCDEFGHI') + d.insert(i, 'Z') + s.insert(i, 'Z') + assertEqual(list(d), s) + + +###########test_insert_bug_26194############# +data = 'ABC' +d = deque(data, maxlen=len(data)) +try: + d.insert(0, 'Z') + print("X Failed Tests!") + exit(1) +except IndexError: + pass + +elements = 'ABCDEFGHI' +for i in range(-len(elements), len(elements)): + d = deque(elements, maxlen=len(elements)+1) + d.insert(i, 'Z') + if i >= 0: + assertEqual(d[i], 'Z') + else: + assertEqual(d[i-1], 'Z') +######### test set_item ############# +n = 200 +d = deque(range(n)) +for i in range(n): + d[i] = 10 * i +assertEqual(list(d), [10*i for i in range(n)]) +l = list(d) +for i in range(1-n, 0, -1): + d[i] = 7*i + l[i] = 7*i +assertEqual(list(d), l) +########## test del_item ############# +n = 500 # O(n**2) test, don't make this too big +d = deque(range(n)) +try: + d.__delitem__(-n-1) + print("X Failed Tests!") + exit(1) +except IndexError: + pass + +try: + d.__delitem__(n) + print("X Failed Tests!") + exit(1) +except IndexError: + pass +for i in range(n): + assertEqual(len(d), n-i) + j = random.randint(0, len(d)-1) + val = d[j] + assertEqual(val in d, True) + del d[j] + assertEqual(val in d, False) +assertEqual(len(d), 0) -print("ALL TEST PASSED!!") \ No newline at end of file +#########test reverse()############### + +n = 500 # O(n**2) test, don't make this too big +data = [random.random() for i in range(n)] +for i in range(n): + d = deque(data[:i]) + r = d.reverse() + assertEqual(list(d), list(reversed(data[:i]))) + assertEqual(r, None) + d.reverse() + assertEqual(list(d), data[:i]) +try: + d.reverse(1) + print("X Failed Tests!") + exit(1) +except TypeError: + pass + +############test rotate############# +s = tuple('abcde') +n = len(s) + +d = deque(s) +d.rotate(1) # verify rot(1) +assertEqual(''.join(d), 'eabcd') + +d = deque(s) +d.rotate(-1) # verify rot(-1) +assertEqual(''.join(d), 'bcdea') +d.rotate() # check default to 1 +assertEqual(tuple(d), s) + +for i in range(n*3): + d = deque(s) + e = deque(d) + d.rotate(i) # check vs. rot(1) n times + for j in range(i): + e.rotate(1) + assertEqual(tuple(d), tuple(e)) + d.rotate(-i) # check that it works in reverse + assertEqual(tuple(d), s) + e.rotate(n-i) # check that it wraps forward + assertEqual(tuple(e), s) + +for i in range(n*3): + d = deque(s) + e = deque(d) + d.rotate(-i) + for j in range(i): + e.rotate(-1) # check vs. rot(-1) n times + assertEqual(tuple(d), tuple(e)) + d.rotate(i) # check that it works in reverse + assertEqual(tuple(d), s) + e.rotate(i-n) # check that it wraps backaround + assertEqual(tuple(e), s) + +d = deque(s) +e = deque(s) +e.rotate(BIG+17) # verify on long series of rotates +dr = d.rotate +for i in range(BIG+17): + dr() +assertEqual(tuple(d), tuple(e)) +try: + d.rotate(1, 2) + print("X Failed Tests!") + exit(1) +except TypeError: + pass + +try: + d.rotate(1,10) + print("X Failed Tests!") + exit(1) +except TypeError: + pass +d = deque() +d.rotate() # rotate an empty deque +assertEqual(d, deque()) + + +##########test len############# + +d = deque('ab') +assertEqual(len(d), 2) +d.popleft() +assertEqual(len(d), 1) +d.pop() +assertEqual(len(d), 0) +try: + d.pop() + print("X Failed Tests!") + exit(1) +except IndexError: + pass +assertEqual(len(d), 0) +d.append('c') +assertEqual(len(d), 1) +d.appendleft('d') +assertEqual(len(d), 2) +d.clear() +assertEqual(len(d), 0) + + +##############test underflow############# +d = deque() +try: + d.pop() + print("X Failed Tests!") + exit(1) +except IndexError: + pass +try: + d.popleft() + print("X Failed Tests!") + exit(1) +except IndexError: + pass + +##############test clear############# +d = deque(range(100)) +assertEqual(len(d), 100) +d.clear() +assertEqual(len(d), 0) +assertEqual(list(d), []) +d.clear() # clear an empty deque +assertEqual(list(d), []) + + +#############test remove############# +d = deque('abcdefghcij') +d.remove('c') +assertEqual(d, deque('abdefghcij')) +d.remove('c') +assertEqual(d, deque('abdefghij')) +try: + d.remove('c') + print("X Failed Tests!") + exit(1) +except ValueError: + pass +assertEqual(d, deque('abdefghij')) + +# Handle comparison errors +d = deque(['a', 'b', BadCmp(), 'c']) +e = deque(d) + +try: + d.remove('c') + print("X Failed Tests!") + exit(1) +except RuntimeError: + pass +for x, y in zip(d, e): + # verify that original order and values are retained. + assertEqual(x is y, True) + +# Handle evil mutator +for match in (True, False): + d = deque(['ab']) + d.extend([MutateCmp(d, match), 'c']) + try: + d.remove('c') + print("X Failed Tests!") + exit(1) + except IndexError: + pass + assertEqual(d, deque()) + +print('✓',"ALL TEST PASSED!!") \ No newline at end of file