From 06767c91961cef473a352ed8397224af39b04829 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 14 Apr 2026 17:43:07 +0300 Subject: [PATCH 01/29] feat: add duration and start time to Task --- src/model/Task.java | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/model/Task.java b/src/model/Task.java index 8ed25ad..71b3bd5 100644 --- a/src/model/Task.java +++ b/src/model/Task.java @@ -1,5 +1,7 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Objects; public class Task { @@ -7,11 +9,19 @@ public class Task { private String name; private String description; private Status status; + private Duration duration; + private LocalDateTime startTime; public Task(String name, String description, Status status) { + this(name, description, status, null, null); + } + + public Task(String name, String description, Status status, Duration duration, LocalDateTime startTime) { this.name = name; this.description = description; this.status = status; + this.duration = duration; + this.startTime = startTime; } public int getId() { @@ -46,6 +56,29 @@ public void setStatus(Status status) { this.status = status; } + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + if (startTime == null || duration == null) { + return null; + } + return startTime.plus(duration); + } + // Переопределение методов equals и hashCode для ID @Override public boolean equals(Object obj) { @@ -67,6 +100,9 @@ public String toString() { + ", name='" + name + '\'' + ", description='" + description + '\'' + ", status=" + status + + ", duration=" + duration + + ", startTime=" + startTime + + ", endTime=" + getEndTime() + '}'; } } From 7853dc76eb1ff7c22ba28cdae14b0f20417c1d90 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 14 Apr 2026 17:46:49 +0300 Subject: [PATCH 02/29] feat: add duration and start time to Subtask --- src/model/Subtask.java | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/model/Subtask.java b/src/model/Subtask.java index 50b0071..8abe94a 100644 --- a/src/model/Subtask.java +++ b/src/model/Subtask.java @@ -1,12 +1,19 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.Objects; public class Subtask extends Task { private int epicId; public Subtask(String name, String description, Status status, int epicId) { - super(name, description, status); + this(name, description, status, null, null, epicId); + } + + public Subtask(String name, String description, Status status, + Duration duration, LocalDateTime startTime, int epicId) { + super(name, description, status, duration, startTime); this.epicId = epicId; } @@ -44,12 +51,15 @@ public int hashCode() { @Override public String toString() { - return "Subtask{" + - "id=" + getId() + - ", name='" + getName() + '\'' + - ", description='" + getDescription() + '\'' + - ", status=" + getStatus() + - ", epicId=" + epicId + - '}'; + return "Subtask{" + + "id=" + getId() + + ", name='" + getName() + '\'' + + ", description='" + getDescription() + '\'' + + ", status=" + getStatus() + + ", duration=" + getDuration() + + ", startTime=" + getStartTime() + + ", endTime=" + getEndTime() + + ", epicId=" + epicId + + '}'; } } From d1eda1c0c94dbeaeb43c2b0c8d6d32baa755c6a4 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 14 Apr 2026 17:49:37 +0300 Subject: [PATCH 03/29] feat: add calculated time fields to Epic --- src/model/Epic.java | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 00edfd5..0e08e37 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -1,9 +1,12 @@ package model; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; public class Epic extends Task { private ArrayList subtaskIds = new ArrayList<>(); + private LocalDateTime endTime; public Epic(String name, String description) { super(name, description, Status.NEW); @@ -34,14 +37,34 @@ public void removeSubtaskId(Integer subtaskId) { subtaskIds.remove(subtaskId); } + public void setEpicDuration(Duration duration) { + setDuration(duration); + } + + public void setEpicStartTime(LocalDateTime startTime) { + setStartTime(startTime); + } + + public void setEpicEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + @Override + public LocalDateTime getEndTime() { + return endTime; + } + @Override public String toString() { - return "Epic{" + - "id=" + getId() + - ", name='" + getName() + '\'' + - ", description='" + getDescription() + '\'' + - ", status=" + getStatus() + - ", subtaskIds=" + subtaskIds + - '}'; + return "Epic{" + + "id=" + getId() + + ", name='" + getName() + '\'' + + ", description='" + getDescription() + '\'' + + ", status=" + getStatus() + + ", duration=" + getDuration() + + ", startTime=" + getStartTime() + + ", endTime=" + getEndTime() + + ", subtaskIds=" + subtaskIds + + '}'; } } From a4aaf79621eab5085e253ef17a758752d8b288dd Mon Sep 17 00:00:00 2001 From: Ksenia Date: Tue, 14 Apr 2026 17:53:05 +0300 Subject: [PATCH 04/29] fix: use defensive copy in Epic setSubtaskIds --- src/model/Epic.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/Epic.java b/src/model/Epic.java index 0e08e37..1c52eb1 100644 --- a/src/model/Epic.java +++ b/src/model/Epic.java @@ -17,7 +17,7 @@ public ArrayList getSubtaskIds() { } public void setSubtaskIds(ArrayList subtaskIds) { - this.subtaskIds = subtaskIds; + this.subtaskIds = new ArrayList<>(subtaskIds); } // Очищение всех ID подзадач @@ -67,4 +67,4 @@ public String toString() { + ", subtaskIds=" + subtaskIds + '}'; } -} +} \ No newline at end of file From c7c5c4681082653857bb85578e884e51d531edb3 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 13:36:29 +0300 Subject: [PATCH 05/29] fix: return full copies of tasks with time fields --- src/service/InMemoryTaskManager.java | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index b2d6126..811bf07 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -87,11 +87,19 @@ public void clearSubtasks() { @Override public Task getTask(int id) { Task task = tasks.get(id); - if (task == null) return null; + if (task == null) { + return null; + } historyManager.add(task); - Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + Task copy = new Task( + task.getName(), + task.getDescription(), + task.getStatus(), + task.getDuration(), + task.getStartTime() + ); copy.setId(task.getId()); return copy; @@ -100,7 +108,9 @@ public Task getTask(int id) { @Override public Epic getEpic(int id) { Epic epic = epics.get(id); - if (epic == null) return null; + if (epic == null) { + return null; + } historyManager.add(epic); @@ -108,6 +118,9 @@ public Epic getEpic(int id) { copy.setId(epic.getId()); copy.setStatus(epic.getStatus()); copy.setSubtaskIds(epic.getSubtaskIds()); + copy.setEpicDuration(epic.getDuration()); + copy.setEpicStartTime(epic.getStartTime()); + copy.setEpicEndTime(epic.getEndTime()); return copy; } @@ -115,7 +128,9 @@ public Epic getEpic(int id) { @Override public Subtask getSubtask(int id) { Subtask subtask = subtasks.get(id); - if (subtask == null) return null; + if (subtask == null) { + return null; + } historyManager.add(subtask); @@ -123,6 +138,8 @@ public Subtask getSubtask(int id) { subtask.getName(), subtask.getDescription(), subtask.getStatus(), + subtask.getDuration(), + subtask.getStartTime(), subtask.getEpicId() ); copy.setId(subtask.getId()); From 7e81a478e0751c807c6a4ab82e3e92179011cc6f Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 15:05:07 +0300 Subject: [PATCH 06/29] feat: add prioritized tasks storage with TreeSet --- src/service/InMemoryTaskManager.java | 91 +++++++++++++++++++++------- src/service/TaskManager.java | 7 ++- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 811bf07..d78942e 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -6,8 +6,10 @@ import model.Epic; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.TreeSet; public class InMemoryTaskManager implements service.TaskManager { // Хеш-таблицы для хранения задач для классов Task, Epic, Subtask @@ -15,18 +17,24 @@ public class InMemoryTaskManager implements service.TaskManager { private HashMap epics = new HashMap<>(); private HashMap subtasks = new HashMap<>(); + // Хранение задач и подзадач в отсортированном порядке по времени начала + private final TreeSet prioritizedTasks = new TreeSet<>( + Comparator.comparing(Task::getStartTime) + .thenComparing(Task::getId) + ); + // Менеджер истории private final HistoryManager historyManager = Managers.getDefaultHistory(); // Счетчик для генерации ID private int nextId = 1; - // Метод для увеличения ID + // Получение следующего идентификатора задачи private int getNextId() { return nextId++; } - // Получение списка всех задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Получение списков задач, эпиков и подзадач @Override public ArrayList getTasks() { return new ArrayList<>(tasks.values()); @@ -42,22 +50,25 @@ public ArrayList getSubtasks() { return new ArrayList<>(subtasks.values()); } - // Удаление всех задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Удаление всех задач @Override public void clearTasks() { - // Удаление каждой задачи из истории перед очисткой хранилища - for (int id : tasks.keySet()) { - historyManager.remove(id); + // Удаление каждой задачи из истории и списка приоритетов перед очисткой хранилища + for (Task task : tasks.values()) { + prioritizedTasks.remove(task); + historyManager.remove(task.getId()); } tasks.clear(); } @Override public void clearEpics() { - // Сначала удаление всех подзадач из истории - for (int id : subtasks.keySet()) { - historyManager.remove(id); + // Сначала удаление всех подзадач из истории и списка приоритетов + for (Subtask subtask : subtasks.values()) { + prioritizedTasks.remove(subtask); + historyManager.remove(subtask.getId()); } + // Затем самих эпиков for (int id : epics.keySet()) { historyManager.remove(id); @@ -69,21 +80,22 @@ public void clearEpics() { @Override public void clearSubtasks() { - // Удаление каждой подзадачи из истории - for (int id : subtasks.keySet()) { - historyManager.remove(id); + // Удаление каждой подзадачи из истории и списка приоритетов + for (Subtask subtask : subtasks.values()) { + prioritizedTasks.remove(subtask); + historyManager.remove(subtask.getId()); } subtasks.clear(); - // Чистка Эпиков, если подзадач больше нет + // Очистка эпиков, если подзадач больше нет for (Epic epic : epics.values()) { epic.clearSubtaskIds(); epic.setStatus(Status.NEW); } } - // Получение по идентификатору для каждого из типов задач(Задача/Эпик/Подзадача) + // Получение задачи, эпика или подзадачи по идентификатору @Override public Task getTask(int id) { Task task = tasks.get(id); @@ -147,18 +159,33 @@ public Subtask getSubtask(int id) { return copy; } - // Возвращение списка истории + // Получение списка истории @Override public List getHistory() { return historyManager.getHistory(); } - // Создание для каждого из типов задач(Задача/Эпик/Подзадача) + // Получение задач и подзадач в порядке приоритета по времени начала + @Override + public List getPrioritizedTasks() { + return new ArrayList<>(prioritizedTasks); + } + + // Добавление задачи или подзадачи в список приоритетов, если задано время начала + protected void addTaskToPrioritizedTasks(Task task) { + if (task.getStartTime() != null) { + prioritizedTasks.add(task); + } + } + + // Создание задач, эпиков и подзадач @Override public Task createTask(Task task) { task.setId(getNextId()); task.setStatus(Status.NEW); tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); + return task; } @@ -175,6 +202,7 @@ public Subtask createSubtask(Subtask subtask) { subtask.setId(getNextId()); subtask.setStatus(Status.NEW); subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); // Добавляем в Эпик ID новой Подзадачи Epic epic = epics.get(subtask.getEpicId()); @@ -186,12 +214,15 @@ public Subtask createSubtask(Subtask subtask) { return subtask; } - // Обновление задач для каждого из типов задач(Задача/Эпик/Подзадача) + // Обновление задачи с синхронизацией списка приоритетов @Override public void updateTask(Task task) { - // Проверка на наличие задачи if (tasks.containsKey(task.getId())) { + Task oldTask = tasks.get(task.getId()); + + prioritizedTasks.remove(oldTask); tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); } } @@ -238,9 +269,13 @@ protected void updateEpicStatus(Epic epic) { @Override public void updateSubtask(Subtask subtask) { if (subtasks.containsKey(subtask.getId())) { + Subtask oldSubtask = subtasks.get(subtask.getId()); + + prioritizedTasks.remove(oldSubtask); subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); - // Пересчёт статуса Эпик после обновления Подзадач + // Пересчёт статуса эпика после обновления подзадачи Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { updateEpicStatus(epic); @@ -251,7 +286,10 @@ public void updateSubtask(Subtask subtask) { // Удаление по идентификатору для каждого из типов задач(Задача/Эпик/Подзадача) @Override public void deleteTask(int id) { - tasks.remove(id); + Task removedTask = tasks.remove(id); + if (removedTask != null) { + prioritizedTasks.remove(removedTask); + } historyManager.remove(id); } @@ -261,7 +299,10 @@ public void deleteEpic(int id) { if (epic != null) { // Удаление подзадач, связанных с этим Эпиком for (Integer subtaskId : epic.getSubtaskIds()) { - subtasks.remove(subtaskId); + Subtask removedSubtask = subtasks.remove(subtaskId); + if (removedSubtask != null) { + prioritizedTasks.remove(removedSubtask); + } historyManager.remove(subtaskId); } } @@ -272,6 +313,8 @@ public void deleteEpic(int id) { public void deleteSubtask(int id) { Subtask subtask = subtasks.remove(id); if (subtask != null) { + prioritizedTasks.remove(subtask); + // Удаление ID Подзадачи из Эпика Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { @@ -282,8 +325,8 @@ public void deleteSubtask(int id) { historyManager.remove(id); } - // Получение списка всех Подзадач для определённого Эпика - public ArrayList getEpicSubtasks(int epicId) { + // Получение списка подзадач определённого эпика + public List getEpicSubtasks(int epicId) { ArrayList result = new ArrayList<>(); Epic epic = epics.get(epicId); if (epic != null) { @@ -297,6 +340,7 @@ public ArrayList getEpicSubtasks(int epicId) { // Служебные методы для восстановления менеджера из файла protected void putLoadedTask(Task task) { tasks.put(task.getId(), task); + addTaskToPrioritizedTasks(task); } protected void putLoadedEpic(Epic epic) { @@ -305,6 +349,7 @@ protected void putLoadedEpic(Epic epic) { protected void putLoadedSubtask(Subtask subtask) { subtasks.put(subtask.getId(), subtask); + addTaskToPrioritizedTasks(subtask); Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { diff --git a/src/service/TaskManager.java b/src/service/TaskManager.java index b70e41d..85c6c17 100644 --- a/src/service/TaskManager.java +++ b/src/service/TaskManager.java @@ -53,6 +53,9 @@ public interface TaskManager { void deleteSubtask(int id); - // Получение списка всех Подзадач для определённого Эпика - ArrayList getEpicSubtasks(int epicId); + // Получение подзадач эпика по id + List getEpicSubtasks(int epicId); + + // Получение задач и подзадач в порядке приоритета (по startTime) + List getPrioritizedTasks(); } From 3b4b597f5b1e133dbe12dd5f9572eee53045cb27 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 15:57:24 +0300 Subject: [PATCH 07/29] feat: add task time overlap validation --- src/service/InMemoryTaskManager.java | 45 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index d78942e..f784ffb 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -171,6 +171,27 @@ public List getPrioritizedTasks() { return new ArrayList<>(prioritizedTasks); } + // Проверка пересечения двух задач по времени выполнения + protected boolean isTaskOverlapping(Task firstTask, Task secondTask) { + if (firstTask.getStartTime() == null || secondTask.getStartTime() == null) { + return false; + } + + if (firstTask.getEndTime() == null || secondTask.getEndTime() == null) { + return false; + } + + return firstTask.getStartTime().isBefore(secondTask.getEndTime()) + && secondTask.getStartTime().isBefore(firstTask.getEndTime()); + } + + // Проверка пересечения задачи с уже существующими задачами и подзадачами + protected boolean hasTimeOverlap(Task task) { + return prioritizedTasks.stream() + .anyMatch(prioritizedTask -> prioritizedTask.getId() != task.getId() + && isTaskOverlapping(task, prioritizedTask)); + } + // Добавление задачи или подзадачи в список приоритетов, если задано время начала protected void addTaskToPrioritizedTasks(Task task) { if (task.getStartTime() != null) { @@ -181,6 +202,10 @@ protected void addTaskToPrioritizedTasks(Task task) { // Создание задач, эпиков и подзадач @Override public Task createTask(Task task) { + if (hasTimeOverlap(task)) { + throw new IllegalArgumentException("Задача пересекается по времени с другой задачей."); + } + task.setId(getNextId()); task.setStatus(Status.NEW); tasks.put(task.getId(), task); @@ -199,12 +224,16 @@ public Epic createEpic(Epic epic) { @Override public Subtask createSubtask(Subtask subtask) { + if (hasTimeOverlap(subtask)) { + throw new IllegalArgumentException("Подзадача пересекается по времени с другой задачей."); + } + subtask.setId(getNextId()); subtask.setStatus(Status.NEW); subtasks.put(subtask.getId(), subtask); addTaskToPrioritizedTasks(subtask); - // Добавляем в Эпик ID новой Подзадачи + // Добавляем в эпик id новой подзадачи Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { epic.addSubtaskId(subtask.getId()); @@ -221,6 +250,12 @@ public void updateTask(Task task) { Task oldTask = tasks.get(task.getId()); prioritizedTasks.remove(oldTask); + + if (hasTimeOverlap(task)) { + addTaskToPrioritizedTasks(oldTask); + throw new IllegalArgumentException("Обновлённая задача пересекается по времени с другой задачей."); + } + tasks.put(task.getId(), task); addTaskToPrioritizedTasks(task); } @@ -272,6 +307,12 @@ public void updateSubtask(Subtask subtask) { Subtask oldSubtask = subtasks.get(subtask.getId()); prioritizedTasks.remove(oldSubtask); + + if (hasTimeOverlap(subtask)) { + addTaskToPrioritizedTasks(oldSubtask); + throw new IllegalArgumentException("Обновлённая подзадача пересекается по времени с другой задачей."); + } + subtasks.put(subtask.getId(), subtask); addTaskToPrioritizedTasks(subtask); @@ -283,7 +324,7 @@ public void updateSubtask(Subtask subtask) { } } - // Удаление по идентификатору для каждого из типов задач(Задача/Эпик/Подзадача) + // Удаление задачи, эпика или подзадачи по идентификатору @Override public void deleteTask(int id) { Task removedTask = tasks.remove(id); From 07ab5ea67943e03c86e2bf9ea65396b6a910fddf Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 16:26:57 +0300 Subject: [PATCH 08/29] feat: calculate epic duration and time based on subtasks --- src/service/InMemoryTaskManager.java | 57 ++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index f784ffb..4e111af 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -5,6 +5,8 @@ import model.Subtask; import model.Epic; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -13,9 +15,9 @@ public class InMemoryTaskManager implements service.TaskManager { // Хеш-таблицы для хранения задач для классов Task, Epic, Subtask - private HashMap tasks = new HashMap<>(); - private HashMap epics = new HashMap<>(); - private HashMap subtasks = new HashMap<>(); + private final HashMap tasks = new HashMap<>(); + private final HashMap epics = new HashMap<>(); + private final HashMap subtasks = new HashMap<>(); // Хранение задач и подзадач в отсортированном порядке по времени начала private final TreeSet prioritizedTasks = new TreeSet<>( @@ -92,6 +94,7 @@ public void clearSubtasks() { for (Epic epic : epics.values()) { epic.clearSubtaskIds(); epic.setStatus(Status.NEW); + updateEpicTime(epic); } } @@ -238,6 +241,7 @@ public Subtask createSubtask(Subtask subtask) { if (epic != null) { epic.addSubtaskId(subtask.getId()); updateEpicStatus(epic); + updateEpicTime(epic); } return subtask; @@ -273,7 +277,7 @@ public void updateEpic(Epic epic) { } } - // Метод для обновления статуса Эпик при обновлении Подзадач + // Пересчёт статуса эпика на основе его подзадач protected void updateEpicStatus(Epic epic) { if (epic.getSubtaskIds().isEmpty()) { epic.setStatus(Status.NEW); @@ -301,6 +305,47 @@ protected void updateEpicStatus(Epic epic) { } } + // Пересчёт времени эпика на основе его подзадач + protected void updateEpicTime(Epic epic) { + if (epic.getSubtaskIds().isEmpty()) { + epic.setEpicDuration(null); + epic.setEpicStartTime(null); + epic.setEpicEndTime(null); + return; + } + + long totalDurationInMinutes = 0; + LocalDateTime earliestStartTime = null; + LocalDateTime latestEndTime = null; + + for (int subtaskId : epic.getSubtaskIds()) { + Subtask subtask = subtasks.get(subtaskId); + if (subtask == null) { + continue; + } + + if (subtask.getDuration() != null) { + totalDurationInMinutes += subtask.getDuration().toMinutes(); + } + + if (subtask.getStartTime() != null) { + if (earliestStartTime == null || subtask.getStartTime().isBefore(earliestStartTime)) { + earliestStartTime = subtask.getStartTime(); + } + } + + if (subtask.getEndTime() != null) { + if (latestEndTime == null || subtask.getEndTime().isAfter(latestEndTime)) { + latestEndTime = subtask.getEndTime(); + } + } + } + + epic.setEpicDuration(Duration.ofMinutes(totalDurationInMinutes)); + epic.setEpicStartTime(earliestStartTime); + epic.setEpicEndTime(latestEndTime); + } + @Override public void updateSubtask(Subtask subtask) { if (subtasks.containsKey(subtask.getId())) { @@ -320,6 +365,7 @@ public void updateSubtask(Subtask subtask) { Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { updateEpicStatus(epic); + updateEpicTime(epic); } } } @@ -361,6 +407,7 @@ public void deleteSubtask(int id) { if (epic != null) { epic.removeSubtaskId(id); updateEpicStatus(epic); + updateEpicTime(epic); } } historyManager.remove(id); @@ -395,6 +442,8 @@ protected void putLoadedSubtask(Subtask subtask) { Epic epic = epics.get(subtask.getEpicId()); if (epic != null) { epic.addSubtaskId(subtask.getId()); + updateEpicStatus(epic); + updateEpicTime(epic); } } From 7ae11d242489c40176b183ee605d0352e722e270 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 16:58:47 +0300 Subject: [PATCH 09/29] feat: save and load task time fields in csv --- src/service/FileBackedTaskManager.java | 37 ++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/service/FileBackedTaskManager.java b/src/service/FileBackedTaskManager.java index 44e50ce..63328b2 100644 --- a/src/service/FileBackedTaskManager.java +++ b/src/service/FileBackedTaskManager.java @@ -12,6 +12,8 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.TreeMap; public class FileBackedTaskManager extends InMemoryTaskManager { @@ -24,7 +26,7 @@ public FileBackedTaskManager(File file) { // Сохраняет текущее состояние менеджера в CSV-файл private void save() { try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - writer.write("id,type,name,status,description,epic"); + writer.write("id,type,name,status,description,duration,startTime,epic"); writer.newLine(); TreeMap allTasks = new TreeMap<>(); @@ -50,18 +52,27 @@ private void save() { } } - // Преобразует задачу в строку формата CSV + // Преобразует задачу в строку формата CSV с учетом времени и продолжительности private String toString(Task task) { StringBuilder builder = new StringBuilder(); builder.append(task.getId()).append(","); builder.append(task.getType()).append(","); - builder.append(task.getName()).append(","); builder.append(task.getStatus()).append(","); builder.append(task.getDescription()).append(","); - if (task instanceof Subtask) { + if (task.getDuration() != null) { + builder.append(task.getDuration().toMinutes()); + } + builder.append(","); + + if (task.getStartTime() != null) { + builder.append(task.getStartTime()); + } + builder.append(","); + + if (task.getType() == TaskType.SUBTASK) { Subtask subtask = (Subtask) task; builder.append(subtask.getEpicId()); } @@ -69,7 +80,7 @@ private String toString(Task task) { return builder.toString(); } - // Преобразует строку CSV в объект задачи + // Преобразует строку CSV в объект задачи с учетом времени и продолжительности private static Task fromString(String value) { String[] fields = value.split(",", -1); @@ -79,16 +90,26 @@ private static Task fromString(String value) { Status status = Status.valueOf(fields[3]); String description = fields[4]; + Duration duration = null; + if (!fields[5].isBlank()) { + duration = Duration.ofMinutes(Long.parseLong(fields[5])); + } + + LocalDateTime startTime = null; + if (!fields[6].isBlank()) { + startTime = LocalDateTime.parse(fields[6]); + } + Task task; if (taskType == TaskType.TASK) { - task = new Task(name, description, status); + task = new Task(name, description, status, duration, startTime); } else if (taskType == TaskType.EPIC) { task = new Epic(name, description); task.setStatus(status); } else { - int epicId = Integer.parseInt(fields[5]); - task = new Subtask(name, description, status, epicId); + int epicId = Integer.parseInt(fields[7]); + task = new Subtask(name, description, status, duration, startTime, epicId); } task.setId(id); From 5374a8e434ee7694d621f03c327e641e6d25cb3a Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 18:18:44 +0300 Subject: [PATCH 10/29] refactor: replace loops with stream api in task processing --- src/service/InMemoryTaskManager.java | 82 +++++++++++++--------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 4e111af..0f5772a 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -284,17 +284,15 @@ protected void updateEpicStatus(Epic epic) { return; } - int countNew = 0; - int countDone = 0; - - for (int subtaskId : epic.getSubtaskIds()) { - Subtask subtask = subtasks.get(subtaskId); - if (subtask.getStatus() == Status.NEW) { - countNew++; - } else if (subtask.getStatus() == Status.DONE) { - countDone++; - } - } + long countNew = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getStatus() == Status.NEW) + .count(); + + long countDone = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getStatus() == Status.DONE) + .count(); if (countNew == epic.getSubtaskIds().size()) { epic.setStatus(Status.NEW); @@ -305,6 +303,7 @@ protected void updateEpicStatus(Epic epic) { } } + // Пересчёт времени эпика на основе его подзадач // Пересчёт времени эпика на основе его подзадач protected void updateEpicTime(Epic epic) { if (epic.getSubtaskIds().isEmpty()) { @@ -314,32 +313,25 @@ protected void updateEpicTime(Epic epic) { return; } - long totalDurationInMinutes = 0; - LocalDateTime earliestStartTime = null; - LocalDateTime latestEndTime = null; - - for (int subtaskId : epic.getSubtaskIds()) { - Subtask subtask = subtasks.get(subtaskId); - if (subtask == null) { - continue; - } - - if (subtask.getDuration() != null) { - totalDurationInMinutes += subtask.getDuration().toMinutes(); - } - - if (subtask.getStartTime() != null) { - if (earliestStartTime == null || subtask.getStartTime().isBefore(earliestStartTime)) { - earliestStartTime = subtask.getStartTime(); - } - } - - if (subtask.getEndTime() != null) { - if (latestEndTime == null || subtask.getEndTime().isAfter(latestEndTime)) { - latestEndTime = subtask.getEndTime(); - } - } - } + long totalDurationInMinutes = epic.getSubtaskIds().stream() + .map(subtasks::get) + .filter(subtask -> subtask.getDuration() != null) + .mapToLong(subtask -> subtask.getDuration().toMinutes()) + .sum(); + + LocalDateTime earliestStartTime = epic.getSubtaskIds().stream() + .map(subtasks::get) + .map(Subtask::getStartTime) + .filter(start -> start != null) + .min(LocalDateTime::compareTo) + .orElse(null); + + LocalDateTime latestEndTime = epic.getSubtaskIds().stream() + .map(subtasks::get) + .map(Subtask::getEndTime) + .filter(end -> end != null) + .max(LocalDateTime::compareTo) + .orElse(null); epic.setEpicDuration(Duration.ofMinutes(totalDurationInMinutes)); epic.setEpicStartTime(earliestStartTime); @@ -413,16 +405,18 @@ public void deleteSubtask(int id) { historyManager.remove(id); } - // Получение списка подзадач определённого эпика + // Получение списка подзадач эпика с использованием Stream API + @Override public List getEpicSubtasks(int epicId) { - ArrayList result = new ArrayList<>(); Epic epic = epics.get(epicId); - if (epic != null) { - for (int subtaskId : epic.getSubtaskIds()) { - result.add(subtasks.get(subtaskId)); - } + + if (epic == null) { + return new ArrayList<>(); } - return result; + + return epic.getSubtaskIds().stream() + .map(subtasks::get) + .toList(); } // Служебные методы для восстановления менеджера из файла From 9b730bd3b298b5f10dcc7f5e333035f5ce817351 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 18:21:49 +0300 Subject: [PATCH 11/29] fix: save time fields in history snapshots --- src/service/InMemoryHistoryManager.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/service/InMemoryHistoryManager.java b/src/service/InMemoryHistoryManager.java index 1a4ad28..7a60012 100644 --- a/src/service/InMemoryHistoryManager.java +++ b/src/service/InMemoryHistoryManager.java @@ -25,6 +25,8 @@ private Task makeSnapshot(Task task) { original.getName(), original.getDescription(), original.getStatus(), + original.getDuration(), + original.getStartTime(), original.getEpicId() ); copy.setId(original.getId()); @@ -35,9 +37,18 @@ private Task makeSnapshot(Task task) { copy.setId(original.getId()); copy.setStatus(original.getStatus()); copy.setSubtaskIds(original.getSubtaskIds()); + copy.setEpicDuration(original.getDuration()); + copy.setEpicStartTime(original.getStartTime()); + copy.setEpicEndTime(original.getEndTime()); return copy; } else { - Task copy = new Task(task.getName(), task.getDescription(), task.getStatus()); + Task copy = new Task( + task.getName(), + task.getDescription(), + task.getStatus(), + task.getDuration(), + task.getStartTime() + ); copy.setId(task.getId()); return copy; } From 033fdefb99cb37f9c098c244ceea28082086a540 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 22:32:25 +0300 Subject: [PATCH 12/29] test: add base task manager test class --- test/service/TaskManagerTest.java | 308 ++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 test/service/TaskManagerTest.java diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java new file mode 100644 index 0000000..7fdde9d --- /dev/null +++ b/test/service/TaskManagerTest.java @@ -0,0 +1,308 @@ +package service; + +import model.Epic; +import model.Status; +import model.Subtask; +import model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +abstract class TaskManagerTest { + + protected T taskManager; + + // Каждый наследник сам создаёт свою реализацию менеджера + protected abstract T createTaskManager(); + + @BeforeEach + void setUp() { + taskManager = createTaskManager(); + } + + // Проверка создания обычной задачи + @Test + void shouldCreateTask() { + Task task = new Task( + "Task name", + "Task description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + + Task createdTask = taskManager.createTask(task); + + assertNotNull(createdTask, "Задача должна создаться."); + assertNotNull(taskManager.getTask(createdTask.getId()), "Задача должна находиться по id."); + assertEquals("Task name", createdTask.getName(), "Имя задачи должно сохраниться."); + assertEquals(Duration.ofMinutes(30), createdTask.getDuration(), + "Продолжительность задачи должна сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), createdTask.getStartTime(), + "Время начала задачи должно сохраниться."); + } + + // Проверка создания эпика + @Test + void shouldCreateEpic() { + Epic epic = new Epic("Epic name", "Epic description"); + + Epic createdEpic = taskManager.createEpic(epic); + + assertNotNull(createdEpic, "Эпик должен создаться."); + assertNotNull(taskManager.getEpic(createdEpic.getId()), "Эпик должен находиться по id."); + assertEquals("Epic name", createdEpic.getName(), "Имя эпика должно сохраниться."); + } + + // Проверка создания подзадачи и связи с эпиком + @Test + void shouldCreateSubtaskWithEpic() { + Epic epic = new Epic("Epic", "Epic description"); + Epic createdEpic = taskManager.createEpic(epic); + + Subtask subtask = new Subtask( + "Subtask", + "Subtask description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 12, 0), + createdEpic.getId() + ); + + Subtask createdSubtask = taskManager.createSubtask(subtask); + + assertNotNull(createdSubtask, "Подзадача должна создаться."); + assertEquals(createdEpic.getId(), createdSubtask.getEpicId(), + "Подзадача должна хранить id связанного эпика."); + + List epicSubtasks = taskManager.getEpicSubtasks(createdEpic.getId()); + assertEquals(1, epicSubtasks.size(), "У эпика должна быть одна подзадача."); + assertEquals(createdSubtask.getId(), epicSubtasks.get(0).getId(), + "Подзадача должна входить в список подзадач эпика."); + } + + // Проверка расчёта статуса эпика: все подзадачи NEW + @Test + void shouldSetEpicStatusNewWhenAllSubtasksAreNew() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.NEW, savedEpic.getStatus(), + "Статус эпика должен быть NEW, если все подзадачи NEW."); + } + + // Проверка расчёта статуса эпика: все подзадачи DONE + @Test + void shouldSetEpicStatusDoneWhenAllSubtasksAreDone() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask1 = taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask subtask2 = taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + subtask1.setStatus(Status.DONE); + subtask2.setStatus(Status.DONE); + + taskManager.updateSubtask(subtask1); + taskManager.updateSubtask(subtask2); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.DONE, savedEpic.getStatus(), + "Статус эпика должен быть DONE, если все подзадачи DONE."); + } + + // Проверка расчёта статуса эпика: подзадачи NEW и DONE + @Test + void shouldSetEpicStatusInProgressWhenSubtasksAreNewAndDone() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask1 = taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(10), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask subtask2 = taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + subtask2.setStatus(Status.DONE); + + taskManager.updateSubtask(subtask1); + taskManager.updateSubtask(subtask2); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.IN_PROGRESS, savedEpic.getStatus(), + "Статус эпика должен быть IN_PROGRESS, если подзадачи NEW и DONE."); + } + + // Проверка расчёта статуса эпика: есть подзадача IN_PROGRESS + @Test + void shouldSetEpicStatusInProgressWhenSubtaskIsInProgress() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + subtask.setStatus(Status.IN_PROGRESS); + taskManager.updateSubtask(subtask); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + assertEquals(Status.IN_PROGRESS, savedEpic.getStatus(), + "Статус эпика должен быть IN_PROGRESS, если есть подзадача IN_PROGRESS."); + } + + // Проверка приоритетного списка задач + @Test + void shouldReturnPrioritizedTasksSortedByStartTime() { + Task laterTask = taskManager.createTask(new Task( + "Later task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0) + )); + + Task earlierTask = taskManager.createTask(new Task( + "Earlier task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 9, 0) + )); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + + assertEquals(2, prioritizedTasks.size(), "Должно быть две задачи в списке приоритетов."); + assertEquals(earlierTask.getId(), prioritizedTasks.get(0).getId(), + "Первая задача должна быть с более ранним startTime."); + assertEquals(laterTask.getId(), prioritizedTasks.get(1).getId(), + "Вторая задача должна быть с более поздним startTime."); + } + + // Проверка, что задача без startTime не попадает в приоритетный список + @Test + void shouldNotAddTaskWithoutStartTimeToPrioritizedTasks() { + Task taskWithoutTime = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + null + ); + + taskManager.createTask(taskWithoutTime); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + assertTrue(prioritizedTasks.isEmpty(), + "Задача без startTime не должна попадать в список приоритетов."); + } + + // Проверка пересечения интервалов при создании задачи + @Test + void shouldThrowExceptionWhenCreatingOverlappingTask() { + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertThrows( + IllegalArgumentException.class, + () -> taskManager.createTask(new Task( + "Task 2", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 30) + )), + "Создание пересекающейся задачи должно приводить к исключению." + ); + } + + // Проверка времени эпика по подзадачам + @Test + void shouldCalculateEpicTimeFromSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0), + epic.getId() + )); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals(Duration.ofMinutes(90), savedEpic.getDuration(), + "Продолжительность эпика должна быть суммой продолжительностей подзадач."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), savedEpic.getStartTime(), + "Время начала эпика должно быть временем начала самой ранней подзадачи."); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 30), savedEpic.getEndTime(), + "Время окончания эпика должно быть временем окончания самой поздней подзадачи."); + } +} \ No newline at end of file From d41c8c4167277281ef795241d05d641bac3b4631 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 22:33:06 +0300 Subject: [PATCH 13/29] test: simplify in-memory task manager test setup --- src/service/InMemoryTaskManager.java | 1 - test/service/InMemoryTaskManagerTest.java | 270 +--------------------- 2 files changed, 4 insertions(+), 267 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 0f5772a..00f6d45 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -303,7 +303,6 @@ protected void updateEpicStatus(Epic epic) { } } - // Пересчёт времени эпика на основе его подзадач // Пересчёт времени эпика на основе его подзадач protected void updateEpicTime(Epic epic) { if (epic.getSubtaskIds().isEmpty()) { diff --git a/test/service/InMemoryTaskManagerTest.java b/test/service/InMemoryTaskManagerTest.java index 8a43bf2..bc0f78c 100644 --- a/test/service/InMemoryTaskManagerTest.java +++ b/test/service/InMemoryTaskManagerTest.java @@ -1,271 +1,9 @@ package service; -import model.Status; -import model.Task; -import model.Epic; -import model.Subtask; +class InMemoryTaskManagerTest extends TaskManagerTest { -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -class InMemoryTaskManagerTest { - - private TaskManager taskManager; - - @BeforeEach - void setUp() { - taskManager = new InMemoryTaskManager(); - } - - //InMemoryTaskManager действительно добавляет задачи разного типа и может найти их по id - @Test - void addNewTask() { - Task task = new Task("Task", "Task Description", Status.NEW); - taskManager.createTask(task); - - final Task savedTask = taskManager.getTask(task.getId()); - - assertNotNull(savedTask, "Задача не найдена."); - assertEquals(task, savedTask, "Задачи не совпадают."); - - final List tasks = taskManager.getTasks(); - assertNotNull(tasks, "Задачи не возвращаются."); - assertEquals(1, tasks.size(), "Неверное количество задач."); - assertEquals(task, tasks.get(0), "Задачи не совпадают"); - } - - @Test - void addNewEpic() { - Epic epic = new Epic("Epic", "Epic Description"); - taskManager.createEpic(epic); - - final Epic savedEpic = taskManager.getEpic(epic.getId()); - - assertNotNull(savedEpic, "Эпик не найден."); - assertEquals(epic, savedEpic, "Эпики не совпадают."); - - final List epics = taskManager.getEpics(); - assertEquals(1, epics.size(), "Неверное количество эпиков."); - } - - @Test - void addNewSubtask() { - Epic epic = new Epic("Epic", "Epic Description"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask", "Subtask Description", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - final Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); - - assertNotNull(savedSubtask, "Подзадача не найдена."); - assertEquals(subtask, savedSubtask, "Подзадачи не совпадают."); - assertEquals(epic.getId(), savedSubtask.getEpicId(), "У Подзадачи неверный EpicID"); - - final List subtasks = taskManager.getSubtasks(); - assertEquals(1, subtasks.size(), "Неверное количество подзадач."); - } - - // Задачи с заданным id и сгенерированным id не конфликтуют внутри менеджера - @Test - void taskWithGeneratedIdShouldNotConflict() { - Task task = new Task("Task", "Task Description", Status.NEW); - task.setId(999); - - taskManager.createTask(task); - - assertNotEquals(999, task.getId(), - "Менеджер должен игнорировать заданный вручную ID и генерировать уникальный"); - - Task savedTask = taskManager.getTask(task.getId()); - assertNotNull(savedTask, "Задача должна быть найдена по сгенерированному ID"); - assertEquals(task, savedTask); - } - - // Tест, в котором проверяется неизменность задачи (по всем полям) при добавлении задачи в менеджер - @Test - void taskShouldBeUnchangedAfterAddingToManager() { - // Эталонные данные - String expectedName = "Orig Name"; - String expectedDescription = "Orig Description"; - Status expectedStatus = Status.NEW; - - Task task = new Task(expectedName, expectedDescription, expectedStatus); - - // Добавляем её - taskManager.createTask(task); - - // Забираем - Task savedTask = taskManager.getTask(task.getId()); - - // Проверяем - assertEquals(expectedName, savedTask.getName(), "Имя задачи изменилось при сохранении"); - assertEquals(expectedDescription, savedTask.getDescription(), - "Описание задачи изменилось при сохранении"); - assertEquals(expectedStatus, savedTask.getStatus(), "Статус задачи изменился при сохранении"); - } - - // Если удалена задача, то она должна исчезнуть из истории - @Test - void deletedTaskShouldBeRemovedFromHistory() { - Task task = new Task("Task", "Description", Status.NEW); - taskManager.createTask(task); - - taskManager.getTask(task.getId()); - assertEquals(1, taskManager.getHistory().size(), "Задача должна быть в истории"); - - taskManager.deleteTask(task.getId()); // удаляем - assertEquals(0, taskManager.getHistory().size(), - "Удалённая задача не должна оставаться в истории"); - } - - // При удалении эпика из истории удаляется и сам эпик, и все его подзадачи - @Test - void deletedEpicShouldBeRemovedFromHistoryWithSubtasks() { - Epic epic = new Epic("Epic", "Description"); - taskManager.createEpic(epic); - - Subtask subtask1 = new Subtask("Sub1", "Desc", Status.NEW, epic.getId()); - Subtask subtask2 = new Subtask("Sub2", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask1); - taskManager.createSubtask(subtask2); - - // Просматриваем всё — всё попадает в историю - taskManager.getEpic(epic.getId()); - taskManager.getSubtask(subtask1.getId()); - taskManager.getSubtask(subtask2.getId()); - assertEquals(3, taskManager.getHistory().size(), "В истории должно быть 3 элемента"); - - taskManager.deleteEpic(epic.getId()); // удаляем эпик - assertEquals(0, taskManager.getHistory().size(), - "После удаления эпика история должна быть пустой"); - } - - // Если удалена подзадача, то её id не должен оставаться внутри эпика - @Test - void deletedSubtaskShouldBeRemovedFromEpic() { - Epic epic = new Epic("Epic", "Description"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - int subtaskId = subtask.getId(); - taskManager.deleteSubtask(subtaskId); - - assertEquals(0, taskManager.getEpicSubtasks(epic.getId()).size(), - "После удаления подзадачи эпик не должен содержать её id"); - } - - // Изменение задачи через сеттер не должно влиять на данные внутри менеджера - @Test - void taskShouldNotChangeInManagerAfterSetterCall() { - Task task = new Task("Оригинальное имя", "Описание", Status.NEW); - taskManager.createTask(task); - - Task savedTask = taskManager.getTask(task.getId()); - savedTask.setName("Изменённое имя"); - - assertEquals("Оригинальное имя", - taskManager.getTask(task.getId()).getName(), - "Сеттер не должен менять данные задачи внутри менеджера"); - } - - // clearTasks() должен удалять задачи из истории - @Test - void clearTasksShouldRemoveTasksFromHistory() { - Task task1 = new Task("Task1", "Desc", Status.NEW); - Task task2 = new Task("Task2", "Desc", Status.NEW); - taskManager.createTask(task1); - taskManager.createTask(task2); - - taskManager.getTask(task1.getId()); - taskManager.getTask(task2.getId()); - assertEquals(2, taskManager.getHistory().size(), - "Перед очисткой в истории должно быть 2 задачи"); - - taskManager.clearTasks(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearTasks история должна быть пустой"); - } - - // clearEpics() должен удалять эпики и их подзадачи из истории - @Test - void clearEpicsShouldRemoveEpicsAndSubtasksFromHistory() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - taskManager.getEpic(epic.getId()); - taskManager.getSubtask(subtask.getId()); - assertEquals(2, taskManager.getHistory().size(), - "Перед очисткой в истории должно быть 2 элемента"); - - taskManager.clearEpics(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearEpics история должна быть пустой"); - } - - // Изменение подзадачи через сеттер не должно влиять на данные внутри менеджера - @Test - void subtaskShouldNotChangeInManagerAfterSetterCall() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Оригинальное имя", "Описание", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); - savedSubtask.setName("Изменённое имя"); - - assertEquals("Оригинальное имя", - taskManager.getSubtask(subtask.getId()).getName(), - "Сеттер не должен менять данные подзадачи внутри менеджера"); - } - - // clearSubtasks() должен удалять подзадачи из истории - @Test - void clearSubtasksShouldRemoveSubtasksFromHistory() { - Epic epic = new Epic("Epic", "Desc"); - taskManager.createEpic(epic); - - Subtask subtask = new Subtask("Sub", "Desc", Status.NEW, epic.getId()); - taskManager.createSubtask(subtask); - - taskManager.getSubtask(subtask.getId()); // добавление в историю - assertEquals(1, taskManager.getHistory().size(), - "Перед очисткой в истории должна быть 1 подзадача"); - - taskManager.clearSubtasks(); - - assertEquals(0, taskManager.getHistory().size(), - "После clearSubtasks история должна быть пустой"); - } - - // Проверка неизменности эпика по всем полям при добавлении в менеджер - @Test - void epicShouldBeUnchangedAfterAddingToManager() { - String expectedName = "Оригинальный эпик"; - String expectedDescription = "Описание эпика"; - - Epic epic = new Epic(expectedName, expectedDescription); - taskManager.createEpic(epic); - - Epic savedEpic = taskManager.getEpic(epic.getId()); - - assertEquals(expectedName, savedEpic.getName(), - "Имя эпика изменилось при сохранении"); - assertEquals(expectedDescription, savedEpic.getDescription(), - "Описание эпика изменилось при сохранении"); + @Override + protected InMemoryTaskManager createTaskManager() { + return new InMemoryTaskManager(); } } \ No newline at end of file From 3c9ecb5ac8cbcc8757241ea5b888f8f0b5e4a24e Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 22:36:36 +0300 Subject: [PATCH 14/29] test: add file-backed task manager persistence tests --- test/service/FileBackedTaskManagerTest.java | 166 +++++++++++--------- 1 file changed, 91 insertions(+), 75 deletions(-) diff --git a/test/service/FileBackedTaskManagerTest.java b/test/service/FileBackedTaskManagerTest.java index 2e2bbe1..e6cd829 100644 --- a/test/service/FileBackedTaskManagerTest.java +++ b/test/service/FileBackedTaskManagerTest.java @@ -6,95 +6,111 @@ import model.Task; import org.junit.jupiter.api.Test; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.*; -class FileBackedTaskManagerTest { +class FileBackedTaskManagerTest extends TaskManagerTest { - // Тест на сохранение и загрузку пустого менеджера - @Test - void shouldSaveAndLoadEmptyManager() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); + private File file; - try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { - writer.write("id,type,name,status,description,epic"); - writer.newLine(); + @Override + protected FileBackedTaskManager createTaskManager() { + try { + file = File.createTempFile("tasks", ".csv"); + return new FileBackedTaskManager(file); + } catch (IOException e) { + throw new RuntimeException("Не удалось создать временный файл для теста.", e); } - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - assertTrue(loadedManager.getTasks().isEmpty(), "Список задач должен быть пустым"); - assertTrue(loadedManager.getEpics().isEmpty(), "Список эпиков должен быть пустым"); - assertTrue(loadedManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым"); } - // Тест на сохранение нескольких задач + // Проверка сохранения и загрузки пустого менеджера @Test - void shouldSaveMultipleTasks() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); - - FileBackedTaskManager manager = new FileBackedTaskManager(tempFile); - - Task task = new Task("Task1", "Description1", Status.NEW); - manager.createTask(task); - - Epic epic = new Epic("Epic1", "Description epic"); - manager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask1", "Description sub", Status.NEW, epic.getId()); - manager.createSubtask(subtask); - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - assertEquals(1, loadedManager.getTasks().size(), "Должна быть 1 задача"); - assertEquals(1, loadedManager.getEpics().size(), "Должен быть 1 эпик"); - assertEquals(1, loadedManager.getSubtasks().size(), "Должна быть 1 подзадача"); + void shouldSaveAndLoadEmptyManager() { + assertDoesNotThrow(() -> { + FileBackedTaskManager manager = createTaskManager(); + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + + assertTrue(loadedManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertTrue(loadedManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(loadedManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + }, "Загрузка пустого менеджера не должна выбрасывать исключение."); } - // Тест на загрузку нескольких задач из файла + // Проверка сохранения и загрузки задачи с полями времени @Test - void shouldLoadMultipleTasks() throws IOException { - File tempFile = File.createTempFile("tasks", ".csv"); - - FileBackedTaskManager manager = new FileBackedTaskManager(tempFile); - - Task task = new Task("Task1", "Description1", Status.NEW); - manager.createTask(task); - - Epic epic = new Epic("Epic1", "Description epic"); - manager.createEpic(epic); - - Subtask subtask = new Subtask("Subtask1", "Description sub", Status.NEW, epic.getId()); - manager.createSubtask(subtask); - - subtask.setStatus(Status.DONE); - manager.updateSubtask(subtask); - - FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(tempFile); - - Task loadedTask = loadedManager.getTasks().get(0); - Epic loadedEpic = loadedManager.getEpics().get(0); - Subtask loadedSubtask = loadedManager.getSubtasks().get(0); - - assertEquals("Task1", loadedTask.getName(), "Имя задачи должно совпадать"); - assertEquals("Description1", loadedTask.getDescription(), "Описание задачи должно совпадать"); - assertEquals(Status.NEW, loadedTask.getStatus(), "Статус задачи должен совпадать"); - - assertEquals("Epic1", loadedEpic.getName(), "Имя эпика должно совпадать"); - assertEquals("Description epic", loadedEpic.getDescription(), "Описание эпика должно совпадать"); - assertEquals(Status.DONE, loadedEpic.getStatus(), "Статус эпика должен пересчитаться по подзадаче"); + void shouldSaveAndLoadTaskWithTimeFields() { + FileBackedTaskManager manager = createTaskManager(); + + Task task = new Task( + "Task", + "Task description", + Status.NEW, + Duration.ofMinutes(45), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + + Task createdTask = manager.createTask(task); + + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + Task loadedTask = loadedManager.getTask(createdTask.getId()); + + assertNotNull(loadedTask, "Задача должна загрузиться из файла."); + assertEquals(Duration.ofMinutes(45), loadedTask.getDuration(), + "Продолжительность задачи должна сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), loadedTask.getStartTime(), + "Время начала задачи должно сохраниться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 45), loadedTask.getEndTime(), + "Время окончания задачи должно корректно рассчитываться после загрузки."); + } - assertEquals("Subtask1", loadedSubtask.getName(), "Имя подзадачи должно совпадать"); - assertEquals("Description sub", loadedSubtask.getDescription(), "Описание подзадачи должно совпадать"); - assertEquals(Status.DONE, loadedSubtask.getStatus(), "Статус подзадачи должен совпадать"); - assertEquals(epic.getId(), loadedSubtask.getEpicId(), "Epic ID подзадачи должен совпадать"); + // Проверка сохранения и загрузки времени эпика через подзадачи + @Test + void shouldSaveAndLoadEpicTimeCalculatedFromSubtasks() { + FileBackedTaskManager manager = createTaskManager(); + + Epic epic = manager.createEpic(new Epic("Epic", "Epic description")); + + manager.createSubtask(new Subtask( + "Subtask 1", + "Description 1", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + manager.createSubtask(new Subtask( + "Subtask 2", + "Description 2", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0), + epic.getId() + )); + + FileBackedTaskManager loadedManager = FileBackedTaskManager.loadFromFile(file); + Epic loadedEpic = loadedManager.getEpic(epic.getId()); + + assertNotNull(loadedEpic, "Эпик должен загрузиться из файла."); + assertEquals(Duration.ofMinutes(90), loadedEpic.getDuration(), + "Продолжительность эпика должна восстановиться по подзадачам."); + assertEquals(LocalDateTime.of(2026, 4, 16, 9, 0), loadedEpic.getStartTime(), + "Время начала эпика должно восстановиться по самой ранней подзадаче."); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 30), loadedEpic.getEndTime(), + "Время окончания эпика должно восстановиться по самой поздней подзадаче."); + } - assertEquals(task.getId(), loadedTask.getId(), "ID задачи должен совпадать"); - assertEquals(epic.getId(), loadedEpic.getId(), "ID эпика должен совпадать"); - assertEquals(subtask.getId(), loadedSubtask.getId(), "ID подзадачи должен совпадать"); + // Проверка исключения при загрузке несуществующего файла + @Test + void shouldThrowExceptionWhenLoadingFromInvalidFile() { + assertThrows( + ManagerSaveException.class, + () -> FileBackedTaskManager.loadFromFile(new File("file_does_not_exist.csv")), + "Загрузка несуществующего файла должна приводить к ManagerSaveException." + ); } -} +} \ No newline at end of file From e430a7a948575cff22c6a51c9b73adf558e3371d Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 22:36:46 +0300 Subject: [PATCH 15/29] test: add history manager edge case tests --- test/service/InMemoryHistoryManagerTest.java | 203 +++++++++---------- 1 file changed, 98 insertions(+), 105 deletions(-) diff --git a/test/service/InMemoryHistoryManagerTest.java b/test/service/InMemoryHistoryManagerTest.java index 35de938..be0c84d 100644 --- a/test/service/InMemoryHistoryManagerTest.java +++ b/test/service/InMemoryHistoryManagerTest.java @@ -1,180 +1,173 @@ package service; import model.Status; +import model.Subtask; import model.Task; - -import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; class InMemoryHistoryManagerTest { - private HistoryManager historyManager; + private InMemoryHistoryManager historyManager; @BeforeEach void setUp() { historyManager = new InMemoryHistoryManager(); } - // Задачи, добавляемые в HistoryManager, сохраняют предыдущую версию задачи и её данных + // Проверка пустой истории @Test - void add() { - Task task = new Task("Task", "Task Description", Status.NEW); - task.setId(1); - - historyManager.add(task); - - final List history = historyManager.getHistory(); - - assertNotNull(history, "После добавления задачи, история не должна быть пустой."); - assertEquals(1, history.size(), "После добавления задачи, история не должна быть пустой."); + void shouldReturnEmptyHistoryWhenNoTasksWereAdded() { + List history = historyManager.getHistory(); - assertEquals(task.getName(), history.get(0).getName(), "Имя задачи в истории не совпадает"); - assertEquals(task.getDescription(), history.get(0).getDescription(), "Описание задачи в истории не совпадает"); - assertEquals(task.getStatus(), history.get(0).getStatus(), "Статус задачи в истории не совпадает"); - assertEquals(task.getId(), history.get(0).getId(), "ID задачи в истории не совпадает"); + assertNotNull(history, "История не должна быть null."); + assertTrue(history.isEmpty(), "История должна быть пустой."); } - // При повторном добавлении задачи в историю дубликат не создастся + // Проверка добавления задачи в историю @Test - void addShouldNotCreateDuplicates() { - Task task = new Task("Task", "Description", Status.NEW); + void shouldAddTaskToHistory() { + Task task = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); task.setId(1); historyManager.add(task); - historyManager.add(task); // добавляем второй раз - historyManager.add(task); // и третий - assertEquals(1, historyManager.getHistory().size(), "История не должна содержать дубликаты"); - } + List history = historyManager.getHistory(); - // История не должна ограничиваться 10 элементами - @Test - void historyShouldBeUnlimited() { - for (int i = 1; i <= 15; i++) { - Task task = new Task("Task " + i, "Description", Status.NEW); - task.setId(i); - historyManager.add(task); - } - - assertEquals(15, historyManager.getHistory().size(), "История должна хранить более 10 элементов"); + assertEquals(1, history.size(), "История должна содержать одну задачу."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна быть добавленная задача."); } - // Удаление из начала истории + // Проверка удаления дубликатов в истории @Test - void removeShouldDeleteFromBeginning() { - Task task1 = new Task("Task1", "Desc", Status.NEW); - task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); - task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); - - historyManager.add(task1); - historyManager.add(task2); - historyManager.add(task3); + void shouldKeepOnlyOneTaskWhenTaskAddedTwice() { + Task task = new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + ); + task.setId(1); - historyManager.remove(1); // удаление первой - головы + historyManager.add(task); + historyManager.add(task); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); + + assertEquals(1, history.size(), "Дубликаты не должны сохраняться в истории."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна остаться одна задача."); } - // Удаление из середины истории + // Проверка удаления задачи из начала истории @Test - void removeShouldDeleteFromMiddle() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromBeginningOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); historyManager.add(task1); historyManager.add(task2); - historyManager.add(task3); - historyManager.remove(2); // удаление из середины + historyManager.remove(1); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(1, history.get(0).getId(), "Первой должна быть task1"); - assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); + + assertEquals(1, history.size(), "После удаления в истории должна остаться одна задача."); + assertEquals(2, history.get(0).getId(), "В истории должна остаться вторая задача."); } - // Удаление из конца истории + // Проверка удаления задачи из середины истории @Test - void removeShouldDeleteFromEnd() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromMiddleOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); + + Task task3 = new Task("Task 3", "Description 3", Status.NEW, + Duration.ofMinutes(30), LocalDateTime.of(2026, 4, 16, 11, 0)); task3.setId(3); historyManager.add(task1); historyManager.add(task2); historyManager.add(task3); - historyManager.remove(3); // удаление последней - хвоста + historyManager.remove(2); List history = historyManager.getHistory(); - assertEquals(2, history.size(), "В истории должно остаться 2 задачи"); - assertEquals(2, history.get(1).getId(), "Последней должна быть task2"); + + assertEquals(2, history.size(), "После удаления в истории должно остаться две задачи."); + assertEquals(1, history.get(0).getId(), "Первая задача должна остаться."); + assertEquals(3, history.get(1).getId(), "Третья задача должна остаться."); } - // Повторный просмотр задачи должен переместить её в конец истории + // Проверка удаления задачи из конца истории @Test - void repeatedViewShouldMoveTaskToEnd() { - Task task1 = new Task("Task1", "Desc", Status.NEW); + void shouldRemoveTaskFromEndOfHistory() { + Task task1 = new Task("Task 1", "Description 1", Status.NEW, + Duration.ofMinutes(10), LocalDateTime.of(2026, 4, 16, 9, 0)); task1.setId(1); - Task task2 = new Task("Task2", "Desc", Status.NEW); + + Task task2 = new Task("Task 2", "Description 2", Status.NEW, + Duration.ofMinutes(20), LocalDateTime.of(2026, 4, 16, 10, 0)); task2.setId(2); - Task task3 = new Task("Task3", "Desc", Status.NEW); - task3.setId(3); historyManager.add(task1); historyManager.add(task2); - historyManager.add(task3); - historyManager.add(task1); // task1 повторно — должна уйти в конец - List history = historyManager.getHistory(); - - assertEquals(3, history.size(), "Дубликатов быть не должно"); - assertEquals(2, history.get(0).getId(), "Первой должна быть task2"); - assertEquals(3, history.get(1).getId(), "Второй должна быть task3"); - assertEquals(1, history.get(2).getId(), "Последней должна быть task1"); - } + historyManager.remove(2); - // Пустая история должна возвращать пустой список - @Test - void emptyHistoryShouldReturnEmptyList() { List history = historyManager.getHistory(); - assertNotNull(history, "getHistory() не должен возвращать null"); - assertEquals(0, history.size(), "Пустая история должна содержать 0 элементов"); + + assertEquals(1, history.size(), "После удаления в истории должна остаться одна задача."); + assertEquals(1, history.get(0).getId(), "В истории должна остаться первая задача."); } - // Статус задачи в истории не должен меняться при изменении оригинала через сеттер + // Проверка сохранения новых полей во snapshot истории @Test - void historyShouldStoreSnapshotNotReference() { - Task task = new Task("Task", "Desc", Status.NEW); - task.setId(1); + void shouldSaveTimeFieldsInHistorySnapshot() { + Subtask subtask = new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(40), + LocalDateTime.of(2026, 4, 16, 13, 0), + 100 + ); + subtask.setId(1); + + historyManager.add(subtask); - historyManager.add(task); + List history = historyManager.getHistory(); - // Меняем оригинал после добавления в историю - task.setName("Изменённое имя"); - task.setStatus(Status.DONE); + assertEquals(1, history.size(), "История должна содержать одну подзадачу."); + Task savedTask = history.get(0); - Task inHistory = historyManager.getHistory().get(0); - assertEquals("Task", inHistory.getName(), - "История должна хранить снимок: имя не должно измениться"); - assertEquals(Status.NEW, inHistory.getStatus(), - "История должна хранить снимок: статус не должен измениться"); + assertEquals(Duration.ofMinutes(40), savedTask.getDuration(), + "Продолжительность должна сохраниться в snapshot истории."); + assertEquals(LocalDateTime.of(2026, 4, 16, 13, 0), savedTask.getStartTime(), + "Время начала должно сохраниться в snapshot истории."); + assertEquals(LocalDateTime.of(2026, 4, 16, 13, 40), savedTask.getEndTime(), + "Время окончания должно корректно рассчитываться в snapshot истории."); } } \ No newline at end of file From 7cc4bc5ec697c96326009a3277e8b1c7e11c534d Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 23:43:57 +0300 Subject: [PATCH 16/29] fix: preserve calculated fields in Epic during update --- src/service/InMemoryTaskManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 00f6d45..ab87a66 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -272,6 +272,9 @@ public void updateEpic(Epic epic) { Epic oldEpic = epics.get(epic.getId()); epic.setSubtaskIds(oldEpic.getSubtaskIds()); epic.setStatus(oldEpic.getStatus()); + epic.setEpicDuration(oldEpic.getDuration()); + epic.setEpicStartTime(oldEpic.getStartTime()); + epic.setEpicEndTime(oldEpic.getEndTime()); epics.put(epic.getId(), epic); } From 727eee05942e6d5833d302351cbeda19dacd3d22 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 23:55:41 +0300 Subject: [PATCH 17/29] fix: prevent creating subtask without existing epic --- src/service/InMemoryTaskManager.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index ab87a66..cf8b99d 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -227,6 +227,11 @@ public Epic createEpic(Epic epic) { @Override public Subtask createSubtask(Subtask subtask) { + Epic epic = epics.get(subtask.getEpicId()); + if (epic == null) { + return null; + } + if (hasTimeOverlap(subtask)) { throw new IllegalArgumentException("Подзадача пересекается по времени с другой задачей."); } @@ -236,13 +241,9 @@ public Subtask createSubtask(Subtask subtask) { subtasks.put(subtask.getId(), subtask); addTaskToPrioritizedTasks(subtask); - // Добавляем в эпик id новой подзадачи - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - epic.addSubtaskId(subtask.getId()); - updateEpicStatus(epic); - updateEpicTime(epic); - } + epic.addSubtaskId(subtask.getId()); + updateEpicStatus(epic); + updateEpicTime(epic); return subtask; } From 9f37d136ccaf63edf3f08affcd27fedf422f72c2 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 23:56:14 +0300 Subject: [PATCH 18/29] fix: validate epic existence in updateSubtask() --- src/service/InMemoryTaskManager.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index cf8b99d..8090c90 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -344,6 +344,11 @@ protected void updateEpicTime(Epic epic) { @Override public void updateSubtask(Subtask subtask) { if (subtasks.containsKey(subtask.getId())) { + Epic epic = epics.get(subtask.getEpicId()); + if (epic == null) { + return; + } + Subtask oldSubtask = subtasks.get(subtask.getId()); prioritizedTasks.remove(oldSubtask); @@ -356,12 +361,8 @@ public void updateSubtask(Subtask subtask) { subtasks.put(subtask.getId(), subtask); addTaskToPrioritizedTasks(subtask); - // Пересчёт статуса эпика после обновления подзадачи - Epic epic = epics.get(subtask.getEpicId()); - if (epic != null) { - updateEpicStatus(epic); - updateEpicTime(epic); - } + updateEpicStatus(epic); + updateEpicTime(epic); } } From d03d01759539d18f681508a6050a6ea71142fb93 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Thu, 16 Apr 2026 23:59:33 +0300 Subject: [PATCH 19/29] refactor: preserve task and subtask status on creation --- src/service/InMemoryTaskManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 8090c90..ab0b04a 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -210,7 +210,6 @@ public Task createTask(Task task) { } task.setId(getNextId()); - task.setStatus(Status.NEW); tasks.put(task.getId(), task); addTaskToPrioritizedTasks(task); From 3c5bf447f98544af749706ede81d1fcdca47b968 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:02:12 +0300 Subject: [PATCH 20/29] refactor: improve safety in getEpicSubtasks() --- src/service/InMemoryTaskManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index ab0b04a..18f4bd9 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -419,6 +419,7 @@ public List getEpicSubtasks(int epicId) { return epic.getSubtaskIds().stream() .map(subtasks::get) + .filter(subtask -> subtask != null) .toList(); } From f0c8de0767eb34dff4c357a46b3e37a044fe557d Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:08:50 +0300 Subject: [PATCH 21/29] test: add tests for clear operations in TaskManager --- test/service/TaskManagerTest.java | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index 7fdde9d..6271db1 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -305,4 +305,75 @@ void shouldCalculateEpicTimeFromSubtasks() { assertEquals(LocalDateTime.of(2026, 4, 16, 12, 30), savedEpic.getEndTime(), "Время окончания эпика должно быть временем окончания самой поздней подзадачи."); } + + // Проверка очистки всех задач + @Test + void shouldClearAllTasks() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + taskManager.getTask(task.getId()); + taskManager.clearTasks(); + + assertTrue(taskManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertNull(taskManager.getTask(task.getId()), "Задача не должна находиться после очистки."); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться от удалённых задач."); + assertTrue(taskManager.getPrioritizedTasks().isEmpty(), "Приоритетный список должен очищаться."); + } + + // Проверка очистки всех подзадач и сброса полей эпика + @Test + void shouldClearAllSubtasksAndResetEpicFields() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.clearSubtasks(); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + assertTrue(savedEpic.getSubtaskIds().isEmpty(), "У эпика не должно остаться подзадач."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус эпика должен стать NEW."); + assertNull(savedEpic.getStartTime(), "Время начала эпика должно стать null."); + assertNull(savedEpic.getEndTime(), "Время окончания эпика должно стать null."); + assertNull(savedEpic.getDuration(), "Продолжительность эпика должна стать null."); + } + + // Проверка очистки всех эпиков и связанных подзадач + @Test + void shouldClearAllEpicsAndSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 11, 0), + epic.getId() + )); + + taskManager.getEpic(epic.getId()); + taskManager.getSubtask(subtask.getId()); + + taskManager.clearEpics(); + + assertTrue(taskManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + assertNull(taskManager.getEpic(epic.getId()), "Эпик не должен находиться после очистки."); + assertNull(taskManager.getSubtask(subtask.getId()), "Подзадача не должна находиться после очистки."); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться."); + } } \ No newline at end of file From ea4d86150f9ac336de3a6e10b1133b11020c2307 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:12:10 +0300 Subject: [PATCH 22/29] test: add tests for update operations in TaskManager --- test/service/TaskManagerTest.java | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index 6271db1..5a48f76 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -376,4 +376,101 @@ void shouldClearAllEpicsAndSubtasks() { assertNull(taskManager.getSubtask(subtask.getId()), "Подзадача не должна находиться после очистки."); assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться."); } + + // Проверка обновления задач, подзадач и эпиков + @Test + void shouldUpdateTask() { + Task task = taskManager.createTask(new Task( + "Old name", + "Old description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + Task updatedTask = new Task( + "New name", + "New description", + Status.IN_PROGRESS, + Duration.ofMinutes(45), + LocalDateTime.of(2026, 4, 16, 12, 0) + ); + updatedTask.setId(task.getId()); + + taskManager.updateTask(updatedTask); + + Task savedTask = taskManager.getTask(task.getId()); + + assertEquals("New name", savedTask.getName()); + assertEquals("New description", savedTask.getDescription()); + assertEquals(Status.IN_PROGRESS, savedTask.getStatus()); + assertEquals(Duration.ofMinutes(45), savedTask.getDuration()); + assertEquals(LocalDateTime.of(2026, 4, 16, 12, 0), savedTask.getStartTime()); + } + + @Test + void shouldUpdateEpicWithoutLosingCalculatedFields() { + Epic epic = taskManager.createEpic(new Epic("Old epic", "Old description")); + + taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + Epic updatedEpic = new Epic("New epic", "New description"); + updatedEpic.setId(epic.getId()); + + taskManager.updateEpic(updatedEpic); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals("New epic", savedEpic.getName()); + assertEquals("New description", savedEpic.getDescription()); + assertEquals(1, savedEpic.getSubtaskIds().size(), "Связь с подзадачами должна сохраниться."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус не должен теряться."); + assertEquals(Duration.ofMinutes(30), savedEpic.getDuration(), "Продолжительность не должна теряться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), savedEpic.getStartTime(), "StartTime не должен теряться."); + assertEquals(LocalDateTime.of(2026, 4, 16, 10, 30), savedEpic.getEndTime(), "EndTime не должен теряться."); + } + + @Test + void shouldUpdateSubtaskAndRecalculateEpic() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Old subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 9, 0), + epic.getId() + )); + + Subtask updatedSubtask = new Subtask( + "New subtask", + "New description", + Status.DONE, + Duration.ofMinutes(40), + LocalDateTime.of(2026, 4, 16, 11, 0), + epic.getId() + ); + updatedSubtask.setId(subtask.getId()); + + taskManager.updateSubtask(updatedSubtask); + + Subtask savedSubtask = taskManager.getSubtask(subtask.getId()); + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertEquals("New subtask", savedSubtask.getName()); + assertEquals(Status.DONE, savedSubtask.getStatus()); + assertEquals(Duration.ofMinutes(40), savedSubtask.getDuration()); + assertEquals(LocalDateTime.of(2026, 4, 16, 11, 0), savedSubtask.getStartTime()); + + assertEquals(Status.DONE, savedEpic.getStatus(), "Статус эпика должен пересчитаться."); + assertEquals(Duration.ofMinutes(40), savedEpic.getDuration(), "Продолжительность эпика должна пересчитаться."); + } } \ No newline at end of file From 1f552a06acb070291b0c7be251173dba47327287 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:14:09 +0300 Subject: [PATCH 23/29] test: add tests for delete operations --- test/service/TaskManagerTest.java | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index 5a48f76..a2484fd 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -473,4 +473,67 @@ void shouldUpdateSubtaskAndRecalculateEpic() { assertEquals(Status.DONE, savedEpic.getStatus(), "Статус эпика должен пересчитаться."); assertEquals(Duration.ofMinutes(40), savedEpic.getDuration(), "Продолжительность эпика должна пересчитаться."); } + + // // Проверка удаления задач, подзадач и эпиков + @Test + void shouldDeleteTaskById() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + taskManager.getTask(task.getId()); + taskManager.deleteTask(task.getId()); + + assertNull(taskManager.getTask(task.getId()), "Задача должна быть удалена."); + assertTrue(taskManager.getTasks().isEmpty(), "Список задач должен быть пустым."); + assertTrue(taskManager.getHistory().isEmpty(), "История должна очищаться от удалённой задачи."); + } + + @Test + void shouldDeleteSubtaskAndUpdateEpic() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.deleteSubtask(subtask.getId()); + + Epic savedEpic = taskManager.getEpic(epic.getId()); + + assertNull(taskManager.getSubtask(subtask.getId()), "Подзадача должна быть удалена."); + assertTrue(savedEpic.getSubtaskIds().isEmpty(), "У эпика не должно остаться подзадач."); + assertEquals(Status.NEW, savedEpic.getStatus(), "Статус эпика должен пересчитаться."); + assertNull(savedEpic.getDuration(), "Продолжительность эпика должна стать null."); + } + + @Test + void shouldDeleteEpicWithItsSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + taskManager.deleteEpic(epic.getId()); + + assertNull(taskManager.getEpic(epic.getId()), "Эпик должен быть удалён."); + assertNull(taskManager.getSubtask(subtask.getId()), "Подзадача эпика тоже должна быть удалена."); + assertTrue(taskManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); + } } \ No newline at end of file From 7b131348aa49ed841134d9bdd535119e709aecef Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:17:31 +0300 Subject: [PATCH 24/29] test: add edge case tests for entity lookup in TaskManager --- test/service/TaskManagerTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index a2484fd..c6cf150 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -536,4 +536,20 @@ void shouldDeleteEpicWithItsSubtasks() { assertTrue(taskManager.getEpics().isEmpty(), "Список эпиков должен быть пустым."); assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен быть пустым."); } + + // Проверка граничных случаев поиска задач, эпиков и подзадач + @Test + void shouldReturnNullWhenTaskNotFound() { + assertNull(taskManager.getTask(999), "Несуществующая задача должна возвращать null."); + } + + @Test + void shouldReturnNullWhenEpicNotFound() { + assertNull(taskManager.getEpic(999), "Несуществующий эпик должен возвращать null."); + } + + @Test + void shouldReturnNullWhenSubtaskNotFound() { + assertNull(taskManager.getSubtask(999), "Несуществующая подзадача должна возвращать null."); + } } \ No newline at end of file From 74db52789a8eed83f44836f6c930b57a662e0cdc Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:21:29 +0300 Subject: [PATCH 25/29] test: add history and subtask validation tests in TaskManager --- test/service/TaskManagerTest.java | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index c6cf150..c6787b4 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -552,4 +552,42 @@ void shouldReturnNullWhenEpicNotFound() { void shouldReturnNullWhenSubtaskNotFound() { assertNull(taskManager.getSubtask(999), "Несуществующая подзадача должна возвращать null."); } + + // Проверка добавления задач в историю просмотров + @Test + void shouldAddViewedTaskToHistory() { + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(15), + LocalDateTime.of(2026, 4, 16, 8, 0) + )); + + taskManager.getTask(task.getId()); + + List history = taskManager.getHistory(); + + assertEquals(1, history.size(), "После просмотра задача должна попасть в историю."); + assertEquals(task.getId(), history.get(0).getId(), "В истории должна быть просмотренная задача."); + } + + // Проверка создания подзадачи без существующего эпика + @Test + void shouldNotCreateSubtaskWithoutExistingEpic() { + Subtask subtask = new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + 999 + ); + + Subtask createdSubtask = taskManager.createSubtask(subtask); + + assertNull(createdSubtask, "Подзадача не должна создаваться без существующего эпика."); + assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен остаться пустым."); + } + } \ No newline at end of file From fac189f8eb58797958be11ef61cedfc1030f5544 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:22:58 +0300 Subject: [PATCH 26/29] test: add overlap validation tests for tasks and subtasks --- test/service/TaskManagerTest.java | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index c6787b4..dbf2b9a 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -590,4 +590,45 @@ void shouldNotCreateSubtaskWithoutExistingEpic() { assertTrue(taskManager.getSubtasks().isEmpty(), "Список подзадач должен остаться пустым."); } + // Проверка пересечения временных интервалов задач и подзадач + @Test + void shouldAllowTasksThatTouchBordersButDoNotOverlap() { + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertDoesNotThrow(() -> taskManager.createTask(new Task( + "Task 2", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 11, 0) + )), "Задачи, соприкасающиеся границами, не должны считаться пересекающимися."); + } + + @Test + void shouldThrowExceptionWhenCreatingOverlappingSubtask() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + taskManager.createTask(new Task( + "Task 1", + "Description", + Status.NEW, + Duration.ofMinutes(60), + LocalDateTime.of(2026, 4, 16, 10, 0) + )); + + assertThrows(IllegalArgumentException.class, () -> taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 30), + epic.getId() + )), "Подзадача не должна создаваться, если пересекается по времени с другой задачей."); + } } \ No newline at end of file From c059c8554af15066c42a3a22c94009877fed9790 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:28:28 +0300 Subject: [PATCH 27/29] fix: preserve epic connection when updating subtask --- src/service/InMemoryTaskManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 18f4bd9..81497e9 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -343,13 +343,15 @@ protected void updateEpicTime(Epic epic) { @Override public void updateSubtask(Subtask subtask) { if (subtasks.containsKey(subtask.getId())) { + Subtask oldSubtask = subtasks.get(subtask.getId()); + + subtask.setEpicId(oldSubtask.getEpicId()); + Epic epic = epics.get(subtask.getEpicId()); if (epic == null) { return; } - Subtask oldSubtask = subtasks.get(subtask.getId()); - prioritizedTasks.remove(oldSubtask); if (hasTimeOverlap(subtask)) { From 1e5ee6f5559ac34940f31e2125567cd4889e8a0c Mon Sep 17 00:00:00 2001 From: Ksenia Date: Fri, 17 Apr 2026 00:35:56 +0300 Subject: [PATCH 28/29] test: add tests for epic subtasks and prioritized tasks --- test/service/TaskManagerTest.java | 48 ++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/test/service/TaskManagerTest.java b/test/service/TaskManagerTest.java index dbf2b9a..2452b28 100644 --- a/test/service/TaskManagerTest.java +++ b/test/service/TaskManagerTest.java @@ -474,7 +474,53 @@ void shouldUpdateSubtaskAndRecalculateEpic() { assertEquals(Duration.ofMinutes(40), savedEpic.getDuration(), "Продолжительность эпика должна пересчитаться."); } - // // Проверка удаления задач, подзадач и эпиков + // Проверка получения списка подзадач эпика + @Test + void shouldReturnEmptyListForEpicWithoutSubtasks() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + List epicSubtasks = taskManager.getEpicSubtasks(epic.getId()); + + assertTrue(epicSubtasks.isEmpty(), "У нового эпика список подзадач должен быть пустым."); + } + + @Test + void shouldReturnEmptyListForNonExistingEpicSubtasks() { + List epicSubtasks = taskManager.getEpicSubtasks(999); + + assertTrue(epicSubtasks.isEmpty(), "Для несуществующего эпика должен возвращаться пустой список."); + } + + // Проверка сортировки задач и подзадач в приоритетном списке + @Test + void shouldReturnTasksAndSubtasksSortedByStartTime() { + Epic epic = taskManager.createEpic(new Epic("Epic", "Description")); + + Task task = taskManager.createTask(new Task( + "Task", + "Description", + Status.NEW, + Duration.ofMinutes(30), + LocalDateTime.of(2026, 4, 16, 12, 0) + )); + + Subtask subtask = taskManager.createSubtask(new Subtask( + "Subtask", + "Description", + Status.NEW, + Duration.ofMinutes(20), + LocalDateTime.of(2026, 4, 16, 10, 0), + epic.getId() + )); + + List prioritizedTasks = taskManager.getPrioritizedTasks(); + + assertEquals(2, prioritizedTasks.size(), "В приоритетном списке должны быть задача и подзадача."); + assertEquals(subtask.getId(), prioritizedTasks.get(0).getId(), "Подзадача с более ранним startTime должна быть первой."); + assertEquals(task.getId(), prioritizedTasks.get(1).getId(), "Задача с более поздним startTime должна быть второй."); + } + + // Проверка удаления задач, подзадач и эпиков @Test void shouldDeleteTaskById() { Task task = taskManager.createTask(new Task( From 1eb78bb37b54be31cc16ea51990cb04c002e6a52 Mon Sep 17 00:00:00 2001 From: Ksenia Date: Sat, 18 Apr 2026 00:06:21 +0300 Subject: [PATCH 29/29] refactor: move null time validation to hasTimeOverlap --- src/service/InMemoryTaskManager.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/service/InMemoryTaskManager.java b/src/service/InMemoryTaskManager.java index 81497e9..0a955cb 100644 --- a/src/service/InMemoryTaskManager.java +++ b/src/service/InMemoryTaskManager.java @@ -176,23 +176,21 @@ public List getPrioritizedTasks() { // Проверка пересечения двух задач по времени выполнения protected boolean isTaskOverlapping(Task firstTask, Task secondTask) { - if (firstTask.getStartTime() == null || secondTask.getStartTime() == null) { - return false; - } - - if (firstTask.getEndTime() == null || secondTask.getEndTime() == null) { - return false; - } - return firstTask.getStartTime().isBefore(secondTask.getEndTime()) && secondTask.getStartTime().isBefore(firstTask.getEndTime()); } // Проверка пересечения задачи с уже существующими задачами и подзадачами protected boolean hasTimeOverlap(Task task) { + if (task.getStartTime() == null || task.getEndTime() == null) { + return false; + } + return prioritizedTasks.stream() - .anyMatch(prioritizedTask -> prioritizedTask.getId() != task.getId() - && isTaskOverlapping(task, prioritizedTask)); + .anyMatch(prioritizedTask -> + prioritizedTask.getId() != task.getId() + && isTaskOverlapping(task, prioritizedTask) + ); } // Добавление задачи или подзадачи в список приоритетов, если задано время начала