diff --git a/README.md b/README.md index 06197e1..c1415c3 100644 --- a/README.md +++ b/README.md @@ -1,372 +1,34 @@ -# Persistent-Data-Structures +# Undo-Redo -Курсовой проект по дисциплине "Современные методы программирования" - "Persistent data structures" - -## Разработчики: -* *Карицкая Полина, 24225.1* -* *Пучков Дмитрий, 24225.1* +Реализация дополнительного требования курсовой работы по дисциплине "Современные методы программирования" - "Persistent data structures" --- ## Оглавление -- [Persistent-Data-Structures](#persistent-data-structures) - - [Разработчики:](#разработчики) - - [Оглавление](#оглавление) - - [Описание задания](#описание-задания) - - [Базовые требования](#базовые-требования) - - [Дополнительные требования](#дополнительные-требования) - - [Календарный план](#календарный-план) - - [Ожидаемое решение](#ожидаемое-решение) - - [Теоретическая часть](#теоретическая-часть) - - [Персистентные структуры данных](#персистентные-структуры-данных) - - [Fat node](#fat-node) - - [Path copying](#path-copying) - - [Более эффективное по скорости доступа представление структур данных](#более-эффективное-по-скорости-доступа-представление-структур-данных) - - [API](#api) - - [Примеры использования](#примеры-использования) - - [Массив (Persistent Array)](#массив-persistent-array) - - [Список (Persistent Linked List)](#список-persistent-linked-list) - - [Aссоциативный массив (Persistent Map)](#aссоциативный-массив-persistent-map) - - [Используемые источники](#используемые-источники) - ---- -## Описание задания - -Реализовать библиотеку в Python со структурами данных в persistent-вариантах. - ---- -## Базовые требования -- [x] Массив (константное время доступа, переменная длина) -- [x] Двусвязный список -- [x] Ассоциативный массив (на основе Hash-таблицы, либо бинарного дерева) - ---- -## Дополнительные требования -- [x] Реализовать более эффективное по памяти представление структур данных; -- [x] Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности (для вложенных структур); -- [x] Реализовать поддержку транзакционной памяти (STM). - ---- -## Календарный план - -| **Сроки** | **Этап работы** | **Разделение ответственностей** | -|--------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| -| **до 23.11.2024** | - Создание каркаса проекта
- Создание репозитория на GitHub | - Ответственный за настройку проекта и репозитория: *Полина*
- Ответственный за организацию структуры файлов и начальную настройку CI/CD: *Дмитрий* | -| **до 07.12.2024** | **Реализация базовой функциональности:**
- Массив (Persistent Array)
- Двусвязный список (Persistent Linked List)
- Ассоциативный массив (Persistent Map)
- Создание единого API | - Ответственный за реализацию Persistent Array: *совместно*
- Ответственный за реализацию Persistent Linked List: *Полина*
- Ответственный за реализацию Persistent Map: *Дмитрий*
- Ответственный за создание API: *совместно* | -| **до 21.12.2024** | **Реализация дополнительной функциональности:**
- Реализовать более эффективное по памяти представление структур данных
- Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности (для вложенных структур)
- Реализовать поддержку транзакционной памяти (STM) | - Ответственный за улучшение эффективности по памяти: *Полина*
- Ответственный за реализацию undo-redo механизма: *Дмитрий*
- Ответственный за поддержку транзакционной памяти (STM): *совместно* | +- [Механизм Undo-Redo](#механизм-undo-redo) +- [Механизм Undo-Redo с использованием персистентных структур данных](#механизм-undo-redo-с-использованием-персистентных-структур-данных) --- -## Ожидаемое решение - -Ожидаемое решение состоит в разработке библиотеки, которая будет поддерживать следующие структуры данных: - -1. **Persistent Array** - массив с возможностью добавления элементов без изменения предыдущих состояний. Операции должны поддерживать константное время доступа. - -2. **Persistent Linked List** - двусвязный список с поддержкой добавления и удаления элементов, при этом сохраняется возможность обратиться к предыдущим состояниям списка. - -3. **Persistent Map** - ассоциативный массив, реализованный на основе хеш-таблицы или бинарного дерева поиска. Это будет позволять эффективно работать с данными, при этом поддерживать возможность обращения к предыдущим версиям данных. - -4. **Оптимизация по памяти** - предложим более эффективное по памяти представление данных операций доступа по сравнению с fat-node. +## Механизм Undo-Redo -5. **Undo/Redo Механизм** - реализуем механизм для всех структур данных, который позволит отменять и повторять изменения, причем для вложенных структур будет поддерживаться каскадность. - -6. **Транзакционная память (STM)** - реализуем механизм транзакционной памяти для обеспечения атомарности операций с данными. - - -Каждая из структур данных будет поддерживать все основные операции: вставку, удаление, обновление, а также предоставлять возможность работы с предыдущими версиями данных через персистентность данных. Вся библиотека будет иметь единый API для работы с данными и их модификацией. Также будет обеспечена возможность взаимодействия с данными в виде вложенных структур. +**Механизм undo-redo** — механизм, позволяющий пользователям отменять или повторно применять изменения. Он обеспечивает гибкость и возможность восстановления ошибок, что особенно важно для задач, требующих частого редактирования. В ветке рассматриваются реализация механизма undo-redo с использованием персистентных структур данных. --- -## Теоретическая часть - -### Персистентные структуры данных - -**Персистентные структуры данных** сохраняют предыдущие версии при изменении. Структура называется *fully persistent*, если все её версии доступны для изменений. В *partially persistent* структурах можно изменять только последнюю версию, но доступ к предыдущим возможен. Эти структуры часто реализуются с использованием алгоритмов, таких как path copying, node copying и fat node, а также с применением бинарных деревьев поиска и красно-черных деревьев. - -Персистентные структуры используются в вычислительной геометрии, объектно-ориентированном программировании, текстовых редакторах, симуляциях и системах контроля версий, таких как Git. - -### Fat node - -*Fat node* используется для создания персистентных структур данных, где изменения сохраняются только в измененных узлах дерева, а сами узлы могут расширяться, чтобы хранить все версии. Основной принцип заключается в том, чтобы хранить обновления и версии данных в списках, что позволяет эффективно отслеживать изменения в структуре. - -**Преимущества:** - -* Экономия памяти, так как изменяются только те части данных, которые действительно изменились; -* Быстрая работа с историей изменений. - -**Недостатки:** - -* Может быть менее эффективен для структур с частыми изменениями, так как увеличение размера узлов приводит к дополнительным затратам на память. - -Можно посмотреть [визуализацию метода *fat node*](https://kumom.io/persistent-bst). - -### Path copying - -Метод *path copying* используется для создания персистентных структур данных, где при изменении узла создается его копия, и нужно пройти по всем узлам от измененного до корня, чтобы обновить ссылки на новый узел. Этот метод упрощает доступ к данным, так как для поиска нужной версии достаточно пройти путь от корня. - -**Преимущества:** - -* Быстрый доступ к данным; -* Простота в реализации. - -**Недостатки:** - -* Большие затраты на память, поскольку изменения могут потребовать копирования всей структуры; -* Могут возникать дополнительные накладные расходы при частых модификациях. - -Метод *path copying* также используется для создания полностью персистентных структур данных. - -На иллюстрации продеминстрирован пример использования *path copying* на бинарном дереве поиска. При внесении изменения создается новый корень, при этом старый корень сохраняется для дальнейшего использования (он показан темно-серым цветом). Заметим также, что старое и новое деревья частично делят общую структуру. - -![Пример использования path copying](path_copying.png) - -### Более эффективное по памяти представление структур данных - -Поскольку одно из требований — использование более эффективного подхода по сравнению с методом *fat-node*, в проекте будет применяться подход с использованием B-деревьев. +## Механизм Undo-Redo с использованием персистентных структур данных ---- -## API - -Для работы с персистентными структурами данных используйте следующий API: - -Создание объекта: -```python -from persistent_data_structure import PersistentLinkedList, PersistentArray, PersistentMap - -lst = PersistentLinkedList() -arr = PersistentLinkedList() -dct = PersistentMap() -``` - -Обращение к элементу текщей версии: -```python -lst[index] -arr[index] -dct[key] -``` - -Обращение по индексу к элементу произвольной версии для массива и списка: -```python -arr.get(version, index) -lst.get(version, index) -``` - -Обращение по ключу к элементу произвольной версии для мапы: -```python -dct.get(version, key) -``` - -Добавление элемента в конец в новую версию для массива и списка: -```python -arr.add(element) -lst.add(element) -``` - -Добавление элемента в начало в новую версию списка: -```python -lst.add_first(element) -``` - -Удаление элемента по индексу для массива и списка в новой версии и возвращение элемента: -```python -arr.pop(index) -lst.pop(index) -``` - -Удаление элемента по ключу для мапы в новой версии и возвращение элемента: -```python -dct.pop(key) -``` - -Обновление элемента по индексу в новой версии массива или списка: -```python -arr[index] = element -lst[index] = element -``` - -Обновление элемента по ключу в новой версии мапы: -```python -dct['key'] = element -``` - -Вставка элемента в новую версию по указанному индексу для массива или списка: -```python -arr.insert(index, element) -lst.insert(index, element) -``` - -Удаление элемента в новой версии массива или списка по индексу: -```python -arr.remove(index) -lst.remove(index) -``` - -Удаление элемента в новой версии мапы по ключу: -```python -dct.remove(key) -``` - -Получение размера массива или списка для текущей версии: -```python -arr.get_size() -lst.get_size() -``` - -Проверка на пустоту для массива или списка: -```python -arr.check_is_empty() -lst.check_is_empty() -``` - -Возвращение состояние объекта для указанной версии: -```python -arr.get_version(version) -lst.get_version(version) -dct.get_version(version) -``` - -Обновление текущей версии объекта до указанной: -```python -arr.update_version(version) -lst.update_version(version) -dct.update_version(version) -``` - -Получение элемента текущей версии массива или списка по указанному индексу. -```python -arr.__getitem__(index) -lst.__getitem__(index) -``` - -Получение элемента текущей версии мапы по указанному ключу. -```python -dct.__getitem__(key) -``` - -Обновление или создание элемента по указанному индексу в новой версии для массива или списка. -```python -arr.__setitem__(index) -lst.__setitem__(index) -``` - -Обновление или создание элемента по указанному ключу в новой версии для мапы. -```python -dct.__setitem__(key) -``` - -Очистка объекта, при этом создается новая версия. -```python -arr.clear() -lst.clear() -dct.clear() -``` - ---- -## Примеры использования - -### Массив (Persistent Array) - -**Примеры использования:** - -``` - >>> array = PersistentArray(size=5, default_value=0) # Создаем массив из 5 элементов, заполненных нулями - >>> array[0] # Получаем значение первого элемента - 0 - >>> array[0] = 10 # Обновляем значение первого элемента в новой версии - >>> array[0] # Проверяем значение первого элемента в текущей версии - 10 - >>> array.add(20) # Добавляем новый элемент в конец массива в новую версию - >>> array[5] # Проверяем значение нового элемента - 20 - >>> array.pop(1) # Удаляем элемент с индексом 1 в новой версии - 0 - >>> array.insert(2, 15) # Вставляем значение 15 на индекс 2 в новой версии - >>> array[2] # Проверяем вставленное значение - 15 - >>> array.remove(3) # Удаляем элемент с индексом 3 - >>> array.get(1, 0) # Получаем значение элемента с индексом 0 из версии 1 - 0 - >>> array.clear() # Очищаем массив, создавая новую версию - >>> array.get_size() # Проверяем размер массива в текущей версии - 0 - >>> array.check_is_empty() # Проверяем, пуст ли массив - True - >>> array.get(0, 2) # Пытаемся получить элемент из очищенного массива в версии 0 - Traceback (most recent call last): - ... - ValueError: Invalid index -``` - -### Список (Persistent Linked List) - -**Примеры использования:** - -``` - >>> linked_list = PersistentLinkedList([1, 2, 3]) # Создаем список с начальными элементами - >>> linked_list.add(4) # Добавляем элемент в конец списка - >>> linked_list[3] # Получаем элемент по индексу в текущей версии - 4 - >>> linked_list.add_first(0) # Добавляем элемент в начало списка - >>> linked_list[0] # Получаем элемент в текущей версии по индексу - 0 - >>> linked_list.insert(2, 1.5) # Вставляем элемент на индекс 2 - >>> linked_list[2] # Проверяем значение вставленного элемента - 1.5 - >>> linked_list.get(1, 2) # Получаем элемент из версии 1 по индексу 2 - 3 - >>> linked_list.pop(1) # Удаляем элемент по индексу 1 в новой версии - 2 - >>> linked_list.remove(3) # Удаляем элемент - >>> linked_list.clear() # Очищаем список, создавая новую версию - >>> linked_list.get(0, 2) # Пытаемся получить элемент из очищенной версии - Traceback (most recent call last): - ... - IndexError: Index out of range - >>> linked_list.update_version(2) # Возвращаемся к версии 2 - >>> linked_list[0] # Получаем элемент по индексу в версии 2 - 1 - >>> linked_list.get_size() # Получаем размер списка в текущей версии - 4 - >>> linked_list.check_is_empty() # Проверяем, пуст ли список в текущей версии - False -``` - -### Aссоциативный массив (Persistent Map) - -**Примеры использования:** - -``` - >>> map = PersistentMap({'foo': 'bar'}) # Создаем пустой ассоциативный массив - >>> map['key'] = 'value' # Добавляем элемент - >>> map['key'] = 'value2' # Изменяем элемент в следующей версии - >>> map['key'] # Получаем элемент последней версии - 'value2' - >>> map.get(1, 'key') - {'key': 'value'} - >>> map.remove('key') # Удаляем элемент в новой версии - >>> map.clear() # Очищаем ассоциативный массив в новой версии - >>> map.get(0, 'key') # Пытаемся получить элемент отсутствующий в переданной версии - Traceback (most recent call last): - ... - KeyError: Key "key" does not exist -``` - ---- -## Используемые источники +Функциональность undo-redo опирается на следующие основные принципы персистентных структур данных: -1. Milan Straka, "Functional Data Structures and Algorithms", Computer Science Institute of Charles University, Prague 2013 +1. **Ведение истории версий:** -2. Крис Окасаки, "Чисто функциональные структуры данных", 2018 + * Изменения в структуре данных записываются как новые версии; -3. Статья на Geeks for geeks ["Persistent data structures"](https://www.geeksforgeeks.org/persistent-data-structures/) + * История версий хранится в виде словаря, где ключами являются номера версий, а значениями — соответствующие состояния. -4. Несколько статей по персистентным векторам: +2. **Восстановление состояния:** - * [Understanding Clojure's Persistent Vectors, pt. 1](https://hypirion.com/musings/understanding-persistent-vector-pt-1) - * [Understanding Clojure's Persistent Vectors, pt. 2](https://hypirion.com/musings/understanding-persistent-vector-pt-2) - * [Understanding Clojure's Persistent Vectors, pt. 3](https://hypirion.com/musings/understanding-persistent-vector-pt-3) + * Операция undo уменьшает индекс текущей версии, возвращая предыдущее состояние; -5. Driscoll J. R. et al. Making data structures persistent //Proceedings of the eighteenth annual ACM symposium on Theory of computing. – 1986. – С. 109-121. + * Операция redo увеличивает индекс версии, повторяя ранее отменённое состояние. -6. Серия видео-лекций по персистентным структурам данных: +3. **Нерушимость изменений:** - * ["Visualizing Persistent Data Structures" by Dann Toliver](https://www.youtube.com/watch?v=2XH_q494U3U) - * [Persistent Data Structures, MIT](https://www.youtube.com/watch?v=T0yzrZL1py0) \ No newline at end of file + * Модификации не перезаписывают существующие данные, а создают новую версию с обновлённым состоянием. \ No newline at end of file diff --git a/persistent_data_structures/base_persistent.py b/persistent_data_structures/base_persistent.py index a4b967b..2cd4f4f 100644 --- a/persistent_data_structures/base_persistent.py +++ b/persistent_data_structures/base_persistent.py @@ -15,6 +15,8 @@ def __init__(self, initial_state=None) -> None: self._history = {0: initial_state} self._current_state = 0 self._last_state = 0 + self._container = None + self._location = None def get_version(self, version): """Возвращает состояние персистентной структуры данных на указанной версии. @@ -27,6 +29,22 @@ def get_version(self, version): raise ValueError(f'Version "{version}" does not exist') return self._history[version] + def getcopy(self, version: int, key: any) -> any: + """Возвращает копию элемента с указанной версией и ключом/индексом. + + :param version: Номер версии + :param key: Ключ/индекс + :return: Копия значения сответствующее указанному ключу или None, + если ключ/индекс не существует. + :raises ValueError: Если версия не существует + :raises KeyError: Если ключ/индекс не существует + """ + value = deepcopy(self.get(version, key)) + if isinstance(value, BasePersistent): + value._container = None + value._location = None + return value + def update_version(self, version): """Обновляет текущую версию персистентной структуры данных до указанной. @@ -37,8 +55,38 @@ def update_version(self, version): raise ValueError(f'Version "{version}" does not exist') self._current_state = version + def undo(self): + """Отменяет последнее изменение.""" + if self._container is not None: + raise NotImplementedError(f'Cannot undo inside container "{self._container}"') + if self._current_state == 0: + raise ValueError("No actions to undo") + if self._current_state > 0: + self._current_state -= 1 + + def redo(self): + """Повторяет последнее отмененное изменение. + + :raises ValueError: Если нет операций.""" + if self._container is not None: + raise NotImplementedError(f'Cannot redo inside container "{self._container}"') + if self._current_state >= self._last_state: + raise ValueError("No operations to redo") + self._current_state += 1 + def _create_new_state(self) -> None: """Создает новую версию.""" + # Персистентные структуры могут быть вложенными, + # поэтому нужно сохранять историю изменений в родительской персистентной структуре + if self._container is not None: + # В текущей версии родительской структуры подменяем имеющуюся вложенную структуру + # на ее копию, таким образом изменения во вложенной структуре не будут отражаться на + # прощлых версиях родительской структуры + self._container._history[ + self._container._current_state + ][self._location] = deepcopy(self) self._last_state += 1 self._history[self._last_state] = deepcopy(self._history[self._current_state]) self._current_state = self._last_state + if self._container is not None: + self._container[self._location] = self diff --git a/persistent_data_structures/persistent_array.py b/persistent_data_structures/persistent_array.py index cee913f..3c19d96 100644 --- a/persistent_data_structures/persistent_array.py +++ b/persistent_data_structures/persistent_array.py @@ -1,6 +1,6 @@ import numpy as np -from persistent_data_structures.base_persistent import BasePersistent +from base_persistent import BasePersistent class PersistentArray(BasePersistent): @@ -54,6 +54,9 @@ def add(self, value: any) -> None: :param value (int): Значение нового элемента, который добавляется в массив. """ self._create_new_state() + if isinstance(value, BasePersistent): + value._container = self + value._location = self.size self._history[self._last_state] = np.append(self._history[self._last_state], value) self.size += 1 @@ -70,6 +73,10 @@ def pop(self, index: int) -> any: self._create_new_state() self._history[self._last_state] = np.delete(self._history[self._last_state], index) self.size -= 1 + # Сдвигаем индексы всех элементов BasePersistent после удаленного элемента + for i in range(index, self.size): + if isinstance(self._history[self._current_state][i], BasePersistent): + self._history[self._current_state][i]._location -= 1 return removed_element def __setitem__(self, index: int, value: any) -> None: @@ -83,6 +90,9 @@ def __setitem__(self, index: int, value: any) -> None: """ if index < 0 or index >= self.size: raise ValueError("Invalid index") + if isinstance(value, BasePersistent): + value._container = self + value._location = index self._create_new_state() self._history[self._last_state][index] = value @@ -97,9 +107,16 @@ def insert(self, index: int, value: any) -> None: """ if index < 0 or index > self.size: raise ValueError("Invalid index") + if isinstance(value, BasePersistent): + value._container = self + value._location = index self._create_new_state() self._history[self._last_state] = np.insert(self._history[self._last_state], index, value) self.size += 1 + # Сдвигаем индексы всех элементов BasePersistent после добавленного элемента + for i in range(index+1, self.size): + if isinstance(self._history[self._current_state][i], BasePersistent): + self._history[self._current_state][i]._location += 1 def remove(self, index: int) -> None: """Удаление элемента в новой версии массива по индексу. diff --git a/persistent_data_structures/persistent_list.py b/persistent_data_structures/persistent_list.py index 75b4fa0..58f043b 100644 --- a/persistent_data_structures/persistent_list.py +++ b/persistent_data_structures/persistent_list.py @@ -1,4 +1,6 @@ -from persistent_data_structures.base_persistent import BasePersistent +from copy import deepcopy + +from base_persistent import BasePersistent class Node: @@ -57,6 +59,9 @@ def add(self, data: any) -> None: """ self._create_new_state() head, tail = self._history[self._last_state] + if isinstance(data, BasePersistent): + data._container = self + data._location = self.size new_node = Node(data) if tail is None: head = tail = new_node @@ -76,6 +81,9 @@ def add_first(self, data: any) -> None: """ self._create_new_state() head, tail = self._history[self._last_state] + if isinstance(data, BasePersistent): + data._container = self + data._location = 0 new_node = Node(data, next_node=head) if head: head.prev = new_node @@ -83,6 +91,13 @@ def add_first(self, data: any) -> None: if tail is None: tail = new_node self.size += 1 + # сдвигаем индексы персистентных элементов + current = head + while current.next_node: + current = current.next_node + if isinstance(current.value, BasePersistent): + current.value._location += 1 + self._history[self._last_state] = (head, tail) def insert(self, index: int, data: any) -> None: @@ -94,8 +109,15 @@ def insert(self, index: int, data: any) -> None: :return: None :raises IndexError: Если индекс выходит за пределы списка. """ + if index < 0 or index > self.size: + raise IndexError("Index out of range") + self._create_new_state() head, tail = self._history[self._last_state] + if isinstance(data, BasePersistent): + data._container = self + data._location = index + current = head count = 0 while current: @@ -107,11 +129,12 @@ def insert(self, index: int, data: any) -> None: if current == head: head = new_node self.size += 1 - break + count += 1 + + if isinstance(current.value, BasePersistent) and count > index: + current.value._location += 1 count += 1 current = current.next_node - else: - raise IndexError("Index out of range") self._history[self._last_state] = (head, tail) def pop(self, index: int) -> any: @@ -122,12 +145,15 @@ def pop(self, index: int) -> any: :return: Значение удаленного элемента. :raises IndexError: Если индекс выходит за пределы списка. """ - head, tail = self._history[self._current_state] + if index < 0 or index >= self.size: + raise IndexError("Index out of range") + self._create_new_state() + head, tail = self._history[self._last_state] + popped_value = self.__getitem__(index) current = head count = 0 while current: if count == index: - value = current.value if current.prev: current.prev.next_node = current.next_node if current.next_node: @@ -136,40 +162,25 @@ def pop(self, index: int) -> any: head = current.next_node if current == tail: tail = current.prev - self._create_new_state() + self.size -= 1 self._history[self._last_state] = (head, tail) - return value + + if isinstance(current.value, BasePersistent) and count > index: + current.value._location -= 1 count += 1 current = current.next_node - raise IndexError("Index out of range") + return popped_value - def remove(self, value: any) -> None: + def remove(self, index: int) -> None: """ - Удаляет элемент из списка в новой версии. + Удаляет элемент по индексу из списка в новой версии. - :param data: Данные элемента для удаления. + :param index: Индекс элемента для удаления. :return: None - :raises ValueError: Если элемент не найден в списке. + :raises IndexError: Если индекс выходит за пределы списка. """ - head, tail = self._history[self._current_state] - current = head - while current: - if current.value == value: - if current.prev: - current.prev.next_node = current.next_node - if current.next_node: - current.next_node.prev = current.prev - if current == head: - head = current.next_node - if current == tail: - tail = current.prev - self._create_new_state() - self.size -= 1 - self._history[self._last_state] = (head, tail) - return - current = current.next_node - raise ValueError(f"Value {value} not found in the list") + self.pop(index) def get(self, version: int = None, index: int = None) -> any: """ @@ -234,6 +245,9 @@ def __setitem__(self, index: int, value: any) -> None: :raises IndexError: Если индекс выходит за пределы списка. """ self._create_new_state() + if isinstance(value, BasePersistent): + value._container = self + value._location = index head, tail = self._history[self._last_state] current = head count = 0 @@ -255,6 +269,20 @@ def get_size(self) -> int: """ return self.size + def calc_size(self) -> int: + """ + Расчет текущего размера списка. + + :return: Количество элементов в текущей версии списка. + """ + size = 0 + head, tail = self._history[self._current_state] + current = head + while current: + size += 1 + current = current.next_node + return size + def check_is_empty(self) -> bool: """ Проверяет, пуст ли список. @@ -263,3 +291,56 @@ def check_is_empty(self) -> bool: """ head, tail = self._history[self._current_state] return head is None + + def update_version(self, version) -> None: + """Обновляет текущую версию персистентной структуры данных до указанной. + + :param version: Номер версии. + :raises ValueError: Если указанная версия не существует. + """ + if version < 0 or version >= len(self._history): + raise ValueError(f'Version "{version}" does not exist') + self._current_state = version + self.size = self.calc_size() + + def undo(self) -> None: + """Отменяет последнее изменение.""" + if self._container is not None: + raise NotImplementedError(f'Cannot undo inside container "{self._container}"') + if self._current_state == 0: + raise ValueError("No actions to undo") + if self._current_state > 0: + self._current_state -= 1 + self.size = self.calc_size() + + def redo(self) -> None: + """Повторяет последнее отмененное изменение. + + :raises ValueError: Если нет операций.""" + if self._container is not None: + raise NotImplementedError(f'Cannot redo inside container "{self._container}"') + if self._current_state >= self._last_state: + raise ValueError("No operations to redo") + self._current_state += 1 + self.size = self.calc_size() + + def _create_new_state(self) -> None: + """Создает новую версию.""" + # Персистентные структуры могут быть вложенными, + # поэтому нужно сохранять историю изменений в родительской персистентной структуре + if self._container is not None: + # В текущей версии родительской структуры подменяем имеющуюся вложенную структуру + # на ее копию, таким образом изменения во вложенной структуре не будут отражаться на + # прощлых версиях родительской структуры + head, tail = self._container._history[self._container._current_state] + current = head + while current: + if current.value is self: + current.value = deepcopy(self) + break + current = current.next_node + self._last_state += 1 + self._history[self._last_state] = deepcopy(self._history[self._current_state]) + self._current_state = self._last_state + if self._container is not None: + self._container[self._location] = self diff --git a/persistent_data_structures/persistent_map.py b/persistent_data_structures/persistent_map.py index 9efb825..f469c57 100644 --- a/persistent_data_structures/persistent_map.py +++ b/persistent_data_structures/persistent_map.py @@ -1,4 +1,4 @@ -from persistent_data_structures.base_persistent import BasePersistent +from base_persistent import BasePersistent class PersistentMap(BasePersistent): @@ -20,6 +20,9 @@ def __setitem__(self, key: any, value: any) -> None: :param value: Значение """ self._create_new_state() + if isinstance(value, BasePersistent): + value._container = self + value._location = key self._history[self._last_state][key] = value def __getitem__(self, key: any) -> any: @@ -42,7 +45,7 @@ def get(self, version: int, key: any) -> any: raise ValueError(f'Version "{version}" does not exist') if key not in self._history[version]: raise KeyError(f'Key "{key}" does not exist') - return self._history[version] + return self._history[version][key] def pop(self, key: any) -> any: """Удаляет элемент по указанному ключу и возвращает его. @@ -50,8 +53,11 @@ def pop(self, key: any) -> any: :param key: Ключ :return: Удаленный элемент """ + if key not in self._history[self._current_state]: + raise KeyError(f'Key "{key}" does not exist') self._create_new_state() - return self._history[self._last_state].pop(key) + popped_item = self._history[self._last_state].pop(key) + return popped_item def remove(self, key: any) -> None: """Удаляет элемент по указанному ключу в новой версии. @@ -63,4 +69,4 @@ def remove(self, key: any) -> None: def clear(self) -> None: """Очищает ассоциативный массив в новой версии.""" self._create_new_state() - self._history[self._current_state] = {} + self._history[self._last_state] = {} diff --git a/tests/test_array.py b/tests/test_array.py index 00b2cca..5a89858 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -1,109 +1,193 @@ import pytest - from persistent_array import PersistentArray -# Тестирование методов класса PersistentArray @pytest.fixture def persistent_array(): - """Фикстура для создания PersistentArray""" + """Фикстура для создания персистентного массива с размером 5 и значением по умолчанию 0.""" return PersistentArray(size=5, default_value=0) def test_initial_state(persistent_array): - """Тест1. Проверка начального состояния массива""" + """Тест 1. Проверка начального состояния массива""" assert persistent_array.get_size() == 5 - assert persistent_array[0] == 0 - assert persistent_array[4] == 0 + for i in range(5): + assert persistent_array[i] == 0 -def test_get_version(persistent_array): - """Тест 2. Проверка получения состояния на определенной версии""" - persistent_array.add(1) - persistent_array.add(2) +def test_get_item(persistent_array): + """Тест 2. Проверка метода получения элемента массива""" + persistent_array[1] = 10 + assert persistent_array[1] == 10 - assert persistent_array.get(0, 0) == 0 - assert persistent_array.get(1, 5) == 1 - assert persistent_array.get(2, 6) == 2 +def test_set_item(persistent_array): + """Тест 3. Проверка метода установки элемента массива""" + persistent_array[2] = 20 + assert persistent_array[2] == 20 + assert persistent_array.get_version(0)[2] == 0 -def test_update_version(persistent_array): - """Тест 3. Проверка обновления текущей версии""" - persistent_array.add(1) - persistent_array.add(2) - persistent_array.update_version(1) - assert persistent_array[5] == 1 - persistent_array.update_version(2) - assert persistent_array[6] == 2 +def test_add(persistent_array): + """Тест 4. Проверка добавления нового элемента в массив""" + persistent_array.add(42) + assert persistent_array.get_size() == 6 + assert persistent_array[5] == 42 -def test_add_element(persistent_array): - """Тест 4. Проверка добавления элемента в массив""" - persistent_array.add(10) +def test_pop(persistent_array): + """Тест 5. Проверка удаления элемента из массива""" + persistent_array[2] = 30 + removed = persistent_array.pop(2) + assert removed == 30 + assert persistent_array.get_size() == 4 + + +def test_insert(persistent_array): + """Тест 6. Проверка вставки элемента в массив по индексу""" + persistent_array.insert(2, 50) + assert persistent_array[2] == 50 assert persistent_array.get_size() == 6 - assert persistent_array[5] == 10 -def test_pop_element(persistent_array): - """Тест 5. Проверка удаления элемента из массива""" +def test_remove(persistent_array): + """Тест 7. Проверка удаления элемента массива по индексу""" + persistent_array.remove(3) + assert persistent_array.get_size() == 4 + + +def test_undo_redo(persistent_array): + """Тест 8. Проверка функционала отмены и повторения изменений""" + persistent_array[1] = 10 + persistent_array.undo() + assert persistent_array[1] == 0 + persistent_array.redo() + assert persistent_array[1] == 10 + + +def test_check_is_empty(): + """Тест 9. Проверка метода проверки пустоты массива""" + array = PersistentArray(size=0) + assert array.check_is_empty() + array.add(5) + assert not array.check_is_empty() + + +def test_get_version(persistent_array): + """Тест 10. Проверка получения версии массива""" + persistent_array[1] = 10 + assert persistent_array.get_version(0)[1] == 0 + assert persistent_array.get_version(1)[1] == 10 + with pytest.raises(ValueError): + persistent_array.get_version(3) + + +def test_update_version(persistent_array): + """Тест 11. Проверка метода обновления версии массива""" + persistent_array[1] = 10 + persistent_array.update_version(1) + assert persistent_array[1] == 10 + with pytest.raises(ValueError): + persistent_array.update_version(2) + + +def test_undo(persistent_array): + """Тест 12. Проверка отмены изменений (undo)""" persistent_array.add(10) persistent_array.add(20) - removed_value = persistent_array.pop(1) - assert removed_value == 0 - assert persistent_array.get_size() == 6 - assert persistent_array[1] == 0 + persistent_array.add(30) + state_after_add = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_add == [0, 0, 0, 0, 0, 10, 20, 30] + persistent_array.undo() + state_after_undo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_undo == [0, 0, 0, 0, 0, 10, 20] + persistent_array.undo() + state_after_second_undo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_second_undo == [0, 0, 0, 0, 0, 10] -def test_insert_element(persistent_array): - """Тест 6. Проверка вставки элемента в массив по индексу""" +def test_redo(persistent_array): + """Тест 13. Проверка возврата изменений после undo (redo)""" persistent_array.add(10) - persistent_array.insert(2, 15) - assert persistent_array.get_size() == 7 - assert persistent_array[2] == 15 + persistent_array.add(20) + persistent_array.add(30) + persistent_array.undo() + persistent_array.undo() + state_after_undo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_undo == [0, 0, 0, 0, 0, 10] + persistent_array.redo() + state_after_redo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_redo == [0, 0, 0, 0, 0, 10, 20] + persistent_array.redo() + state_after_second_redo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_second_redo == [0, 0, 0, 0, 0, 10, 20, 30] -def test_remove_element(persistent_array): - """Тест 7. Проверка удаления элемента по индексу""" +def test_undo_redo_integrity(persistent_array): + """Тест 14. Проверка целостности данных при использовании undo и redo""" persistent_array.add(10) persistent_array.add(20) - persistent_array.remove(0) - assert persistent_array.get_size() == 6 - assert persistent_array[0] == 0 + state_after_add = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_add == [0, 0, 0, 0, 0, 10, 20] + persistent_array.undo() + state_after_undo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_undo == [0, 0, 0, 0, 0, 10] + persistent_array.redo() + state_after_redo = persistent_array._history[persistent_array._current_state].tolist() + assert state_after_redo == [0, 0, 0, 0, 0, 10, 20] -def test_set_item(persistent_array): - """Тест 8. Проверка обновления элемента в массиве по индексу""" - persistent_array[0] = 99 - assert persistent_array[0] == 99 +def test_remove_nested_persistent_array(persistent_array): + """Тест 15. Проверка удаления вложенной структуры из массива""" + nested_array = PersistentArray(size=3, default_value=5) + persistent_array.add(nested_array) + persistent_array.remove(5) assert persistent_array.get_size() == 5 + with pytest.raises(ValueError): + persistent_array[5] + +def test_nested_persistent_array_get(persistent_array): + """Тест 16. Проверка получения элемента вложенной структуры""" + nested_array = PersistentArray(size=3, default_value=5) + persistent_array.add(nested_array) + assert persistent_array[5][1] == 5 -def test_check_is_empty(persistent_array): - """Тест 9. Проверка, является ли массив пустым""" - assert not persistent_array.check_is_empty() - persistent_array.remove(0) - persistent_array.remove(0) - persistent_array.remove(0) - persistent_array.remove(0) - persistent_array.remove(0) - assert persistent_array.check_is_empty() + +def test_size_after_pop(persistent_array): + """Тест 17. Проверка размера после удаления элемента""" + persistent_array.add(10) + persistent_array.add(20) + assert persistent_array.get_size() == 7 + persistent_array.pop(1) + assert persistent_array.get_size() == 6 + + +def test_multiple_versions(persistent_array): + """Тест 18. Проверка сохранения нескольких версий""" + persistent_array.add(10) + persistent_array.add(20) + persistent_array.add(30) + assert persistent_array.get(0, 0) == 0 + assert persistent_array.get(1, 5) == 10 + assert persistent_array.get(2, 6) == 20 + assert persistent_array.get(3, 7) == 30 def test_invalid_index_get(persistent_array): - """Тест 10. Проверка на исключение для недопустимого индекса при получении""" + """Тест 20. Проверка на исключение для недопустимого индекса при получении""" with pytest.raises(ValueError): persistent_array[10] def test_invalid_index_set(persistent_array): - """Тест 11. Проверка на исключение для недопустимого индекса при обновлении""" + """Тест 21. Проверка на исключение для недопустимого индекса при обновлении""" with pytest.raises(ValueError): persistent_array[10] = 5 def test_invalid_version_get(persistent_array): - """Тест 12. Проверка на исключение для недопустимой версии при получении""" + """Тест 22. Проверка на исключение для недопустимой версии при получении""" persistent_array.add(1) persistent_array.add(2) with pytest.raises(ValueError): @@ -111,8 +195,87 @@ def test_invalid_version_get(persistent_array): def test_invalid_version_update(persistent_array): - """Тест 13. Проверка на исключение для недопустимой версии при обновлении""" + """Тест 23. Проверка на исключение для недопустимой версии при обновлении""" persistent_array.add(1) persistent_array.add(2) with pytest.raises(ValueError): persistent_array.update_version(10) + + +def test_nested_array(persistent_array): + """Тест 24. Проверка вложенности персистентных массивов""" + nested_array = PersistentArray(size=3, default_value=1) + persistent_array.add(nested_array) + assert persistent_array.get_size() == 6 + assert persistent_array[5] == nested_array + assert persistent_array[5][1] == 1 + + +def test_nested_array_undo_redo(persistent_array): + """Тест 25. Проверка отмены и повторения изменений во вложенных массивах""" + nested_array = PersistentArray(size=3, default_value=1) + persistent_array.add(nested_array) + persistent_array[5][0] = 42 + persistent_array.undo() + assert persistent_array[5][0] == 1 + persistent_array.redo() + assert persistent_array[5][0] == 42 + + +def test_nested_array_deepcopy(persistent_array): + """Тест 26. Проверка правильности работы deepcopy для вложенных массивов""" + nested_array = PersistentArray(size=3, default_value=1) + persistent_array.add(nested_array) + persistent_array[5][0] = 42 + nested_array_copy = persistent_array.get_version(1)[5] + assert nested_array_copy[0] == 1 + assert persistent_array[5][0] == 42 + + +def test_remove_nested_array(persistent_array): + """Тест 27. Проверка удаления вложенного массива""" + nested_array = PersistentArray(size=3, default_value=1) + persistent_array.add(nested_array) + persistent_array.remove(5) + assert persistent_array.get_size() == 5 + with pytest.raises(ValueError): + persistent_array[5] + + +def test_undo_redo_with_nested(persistent_array): + """Тест 28. Проверка функционала отмены и повторения изменений с вложенной структурой""" + nested_array = PersistentArray(size=3, default_value=1) + persistent_array.add(nested_array) + persistent_array[5][1] = 20 + persistent_array.undo() + assert persistent_array[5][1] == 1 + persistent_array.redo() + assert persistent_array[5][1] == 20 + + +def test_triple_nested_persistent_array(persistent_array): + """Тест 29. Проверка удаления вложенной структуры из массива (тройная вложенность)""" + nested_array_1 = PersistentArray(size=3, default_value=1) + nested_array_1[1] = 42 + nested_array_2 = PersistentArray(size=2, default_value=5) + nested_array_2[0] = 99 + nested_array_3 = PersistentArray(size=1, default_value=7) + nested_array_3[0] = 77 + persistent_array.add(nested_array_1) + persistent_array.add(nested_array_2) + persistent_array.add(nested_array_3) + assert persistent_array.get_size() == 8 + assert isinstance(persistent_array[5], PersistentArray) + assert isinstance(persistent_array[6], PersistentArray) + assert isinstance(persistent_array[7], PersistentArray) + removed_element = persistent_array.pop(7) + assert removed_element == nested_array_3 + assert persistent_array.get_size() == 7 + assert persistent_array[5][0] == 1 + assert persistent_array[5][1] == 42 + assert persistent_array[6][0] == 99 + persistent_array.undo() + assert persistent_array.get_size() == 7 + persistent_array[6] = 88 + persistent_array.undo() + assert persistent_array[6][0] == 99 diff --git a/tests/test_list.py b/tests/test_list.py index 75f6742..2723d8a 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -5,93 +5,178 @@ # Тестирование методов класса PersistentLinkedList @pytest.fixture -def linked_list(): - """ - Фикстура для создания экземпляра персистентного двусвязного списка. - """ - return PersistentLinkedList([1, 2, 3, 4, 5]) +def persistent_list(): + """Создает фикстуру для тестирования PersistentLinkedList.""" + initial_data = [1, 2, 3] + return PersistentLinkedList(initial_data) -def test_add(linked_list): - """Тест 1. Проверка добавления элемента в конец списка""" - linked_list.add(6) - assert linked_list.get(index=5) == 6 +def test_initial_state(persistent_list): + """Тест 1. Проверка, что начальное состояние списка корректно""" + assert persistent_list.get(version=0, index=0) == 1 + assert persistent_list.get(version=0, index=1) == 2 + assert persistent_list.get(version=0, index=2) == 3 + assert persistent_list.size == 3 -def test_add_first(linked_list): - """Тест 2. Проверка добавления элемента в начало списка""" - linked_list.add_first(0) - assert linked_list.get(index=0) == 0 +def test_add_element(persistent_list): + """Тест 2. Проверка на добавление нового элемента в конец списка""" + persistent_list.add(4) + assert persistent_list.get(version=1, index=3) == 4 + assert persistent_list.size == 4 -def test_insert(linked_list): - """Тест 3. Проверка вставки элемента по индексу""" - linked_list.insert(2, 10) - assert linked_list.get(index=2) == 10 - assert linked_list.get(index=3) == 3 +def test_add_first_element(persistent_list): + """Тест 3. Проверка на добавление элемента в начало списка""" + persistent_list.add_first(0) + assert persistent_list.get(version=1, index=0) == 0 + assert persistent_list.get(version=1, index=1) == 1 + assert persistent_list.size == 4 -def test_pop(linked_list): - """Тест 4. Проверка удаления элемента по индексу""" - removed_value = linked_list.pop(2) - assert removed_value == 3 - assert linked_list.get(index=2) == 4 +def test_insert_element(persistent_list): + """Тест 4. Проверка на вставку элемента на заданный индекс""" + persistent_list.insert(1, 10) + assert persistent_list.get(version=1, index=1) == 10 + assert persistent_list.get(version=1, index=2) == 2 + assert persistent_list.size == 4 -def test_remove(linked_list): - """Тест 5. Проверка удаления элемента по значению""" - linked_list.remove(4) +def test_pop_element(persistent_list): + """Тест 5. Проверка на удаление элемента по индексу""" + value = persistent_list.pop(1) + assert value == 2 + assert persistent_list.get(version=1, index=1) == 3 + assert persistent_list.size == 2 + + +def test_remove_element(persistent_list): + """Тест 6. Проверка на удаление элемента с использованием remove()""" + persistent_list.remove(0) + assert persistent_list.get(version=1, index=0) == 2 + assert persistent_list.size == 2 + + +def test_undo_operation(persistent_list): + """Тест 7. Проверка на возможность отмены последней операции""" + persistent_list.add(4) + persistent_list.undo() + assert persistent_list.size == 3 + assert persistent_list.get(version=0, index=2) == 3 + + +def test_redo_operation(persistent_list): + """Тест 8. Проверка на возможность повторения отмененной операции""" + persistent_list.add(4) + persistent_list.undo() + persistent_list.redo() + assert persistent_list.get(version=1, index=3) == 4 + assert persistent_list.size == 4 + + +def test_version_retrieval(persistent_list): + """Тест 9. Проверка на восстановление предыдущей версии списка""" + persistent_list.add(4) + persistent_list.add(5) + persistent_list.update_version(0) + assert persistent_list.size == 3 + assert persistent_list.get(version=0, index=2) == 3 + + +def test_invalid_version(persistent_list): + """Тест 10. Проверка на обработку неверного номера версии""" with pytest.raises(ValueError): - linked_list.remove(4) - - -def test_get(linked_list): - """Тест 6. Проверка получения элемента по индексу""" - assert linked_list.get(index=0) == 1 - assert linked_list.get(index=4) == 5 - - -def test_get_version(linked_list): - """Тест 7. Проверка получения версии списка""" - linked_list.add(6) - linked_list.add(7) - version_1 = linked_list.get_version(1) - assert version_1[0] is not None - version_0_values = [] - current = version_1[0] - while current: - version_0_values.append(current.value) - current = current.next_node - assert version_0_values == [1, 2, 3, 4, 5, 6] - - -def test_update_version(linked_list): - """Тест 8. Проверка обновления версии списка""" - linked_list.add(6) - linked_list.add(7) - linked_list.update_version(1) - assert linked_list.get(index=5) == 6 - linked_list.update_version(0) - assert linked_list.get(index=4) == 5 - - -def test_clear(linked_list): - """Тест 9. Проверка очистки списка""" - linked_list.clear() - assert linked_list.get_size() == 0 - assert linked_list.check_is_empty() - - -def test_get_size(linked_list): - """Тест 10. Проверка получения размера списка""" - assert linked_list.get_size() == 5 - linked_list.add(6) - assert linked_list.get_size() == 6 - - -def test_check_is_empty(linked_list): - """Тест 11. Проверка метода для проверки пустоты списка""" - linked_list.clear() - assert linked_list.check_is_empty() is True - linked_list.add(10) - assert linked_list.check_is_empty() is False + persistent_list.get_version(10) + + +def test_index_out_of_range(persistent_list): + """Тест 11. Проверка на обработку индекса за пределами диапазона""" + with pytest.raises(IndexError): + persistent_list.get(version=0, index=10) + + +@pytest.fixture +def setup_nested_persistent_list(): + """Фикстура для создания вложенной персистентной структуры""" + inner_list = PersistentLinkedList([4, 5]) + outer_list = PersistentLinkedList([1, inner_list, 3]) + return outer_list, inner_list + + +def test_nested_persistence(setup_nested_persistent_list): + """Тест 12. Проверка на вложенность и персистентность вложенных структур""" + outer_list, inner_list = setup_nested_persistent_list + assert outer_list.get(index=0) == 1 + assert isinstance(outer_list.get(index=1), PersistentLinkedList) + assert outer_list.get(index=2) == 3 + nested_list = outer_list.get(index=1) + assert nested_list.get(index=0) == 4 + assert nested_list.get(index=1) == 5 + nested_list.add(6) + assert nested_list.get(version=1, index=2) == 6 + updated_nested = outer_list.get(index=1) + assert updated_nested.get(version=1, index=2) == 6 + assert outer_list.get(version=0, index=1).get(index=1) == 5 + + +def test_nested_undo_redo(setup_nested_persistent_list): + """Тест 13. Проверка работы undo и redo для вложенных структур""" + outer_list, inner_list = setup_nested_persistent_list + inner_list.add(7) + assert inner_list.get(version=1, index=2) == 7 + inner_list.undo() + assert inner_list.get(index=1) == 5 + with pytest.raises(IndexError): + inner_list.get(index=2) + inner_list.redo() + assert inner_list.get(index=2) == 7 + + +def test_deep_nested_persistence(): + """Тест 14. Проверка на многослойную вложенность персистентных структур""" + inner_list = PersistentLinkedList([1, 2]) + middle_list = PersistentLinkedList([inner_list]) + outer_list = PersistentLinkedList([middle_list]) + inner_list.add(3) + assert inner_list.get(index=2) == 3 + assert middle_list.get(index=0).get(index=2) == 3 + assert outer_list.get(index=0).get(index=0).get(index=2) == 3 + assert inner_list.get(version=0, index=1) == 2 + assert middle_list.get(version=0, index=0).get(version=0, index=1) == 2 + assert outer_list.get(version=0, index=0).get(version=0, index=0).get(version=0, index=1) == 2 + + +def test_cascade_undo_redo(): + """Тест 15. Проверка каскадную вложенность""" + list1 = PersistentLinkedList([1, 2, 3]) + list2 = PersistentLinkedList([4, 5]) + list3 = PersistentLinkedList([6, 7]) + list1.add(list2) + list2.add(list3) + assert list1[3] == list2 + assert list1[3][2] == list3 + + +def test_triple_nested_undo_redo(): + """Тест 16. Проверка undo-redo с тройной вложенностью""" + list1 = PersistentLinkedList([1, 2, 3]) + list2 = PersistentLinkedList([4, 5]) + list3 = PersistentLinkedList([6, 7]) + list1.add(list2) + list2.add(list3) + list3.add(8) + list1.undo() + with pytest.raises(IndexError): + list1[3][2][2] + assert list1[3][2][1] == 7 + assert list1[3][2][0] == 6 + list1.undo() + with pytest.raises(IndexError): + list1[3][2] + list1.redo() + with pytest.raises(IndexError): + list1[3][2][2] + assert list1[3][2][1] == 7 + assert list1[3][2][0] == 6 + with pytest.raises(NotImplementedError): + list2.redo() diff --git a/tests/test_map.py b/tests/test_map.py index 580e6b6..115cab9 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -1,83 +1,204 @@ import pytest - from persistent_map import PersistentMap -# Тестирование методов класса PersistentMap @pytest.fixture -def persistent_map(): - """Фикстура для создания PersistentMap""" - return PersistentMap({'a': 1, 'b': 2}) +def setup_persistent_map(): + """Фикстура для создания персистентного ассоциативного массива.""" + return PersistentMap({"a": 1, "b": 2, "c": 3}) + +def test_get_version(setup_persistent_map): + """Тест 1. Проверка получения версии персистентного массива""" + persistent_map = setup_persistent_map + assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3} -def test_get_version(persistent_map): - """Тест 1. Проверка получения состояния на определенной версии""" - persistent_map['c'] = 3 - persistent_map.update_version(1) - assert persistent_map['c'] == 3 +def test_update_version(setup_persistent_map): + """Тест 2. Проверка обновления версии массива""" + persistent_map = setup_persistent_map + persistent_map["d"] = 4 persistent_map.update_version(0) - with pytest.raises(KeyError, match='Key "c" does not exist'): - persistent_map.get(0, 'c') + assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3} -def test_update_version(persistent_map): - """Тест 2. Проверка обновления версии""" - persistent_map['c'] = 3 - persistent_map.update_version(1) - assert persistent_map['c'] == 3 +def test_setitem_add_new_key(setup_persistent_map): + """Тест 3. Проверка добавления нового ключа""" + persistent_map = setup_persistent_map + persistent_map["d"] = 4 + assert persistent_map["d"] == 4 + assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "d": 4} -def test_invalid_version_get(persistent_map): - """Тест 3. Проверка на исключение для недопустимой версии при получении""" - with pytest.raises(ValueError, match='Version "2" does not exist'): - persistent_map.get(2, 'a') +def test_setitem_update_key(setup_persistent_map): + """Тест 4. Проверка изменения значения существующего ключа""" + persistent_map = setup_persistent_map + persistent_map["a"] = 10 + assert persistent_map["a"] == 10 + assert persistent_map.get_version(1) == {"a": 10, "b": 2, "c": 3} -def test_invalid_key_get(persistent_map): - """Тест 4. Проверка на исключение для недопустимого ключа при получении""" - with pytest.raises(KeyError, match='Key "c" does not exist'): - persistent_map.get(0, 'c') +def test_getitem(setup_persistent_map): + """Тест 5. Проверка получения значения по ключу""" + persistent_map = setup_persistent_map + assert persistent_map["a"] == 1 + assert persistent_map["b"] == 2 + assert persistent_map["c"] == 3 -def test_setitem(persistent_map): - """Тест 5. Проверка обновления элемента с использованием __setitem__""" - persistent_map['c'] = 3 - assert persistent_map['c'] == 3 +def test_get_existing_version_key(setup_persistent_map): + """Тест 6. Проверка получения значения существующего ключа из версии""" + persistent_map = setup_persistent_map + assert persistent_map.get(0, "a") == 1 + with pytest.raises(KeyError): + persistent_map.get(0, "d") -def test_getitem(persistent_map): - """Тест 6. Проверка получения элемента с использованием __getitem__""" - assert persistent_map['a'] == 1 - assert persistent_map['b'] == 2 +def test_get_invalid_version(setup_persistent_map): + """Тест 7. Проверка ошибки при запросе несуществующей версии""" + persistent_map = setup_persistent_map + with pytest.raises(ValueError): + persistent_map.get(10, "a") -def test_pop(persistent_map): - """Тест 7. Проверка удаления элемента с использованием pop""" - value = persistent_map.pop('a') - assert value == 1 - assert 'a' not in persistent_map._history[persistent_map._current_state] +def test_pop_existing_key(setup_persistent_map): + """Тест 8. Проверка удаления ключа методом pop""" + persistent_map = setup_persistent_map + popped_value = persistent_map.pop("a") + assert popped_value == 1 + assert "a" not in persistent_map.get_version(1) -def test_remove(persistent_map): - """Тест 8. Проверка удаления элемента с использованием remove""" - persistent_map.remove('a') - assert 'a' not in persistent_map._history[persistent_map._current_state] +def test_pop_non_existing_key(setup_persistent_map): + """Тест 9. Проверка ошибки при pop несуществующего ключа""" + persistent_map = setup_persistent_map + with pytest.raises(KeyError): + persistent_map.pop("d") -def test_clear(persistent_map): - """Тест 9. Проверка очистки структуры данных""" - persistent_map.clear() - assert persistent_map._history[persistent_map._current_state] == {} +def test_remove_existing_key(setup_persistent_map): + """Тест 10. Проверка удаления ключа методом remove""" + persistent_map = setup_persistent_map + persistent_map.remove("b") + assert "b" not in persistent_map.get_version(1) -def test_version_history(persistent_map): - """Тест 10. Проверка истории версий""" - persistent_map['c'] = 3 - persistent_map.update_version(1) - persistent_map['d'] = 4 - persistent_map.update_version(2) +def test_remove_non_existing_key(setup_persistent_map): + """Тест 11. Проверка ошибки при remove несуществующего ключа""" + persistent_map = setup_persistent_map + with pytest.raises(KeyError): + persistent_map.remove("d") - assert persistent_map.get_version(0) == {'a': 1, 'b': 2} - assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} - assert persistent_map.get_version(2) == {'a': 1, 'b': 2, 'c': 3, 'd': 4} + +def test_clear(setup_persistent_map): + """Тест 12. Проверка очистки массива методом clear""" + persistent_map = setup_persistent_map + persistent_map.clear() + assert persistent_map.get_version(1) == {} + + +def test_undo(setup_persistent_map): + """Тест 13. Проверка отмены последней операции""" + persistent_map = setup_persistent_map + persistent_map["d"] = 4 + persistent_map.undo() + assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3} + + +def test_redo(setup_persistent_map): + """Тест 14. Проверка повторения последней отмененной операции""" + persistent_map = setup_persistent_map + persistent_map["d"] = 4 + persistent_map.undo() + persistent_map.redo() + assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "d": 4} + + +def test_undo_without_changes(setup_persistent_map): + """Тест 15. Проверка ошибки отмены операции без изменений""" + persistent_map = setup_persistent_map + with pytest.raises(ValueError): + persistent_map.undo() + + +def test_redo_without_undo(setup_persistent_map): + """Тест 16. Проверка ошибки повтора без отмены операции""" + persistent_map = setup_persistent_map + persistent_map["d"] = 4 + with pytest.raises(ValueError): + persistent_map.redo() + + +def test_setitem_nested_structure(setup_persistent_map): + """Тест 17. Проверка добавления вложенной структуры в персистентный массив""" + persistent_map = setup_persistent_map + nested_map = PersistentMap({"nested_key": 1}) + persistent_map["nested"] = nested_map + assert persistent_map["nested"].get_version(0) == {"nested_key": 1} + assert persistent_map.get_version(1) == {"a": 1, "b": 2, "c": 3, "nested": nested_map} + + +def test_nested_undo(setup_persistent_map): + """Тест 18. Проверка отмены изменений в вложенной структуре""" + persistent_map = setup_persistent_map + nested_map = PersistentMap({"nested_key": 1}) + persistent_map["nested"] = nested_map + nested_map["nested_key"] = 2 + persistent_map.undo() + assert persistent_map.get_version(0) == {"a": 1, "b": 2, "c": 3} + assert persistent_map.get_version(1)["nested"]["nested_key"] == 1 + + +def test_nested_redo(setup_persistent_map): + """Тест 19. Проверка повтора изменений в вложенной структуре""" + persistent_map = setup_persistent_map + nested_map = PersistentMap({"nested_key": 1}) + persistent_map["nested"] = nested_map + nested_map["nested_key"] = 2 + persistent_map.undo() + persistent_map.redo() + assert persistent_map.get_version(2)["nested"]["nested_key"] == 2 + + +def test_remove_nested_structure(setup_persistent_map): + """Тест 20. Проверка удаления вложенной структуры методом remove""" + persistent_map = setup_persistent_map + nested_map = PersistentMap({"nested_key": 1}) + persistent_map["nested"] = nested_map + persistent_map.remove("nested") + with pytest.raises(KeyError): + persistent_map["nested"] + + +def test_cascade_undo_redo_nested(): + """Тест 13. Проверка каскадного undo/redo с вложенными структурами""" + persistent_map = PersistentMap({ + "level1": PersistentMap({ + "level2": PersistentMap({ + "level3": 42 + }) + }) + }) + persistent_map["level1"]["level2"]["level3"] = 100 + assert persistent_map["level1"]["level2"]["level3"] == 100 + persistent_map["level1"]["level2"].undo() + assert persistent_map["level1"]["level2"]["level3"] == 42 + persistent_map["level1"]["level2"].redo() + assert persistent_map["level1"]["level2"]["level3"] == 100 + + +def test_triple_nested_undo_redo(): + """Тест 14. Проверка undo/redo с тройной вложенностью""" + persistent_map = PersistentMap({ + "level1": PersistentMap({ + "level2": PersistentMap({ + "level3": PersistentMap({"key": 333}) + }) + }) + }) + persistent_map["level1"]["level2"]["level3"]["key"] = 200 + assert persistent_map["level1"]["level2"]["level3"]["key"] == 200 + persistent_map["level1"]["level2"]["level3"].undo() + assert persistent_map["level1"]["level2"]["level3"]["key"] == 333 + persistent_map["level1"]["level2"]["level3"].redo() + assert persistent_map["level1"]["level2"]["level3"]["key"] == 200