From c22a8f82f0ded57f655b623aa79f592ea1fb6612 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Fri, 12 Jan 2024 13:56:06 +0700 Subject: [PATCH] Fully implemented LeastPickedPicker --- .../picker/config/TestPickersConfig.java | 64 +++++++ .../repository/impl/NodeRepositoryImpl.java | 6 +- .../repository/impl/PickerRepository.java | 17 +- .../repository/impl/UserRepositoryImpl.java | 17 ++ .../impl/cache/NodeId2PickerModeCache.java | 25 +++ .../impl/collection/DynamicSortedMap.java | 179 ++++++++++++++++++ .../impl/picker/LeastPickedPicker.java | 38 +++- .../repository/impl/picker/RoomPicker.java | 10 + .../repository/impl/picker/RoomWrapper.java | 27 ++- .../impl/picker/RoundRobinPicker.java | 8 +- .../impl/picker/SequentialFillingPicker.java | 8 +- .../collection/DynamicSortedMapTests.java | 103 ++++++++++ .../collection/QueuedLinkedListTests.java | 15 ++ 13 files changed, 500 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/ru/dragonestia/picker/config/TestPickersConfig.java create mode 100644 app/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java create mode 100644 app/src/main/java/ru/dragonestia/picker/repository/impl/collection/DynamicSortedMap.java create mode 100644 app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java create mode 100644 app/src/test/java/ru/dragonestia/picker/collection/DynamicSortedMapTests.java diff --git a/app/src/main/java/ru/dragonestia/picker/config/TestPickersConfig.java b/app/src/main/java/ru/dragonestia/picker/config/TestPickersConfig.java new file mode 100644 index 0000000..2191821 --- /dev/null +++ b/app/src/main/java/ru/dragonestia/picker/config/TestPickersConfig.java @@ -0,0 +1,64 @@ +package ru.dragonestia.picker.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import ru.dragonestia.picker.model.Node; +import ru.dragonestia.picker.model.Room; +import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; +import ru.dragonestia.picker.model.type.SlotLimit; +import ru.dragonestia.picker.repository.NodeRepository; +import ru.dragonestia.picker.repository.RoomRepository; +import ru.dragonestia.picker.repository.UserRepository; + +import java.util.List; + +@Profile("test_pickers") +@Configuration +@RequiredArgsConstructor +public class TestPickersConfig { + + private final NodeRepository nodeRepository; + private final RoomRepository roomRepository; + private final UserRepository userRepository; + + @Bean + void createSequentialFillingNode() { + var node = new Node("seq", PickingMode.SEQUENTIAL_FILLING); + nodeRepository.create(node); + + fillNode(node); + } + + @Bean + void createRoundRobinNode() { + var node = new Node("round", PickingMode.ROUND_ROBIN); + nodeRepository.create(node); + + fillNode(node); + } + + @Bean + void createLeastPickerNode() { + var node = new Node("least", PickingMode.LEAST_PICKED); + nodeRepository.create(node); + + fillNode(node); + } + + private void fillNode(Node node) { + for (int i = 0, n = 5; i < n; i++) { + for (int j = 0; j < 3; j++) { + var room = Room.create("room-" + i + "-" + j, node, SlotLimit.of(n), ""); + roomRepository.create(room); + + for (int k = n - i - 1; k >= 0; k--) { + var user = new User("user-" + k); + userRepository.linkWithRoom(room, List.of(user), false); + } + } + } + } +} diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java index a9c7eb6..78cc845 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.NodeRepository; +import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache; import java.util.List; import java.util.Map; @@ -17,6 +18,7 @@ public class NodeRepositoryImpl implements NodeRepository { private final RoomRepository roomRepository; private final PickerRepository pickerRepository; + private final NodeId2PickerModeCache nodeId2PickerModeCache; private final Map nodeMap = new ConcurrentHashMap<>(); @Override @@ -27,7 +29,8 @@ public class NodeRepositoryImpl implements NodeRepository { } nodeMap.put(node.id(), node); - pickerRepository.create(node.id(), node.mode()); + var picker = pickerRepository.create(node.id(), node.mode()); + nodeId2PickerModeCache.put(node.id(), picker); } roomRepository.onCreateNode(node); @@ -38,6 +41,7 @@ public class NodeRepositoryImpl implements NodeRepository { synchronized (nodeMap) { nodeMap.remove(node.id()); pickerRepository.remove(node.id()); + nodeId2PickerModeCache.remove(node.id()); } roomRepository.onRemoveNode(node); diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java index 0001e1f..62aa45c 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java @@ -6,10 +6,7 @@ import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.type.PickingMode; import ru.dragonestia.picker.repository.UserRepository; -import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker; -import ru.dragonestia.picker.repository.impl.picker.Picker; -import ru.dragonestia.picker.repository.impl.picker.RoundRobinPicker; -import ru.dragonestia.picker.repository.impl.picker.SequentialFillingPicker; +import ru.dragonestia.picker.repository.impl.picker.*; import java.security.InvalidParameterException; import java.util.Collection; @@ -21,17 +18,19 @@ import java.util.concurrent.ConcurrentHashMap; public class PickerRepository { private final UserRepository userRepository; - private final Map> pickers = new ConcurrentHashMap<>(); + private final Map pickers = new ConcurrentHashMap<>(); - public void create(String nodeId, PickingMode mode) { - pickers.put(nodeId, of(mode)); + public RoomPicker create(String nodeId, PickingMode mode) { + var picker = of(mode); + pickers.put(nodeId, picker); + return picker; } public void remove(String nodeId) { pickers.remove(nodeId); } - public Picker find(String nodeId) { + public RoomPicker find(String nodeId) { return pickers.get(nodeId); } @@ -39,7 +38,7 @@ public class PickerRepository { return pickers.get(nodeId).pick(users); } - private Picker of(PickingMode mode) { + private RoomPicker of(PickingMode mode) { switch (mode) { case SEQUENTIAL_FILLING -> { return new SequentialFillingPicker(userRepository); diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java index 837ae15..1d35858 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java @@ -1,17 +1,24 @@ package ru.dragonestia.picker.repository.impl; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; +import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.UserRepository; +import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache; +import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @Repository +@RequiredArgsConstructor public class UserRepositoryImpl implements UserRepository { + private final NodeId2PickerModeCache nodeId2PickerModeCache; private final Map> usersMap = new ConcurrentHashMap<>(); private final Map> roomUsers = new ConcurrentHashMap<>(); @@ -44,6 +51,11 @@ public class UserRepositoryImpl implements UserRepository { usersSet.addAll(users); roomUsers.put(path, usersSet); + + var picker = nodeId2PickerModeCache.get(room.getNodeId()); + if (picker instanceof LeastPickedPicker leastPickedPicker) { + leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size()); + } } return result; @@ -72,6 +84,11 @@ public class UserRepositoryImpl implements UserRepository { } else { roomUsers.put(path, set); } + + var picker = nodeId2PickerModeCache.get(room.getNodeId()); + if (picker instanceof LeastPickedPicker leastPickedPicker) { + leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size()); + } } return counter.get(); } diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java new file mode 100644 index 0000000..4e1a005 --- /dev/null +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java @@ -0,0 +1,25 @@ +package ru.dragonestia.picker.repository.impl.cache; + +import org.springframework.stereotype.Component; +import ru.dragonestia.picker.repository.impl.picker.RoomPicker; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class NodeId2PickerModeCache { + + private final Map cache = new ConcurrentHashMap<>(); + + public void put(String nodeId, RoomPicker picker) { + cache.put(nodeId, picker); + } + + public void remove(String nodeId) { + cache.remove(nodeId); + } + + public RoomPicker get(String nodeId) { + return cache.get(nodeId); + } +} diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/collection/DynamicSortedMap.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/collection/DynamicSortedMap.java new file mode 100644 index 0000000..625c545 --- /dev/null +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/collection/DynamicSortedMap.java @@ -0,0 +1,179 @@ +package ru.dragonestia.picker.repository.impl.collection; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +public class DynamicSortedMap { + + private final TreeMap>> tree = new TreeMap<>(); + private final HashMap> indexes = new HashMap<>(); + + public void put(ITEM item) { + var node = new Node<>(item, room -> { + // TODO: update map state + }); + + indexes.put(node.object.getId(), node); + var list = tree.getOrDefault(node.cachedScore, new LinkedList<>()); + if (list.isEmpty()) tree.put(node.cachedScore, list); + list.add(node); + } + + public void removeById(String id) { + if (!indexes.containsKey(id)) return; + + remove(indexes.get(id).object); + } + + public void remove(ITEM item) { + if (!indexes.containsKey(item.getId())) return; + + var node = indexes.get(item.getId()); + node.removed = true; + indexes.remove(item.getId()); + + var list = tree.get(node.cachedScore); + list.remove(node); + if (list.isEmpty()) tree.remove(node.cachedScore); + } + + public int size() { + return indexes.size(); + } + + public ITEM getMinimum() { + return getMinimum(0); + } + + public ITEM getMinimum(int needAppendScore) { + if (size() == 0) { + throw new RuntimeException("Map is empty"); + } + + ITEM result = null; + rootLoop: + for (var index: tree.navigableKeySet()) { + var list = tree.get(index); + for (var node: list) { + if (!node.object.canBeUsed(needAppendScore)) continue; + + result = node.object; + break rootLoop; + } + } + + if (result == null) { + throw new RuntimeException("Cant get available item"); + } + return result; + } + + public ITEM getMaximum() { + if (size() == 0) { + throw new RuntimeException("Map is empty"); + } + + ITEM result = null; + rootLoop: + for (var index: tree.descendingKeySet()) { + var list = tree.get(index); + for (var node: list) { + if (!node.object.canBeUsed(0)) continue; + + result = node.object; + break rootLoop; + } + } + + if (result == null) { + throw new RuntimeException("Cant get available item"); + } + return result; + } + + public Set> getItems() { + var set = new LinkedHashSet>(); + for (var index: tree.navigableKeySet()) { + set.addAll(tree.get(index)); + } + return set; + } + + public Node getNode(String id) { + return indexes.get(id); + } + + public void updateItem(String id, Function setter) { + var node = indexes.get(id); + + var prevScore = node.cachedScore; + node.object.updateScore(setter.apply(node.cachedScore)); + var newScore = node.cachedScore; + + var prevList = tree.get(prevScore); + prevList.remove(node); + if (prevList.isEmpty()) { + tree.remove(prevScore); + } + + var newList = tree.getOrDefault(newScore, new LinkedList<>()); + if (newList.isEmpty()) tree.put(newScore, newList); + newList.add(node); + } + + public static class Node { + + private final ITEM object; + private int cachedScore; + private boolean removed = false; + + private Node(ITEM object, Consumer onUpdate) { + this.object = object; + cachedScore = object.getScore(); + + object.setOnUpdateScore(newScore -> { + cachedScore = newScore; + onUpdate.accept(object); + }); + } + + public int getScore() { + return cachedScore; + } + + @Override + public String toString() { + return "{Node id=%s, score=%s, removed=%s }".formatted( + object.getId(), + getScore(), + removed + ); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (obj instanceof DynamicSortedMap.Node node) { + return object.getId().equals(node.object.getId()); + } + return false; + } + } + + public interface Item { + + String getId(); + + int getScore(); + + void updateScore(int value); + + void setOnUpdateScore(Consumer setter); + + default boolean canBeUsed(int units) { + return true; + } + } +} diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java index 02a1dac..42aea26 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java @@ -2,13 +2,16 @@ package ru.dragonestia.picker.repository.impl.picker; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; import ru.dragonestia.picker.repository.UserRepository; +import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap; import java.util.Collection; -public class LeastPickedPicker implements Picker { +public class LeastPickedPicker implements RoomPicker { private final UserRepository userRepository; + private final DynamicSortedMap map = new DynamicSortedMap<>(); public LeastPickedPicker(UserRepository userRepository) { this.userRepository = userRepository; @@ -16,16 +19,43 @@ public class LeastPickedPicker implements Picker { @Override public void add(Room room) { - throw new UnsupportedOperationException("Not implemented"); + synchronized (map) { + map.put(new RoomWrapper(room, () -> userRepository.usersOf(room).size())); + } } @Override public void remove(Room room) { - throw new UnsupportedOperationException("Not implemented"); + synchronized (map) { + map.removeById(room.getId()); + } } @Override public Room pick(Collection users) { - throw new UnsupportedOperationException("Not implemented"); + RoomWrapper wrapper; + + synchronized (map) { + try { + wrapper = map.getMinimum(); + + if (wrapper.isFull()) throw new RuntimeException(); + } catch (RuntimeException ex) { + throw new RuntimeException("There are no rooms available"); + } + } + + return wrapper.getItem(); + } + + public void updateUsersAmount(Room room, int users) { + synchronized (map) { + map.updateItem(room.getId(), prevValue -> users); + } + } + + @Override + public PickingMode getPickingMode() { + return PickingMode.LEAST_PICKED; } } diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java new file mode 100644 index 0000000..9367af8 --- /dev/null +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java @@ -0,0 +1,10 @@ +package ru.dragonestia.picker.repository.impl.picker; + +import ru.dragonestia.picker.model.Room; +import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; + +public interface RoomPicker extends Picker { + + PickingMode getPickingMode(); +} diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java index c8093e0..d58edc4 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java @@ -2,13 +2,16 @@ package ru.dragonestia.picker.repository.impl.picker; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList; +import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap; +import java.util.function.Consumer; import java.util.function.Supplier; -public class RoomWrapper implements ItemWrapper, QueuedLinkedList.Item { +public class RoomWrapper implements ItemWrapper, QueuedLinkedList.Item, DynamicSortedMap.Item { private final Room room; private final Supplier userCountSupplier; + private Consumer setter; public RoomWrapper(Room room, Supplier userCountSupplier) { this.room = room; @@ -39,4 +42,26 @@ public class RoomWrapper implements ItemWrapper, QueuedLinkedList.Item { public boolean canAddUnits(int amount) { return ItemWrapper.super.canAddUnits(amount) && !room.isLocked(); } + + @Override + public int getScore() { + return countUnits(); + } + + @Override + public void updateScore(int value) { + if (setter == null) return; + + setter.accept(value); + } + + @Override + public void setOnUpdateScore(Consumer setter) { + this.setter = setter; + } + + @Override + public boolean canBeUsed(int units) { + return canAddUnits(units); + } } diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java index bbdcd76..e758e72 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java @@ -2,12 +2,13 @@ package ru.dragonestia.picker.repository.impl.picker; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList; import java.util.Collection; -public class RoundRobinPicker implements Picker { +public class RoundRobinPicker implements RoomPicker { private final UserRepository userRepository; private final QueuedLinkedList list = new QueuedLinkedList<>(); @@ -45,4 +46,9 @@ public class RoundRobinPicker implements Picker { return wrapper.getItem(); } + + @Override + public PickingMode getPickingMode() { + return PickingMode.ROUND_ROBIN; + } } diff --git a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java index 8d45787..ba0edd5 100644 --- a/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java +++ b/app/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java @@ -2,13 +2,14 @@ package ru.dragonestia.picker.repository.impl.picker; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.User; +import ru.dragonestia.picker.model.type.PickingMode; import ru.dragonestia.picker.repository.UserRepository; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; -public class SequentialFillingPicker implements Picker { +public class SequentialFillingPicker implements RoomPicker { private final UserRepository userRepository; private final Map wrappers = new LinkedHashMap<>(); @@ -45,4 +46,9 @@ public class SequentialFillingPicker implements Picker { throw new RuntimeException("There are no rooms available"); } + + @Override + public PickingMode getPickingMode() { + return PickingMode.SEQUENTIAL_FILLING; + } } diff --git a/app/src/test/java/ru/dragonestia/picker/collection/DynamicSortedMapTests.java b/app/src/test/java/ru/dragonestia/picker/collection/DynamicSortedMapTests.java new file mode 100644 index 0000000..3f8a27b --- /dev/null +++ b/app/src/test/java/ru/dragonestia/picker/collection/DynamicSortedMapTests.java @@ -0,0 +1,103 @@ +package ru.dragonestia.picker.collection; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap; + +import java.util.function.Consumer; + +public class DynamicSortedMapTests { + + @Test + void testMap() { + var map = new DynamicSortedMap(); + for (int i = 0; i < 10; i++) { + int value = i; + if ((i & 1) == 1) value = 10 - value; + + var item = new Item(Integer.toString(i), value); + map.put(item); + } + + printMap(map); + + Assertions.assertEquals(new Item("0", 0), map.getMinimum()); + Assertions.assertEquals(new Item("1", 9), map.getMaximum()); + + var minId = map.getMinimum().id; + var maxId = map.getMaximum().id; + setFor(map, minId, 100); + setFor(map, maxId, -100); + + printMap(map); + + Assertions.assertEquals(new Item("1", -100), map.getMinimum()); + Assertions.assertEquals(new Item("0", 100), map.getMaximum()); + } + + private void setFor(DynamicSortedMap map, String id, int score) { + var before = map.getNode(id).toString(); + map.updateItem(id, (current) -> score); + var after = map.getNode(id).toString(); + + System.out.printf("Updated '%s' from %s to %s\n", id, before, after); + } + + private void printMap(DynamicSortedMap map) { + var list = map.getItems().stream().toList(); + var sb = new StringBuilder("Map(" + map.size() + "): "); + for (int i = 0, n = map.size(); i < n; i++) { + sb.append(list.get(i)); + if (i + 1 == n) sb.append(", "); + } + System.out.println(sb); + } + + public static class Item implements DynamicSortedMap.Item { + + private final String id; + private Consumer setter = val -> {}; + private int score; + + public Item(String id, int score) { + this.id = id; + this.score = score; + } + + @Override + public String getId() { + return id; + } + + @Override + public int getScore() { + return score; + } + + @Override + public void updateScore(int value) { + setter.accept(value); + score = value; + } + + @Override + public void setOnUpdateScore(Consumer setter) { + this.setter = setter; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (obj instanceof Item other) { + return id.equals(other.id) && score == other.score; + } + return false; + } + + @Override + public String toString() { + return "{Item id='%s' score=%s}".formatted(id, score); + } + } +} diff --git a/app/src/test/java/ru/dragonestia/picker/collection/QueuedLinkedListTests.java b/app/src/test/java/ru/dragonestia/picker/collection/QueuedLinkedListTests.java index b0c9795..139663e 100644 --- a/app/src/test/java/ru/dragonestia/picker/collection/QueuedLinkedListTests.java +++ b/app/src/test/java/ru/dragonestia/picker/collection/QueuedLinkedListTests.java @@ -80,5 +80,20 @@ public class QueuedLinkedListTests { public String getId() { return id; } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + if (obj instanceof Item other) { + return id.equals(other.id); + } + return false; + } + + @Override + public String toString() { + return "{Item id='%s' }".formatted(id); + } } }