diff --git a/pom.xml b/pom.xml
index f508fbd..6509584 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,12 @@
provided
+
+ commons-codec
+ commons-codec
+ 1.16.0
+
+
org.apache.logging.log4j
diff --git a/src/main/java/edu/hw8/task1/QuotesClient.java b/src/main/java/edu/hw8/task1/QuotesClient.java
new file mode 100644
index 0000000..25852a7
--- /dev/null
+++ b/src/main/java/edu/hw8/task1/QuotesClient.java
@@ -0,0 +1,51 @@
+package edu.hw8.task1;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import lombok.SneakyThrows;
+
+public class QuotesClient {
+ private final String address;
+ private final int port;
+ private final ByteBuffer buffer = ByteBuffer.allocate(1024);
+ private SocketChannel clientChannel;
+
+ public QuotesClient(String address, int port) {
+ this.address = address;
+ this.port = port;
+ }
+
+ @SneakyThrows
+ public void start() {
+ clientChannel = SocketChannel.open(new InetSocketAddress(address, port));
+ clientChannel.configureBlocking(false);
+ }
+
+ @SneakyThrows
+ public String requestQuote(String message) {
+ clientChannel.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
+ return readFromServer();
+ }
+
+ @SneakyThrows
+ private String readFromServer() {
+ buffer.clear();
+ StringBuilder answer = new StringBuilder();
+ while (clientChannel.read(buffer) > 0 || answer.isEmpty()) {
+ buffer.flip();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ answer.append(new String(bytes, StandardCharsets.UTF_8));
+ buffer.clear();
+ }
+ return answer.toString();
+ }
+
+ @SneakyThrows
+ public void close() {
+ clientChannel.close();
+ }
+
+}
diff --git a/src/main/java/edu/hw8/task1/QuotesServer.java b/src/main/java/edu/hw8/task1/QuotesServer.java
new file mode 100644
index 0000000..e3c754e
--- /dev/null
+++ b/src/main/java/edu/hw8/task1/QuotesServer.java
@@ -0,0 +1,80 @@
+package edu.hw8.task1;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.Iterator;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.function.Consumer;
+import lombok.SneakyThrows;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class QuotesServer {
+ private static final Logger LOGGER = LogManager.getLogger(QuotesServer.class);
+ private final int port;
+ private final QuotesStorage quotesStorage;
+ private final ExecutorService executorService;
+ private final Semaphore parallelConnectionSemaphore;
+ private Consumer messageConsumer;
+ private ServerSocketChannel serverSocketChannel;
+
+ public QuotesServer(int port, QuotesStorage quotesStorage, int parallelConnections) {
+ this.port = port;
+ this.quotesStorage = quotesStorage;
+ this.parallelConnectionSemaphore = new Semaphore(parallelConnections);
+ this.executorService = Executors.newFixedThreadPool(parallelConnections);
+ this.messageConsumer = message -> LOGGER.info("Ваня: {}", message);
+ }
+
+ @SneakyThrows
+ public void start() {
+ try (ServerSocketChannel channel = ServerSocketChannel.open()) {
+ serverSocketChannel = channel;
+ Selector selector = Selector.open();
+ channel.configureBlocking(false);
+ channel.register(selector, SelectionKey.OP_ACCEPT);
+ channel.bind(new InetSocketAddress(port));
+ processConnections(channel, selector);
+ }
+ }
+
+ @SneakyThrows
+ private void processConnections(ServerSocketChannel channel, Selector selector) {
+ while (channel.isOpen()) {
+ if (selector.selectNow() > 0 || !selector.selectedKeys().isEmpty()) {
+ Iterator iterator = selector.selectedKeys().iterator();
+ while (iterator.hasNext()) {
+ SelectionKey key = iterator.next();
+ if (key.isAcceptable() && parallelConnectionSemaphore.tryAcquire()) {
+ accept(channel);
+ iterator.remove();
+ }
+ }
+ }
+ }
+ }
+
+ @SneakyThrows
+ private void accept(ServerSocketChannel channel) {
+ SocketChannel clientChannel = channel.accept();
+ executorService.execute(new QuotesServerWorker(clientChannel, quotesStorage)
+ .afterClosing(parallelConnectionSemaphore::release)
+ .onMessage(messageConsumer)
+ );
+ }
+
+ @SneakyThrows
+ public void stop() {
+ serverSocketChannel.close();
+ executorService.shutdownNow();
+ }
+
+ public void setMessageConsumer(Consumer messageConsumer) {
+ this.messageConsumer = messageConsumer;
+ }
+}
diff --git a/src/main/java/edu/hw8/task1/QuotesServerWorker.java b/src/main/java/edu/hw8/task1/QuotesServerWorker.java
new file mode 100644
index 0000000..2fdfdf9
--- /dev/null
+++ b/src/main/java/edu/hw8/task1/QuotesServerWorker.java
@@ -0,0 +1,89 @@
+package edu.hw8.task1;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.function.Consumer;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+@RequiredArgsConstructor
+public class QuotesServerWorker implements Runnable {
+
+ private final SocketChannel clientChannel;
+ private final QuotesStorage quotesStorage;
+ private boolean isConnected = true;
+ private final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
+ private Runnable after;
+ private Consumer messageConsumer;
+
+ @SneakyThrows @Override
+ public void run() {
+ Selector selector = Selector.open();
+ clientChannel.configureBlocking(false);
+ clientChannel.register(selector, SelectionKey.OP_READ);
+ while (isConnected) {
+ if (selector.selectNow() > 0) {
+ Iterator iterator = selector.selectedKeys().iterator();
+ while (iterator.hasNext()) {
+ SelectionKey key = iterator.next();
+ if (key.isReadable()) {
+ String message = readMessageFromClient();
+ if (message == null) {
+ isConnected = false;
+ break;
+ }
+ if (messageConsumer != null) {
+ messageConsumer.accept(message);
+ }
+ writeMessageToClient(quotesStorage.getQuote(message));
+ }
+ iterator.remove();
+ }
+ }
+ }
+ if (after != null) {
+ after.run();
+ }
+ }
+
+ @SneakyThrows
+ private String readMessageFromClient() {
+ try {
+ StringBuilder message = new StringBuilder();
+ int read = clientChannel.read(byteBuffer);
+ if (read <= 0) {
+ return null;
+ }
+ while (read > 0) {
+ byteBuffer.flip();
+ byte[] bytesArray = new byte[byteBuffer.remaining()];
+ byteBuffer.get(bytesArray);
+ message.append(new String(bytesArray, StandardCharsets.UTF_8));
+ byteBuffer.clear();
+ read = clientChannel.read(byteBuffer);
+ }
+ return message.toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @SneakyThrows
+ private void writeMessageToClient(String message) {
+ clientChannel.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ public QuotesServerWorker afterClosing(Runnable after) {
+ this.after = after;
+ return this;
+ }
+
+ public QuotesServerWorker onMessage(Consumer messageSupplier) {
+ this.messageConsumer = messageSupplier;
+ return this;
+ }
+}
diff --git a/src/main/java/edu/hw8/task1/QuotesStorage.java b/src/main/java/edu/hw8/task1/QuotesStorage.java
new file mode 100644
index 0000000..be27098
--- /dev/null
+++ b/src/main/java/edu/hw8/task1/QuotesStorage.java
@@ -0,0 +1,34 @@
+package edu.hw8.task1;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class QuotesStorage {
+ private final Map quotesMap = new HashMap<>();
+
+ public static QuotesStorage createDefault() {
+ QuotesStorage quotesStorage = new QuotesStorage();
+ quotesStorage.appendQuote("личности", "Не переходи на личности там, где их нет");
+ quotesStorage.appendQuote(
+ "оскорбления",
+ "Если твои противники перешли на личные оскорбления, будь уверена — твоя победа не за горами"
+ );
+ quotesStorage.appendQuote(
+ "глупый",
+ "А я тебе говорил, что ты глупый? Так вот, я забираю свои слова обратно... Ты просто бог идиотизма"
+ );
+ quotesStorage.appendQuote(
+ "интеллект",
+ "Чем ниже интеллект, тем громче оскорбления"
+ );
+ return quotesStorage;
+ }
+
+ public String getQuote(String id) {
+ return quotesMap.getOrDefault(id, "Неизвестная цитата");
+ }
+
+ public void appendQuote(String id, String quote) {
+ quotesMap.put(id, quote);
+ }
+}
diff --git a/src/main/java/edu/hw8/task2/FibonacciComputer.java b/src/main/java/edu/hw8/task2/FibonacciComputer.java
new file mode 100644
index 0000000..4ab7b7e
--- /dev/null
+++ b/src/main/java/edu/hw8/task2/FibonacciComputer.java
@@ -0,0 +1,33 @@
+package edu.hw8.task2;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public final class FibonacciComputer {
+
+ private FibonacciComputer() {
+ }
+
+ public static int compute(int n) {
+ if (n == 0) {
+ return 0;
+ }
+ if (n == 1) {
+ return 1;
+ }
+ return compute(n - 1) + compute(n - 2);
+ }
+
+ public static List computeList(List list, int threadsCount) {
+ ThreadPool threadPool = FixedThreadPool.create(threadsCount);
+ threadPool.start();
+ List result = new CopyOnWriteArrayList<>();
+ for (int i = 0; i < list.size(); i++) {
+ int current = i;
+ threadPool.execute(() -> result.add(FibonacciComputer.compute(list.get(current))));
+ }
+ threadPool.close();
+ threadPool.awaitTermination();
+ return result;
+ }
+}
diff --git a/src/main/java/edu/hw8/task2/FixedThreadPool.java b/src/main/java/edu/hw8/task2/FixedThreadPool.java
new file mode 100644
index 0000000..6ceff9f
--- /dev/null
+++ b/src/main/java/edu/hw8/task2/FixedThreadPool.java
@@ -0,0 +1,85 @@
+package edu.hw8.task2;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.SneakyThrows;
+
+public final class FixedThreadPool implements ThreadPool {
+
+ private final int poolSize;
+ private final FixedThreadPoolWorker[] workers;
+ private final BlockingQueue runnableBlockingQueue;
+ private final AtomicInteger currentWorkersCount = new AtomicInteger(0);
+ private final AtomicBoolean canAcceptTasks = new AtomicBoolean(false);
+
+ private FixedThreadPool(int poolSize) {
+ if (poolSize <= 0) {
+ throw new IllegalArgumentException("Pool size must be greater than 0");
+ }
+ this.poolSize = poolSize;
+ this.workers = new FixedThreadPoolWorker[poolSize];
+ runnableBlockingQueue = new LinkedBlockingQueue<>();
+ }
+
+ public static FixedThreadPool create(int poolSize) {
+ return new FixedThreadPool(poolSize);
+ }
+
+ @SneakyThrows
+ public void execute(Runnable task) {
+ if (canAcceptTasks.get()) {
+ if (currentWorkersCount.get() < poolSize) {
+ workers[currentWorkersCount.get()] = new FixedThreadPoolWorker(); // Lazy initialization
+ currentWorkersCount.incrementAndGet();
+ }
+ runnableBlockingQueue.put(task);
+ }
+ }
+
+ @Override
+ public void start() {
+ canAcceptTasks.set(true);
+ }
+
+ @Override
+ public void awaitTermination() {
+ for (FixedThreadPoolWorker worker : workers) {
+ worker.join();
+ }
+ }
+
+ @Override
+ public void close() {
+ canAcceptTasks.set(false);
+ }
+
+ public class FixedThreadPoolWorker implements Runnable {
+
+ private Runnable task;
+ private final Thread thread;
+
+ public FixedThreadPoolWorker() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void run() {
+ while (canAcceptTasks.get() || !runnableBlockingQueue.isEmpty() || task != null) {
+ if (task == null) {
+ task = runnableBlockingQueue.poll();
+ continue;
+ }
+ task.run();
+ task = runnableBlockingQueue.poll();
+ }
+ }
+
+ @SneakyThrows
+ public void join() {
+ thread.join();
+ }
+ }
+
+}
diff --git a/src/main/java/edu/hw8/task2/ThreadPool.java b/src/main/java/edu/hw8/task2/ThreadPool.java
new file mode 100644
index 0000000..d9a5795
--- /dev/null
+++ b/src/main/java/edu/hw8/task2/ThreadPool.java
@@ -0,0 +1,13 @@
+package edu.hw8.task2;
+
+public interface ThreadPool extends AutoCloseable {
+
+ void execute(Runnable runnable);
+
+ void start();
+
+ void awaitTermination();
+
+ @Override
+ void close();
+}
diff --git a/src/main/java/edu/hw8/task3/AbstractPasswordsDecoder.java b/src/main/java/edu/hw8/task3/AbstractPasswordsDecoder.java
new file mode 100644
index 0000000..85d904c
--- /dev/null
+++ b/src/main/java/edu/hw8/task3/AbstractPasswordsDecoder.java
@@ -0,0 +1,132 @@
+package edu.hw8.task3;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import lombok.SneakyThrows;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+
+public abstract class AbstractPasswordsDecoder implements PasswordsDecoder {
+
+ private static final int MINIMAL_PASSWORD_LENGTH = 4;
+ private static final int MAX_PASSWORD_LENGTH = 6;
+
+ // Actually, we not need to synchronize userMap and decoded,
+ // because probability of hash collision is very small with MD5 algorithm
+ protected Map usersMap;
+ protected final List decoded = new CopyOnWriteArrayList<>();
+ protected static final byte[] ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789".getBytes(StandardCharsets.UTF_8);
+
+ public AbstractPasswordsDecoder(Map usersMap) {
+ this.usersMap = usersMap
+ .entrySet()
+ .stream().collect(Collectors.toMap(
+ entry -> {
+ try {
+ // To provide more performance
+ return new ByteArray(Hex.decodeHex(entry.getValue().toCharArray()));
+ } catch (DecoderException e) {
+ throw new RuntimeException(e);
+ }
+ },
+ Map.Entry::getKey
+ ));
+ }
+
+ @Override
+ public abstract List decode();
+
+ @SneakyThrows
+ protected void decodeInRange(int min, int max) {
+ boolean containsSmallPasswords = min != -1;
+ int[] indexes =
+ createIndexes(MAX_PASSWORD_LENGTH, containsSmallPasswords ? MINIMAL_PASSWORD_LENGTH : MAX_PASSWORD_LENGTH);
+ indexes[indexes.length - 1] = min;
+ // Create there to provide each thread new MessageDigest instance
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+ while (indexes[indexes.length - 1] < max) {
+ if (decoded.size() == usersMap.size()) {
+ break;
+ }
+ var bytes = nextPassword(indexes);
+ ByteArray encodedPassword = encodeMd5(bytes, messageDigest);
+ if (usersMap.containsKey(encodedPassword)) {
+ decoded.add(new User(usersMap.get(encodedPassword), new String(bytes)));
+ }
+ }
+ }
+
+ private int[] createIndexes(int passwordLength, int minimumLength) {
+ int[] indexes = new int[passwordLength];
+ for (int i = 0; i < indexes.length; i++) {
+ if (i >= minimumLength) {
+ indexes[i] = -1;
+ } else {
+ indexes[i] = 0;
+ }
+ }
+ return indexes;
+ }
+
+ private byte[] nextPassword(int[] indexes) {
+ byte[] bytes = makeByteString(indexes);
+ addOneToIndexes(indexes, ALPHABET.length);
+ return bytes;
+ }
+
+ private byte[] makeByteString(int[] indexes) {
+ byte[] bytes = new byte[indexes.length];
+ int realCount = indexes.length;
+ for (int i = 0; i < indexes.length; i++) {
+ if (indexes[i] < 0) {
+ realCount = i;
+ break;
+ }
+ bytes[i] = ALPHABET[indexes[i]];
+ }
+ bytes = Arrays.copyOf(bytes, realCount);
+ return bytes;
+ }
+
+ private void addOneToIndexes(int[] indexes, int max) {
+ indexes[0]++;
+ for (int i = 0; i < indexes.length - 1; i++) {
+ if (indexes[i] >= max) {
+ indexes[i] = 0;
+ indexes[i + 1]++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ @SneakyThrows
+ private ByteArray encodeMd5(byte[] password, MessageDigest messageDigest) {
+ messageDigest.update(password);
+ return new ByteArray(messageDigest.digest());
+ }
+
+ protected record ByteArray(byte[] array) {
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ ByteArray byteArray = (ByteArray) object;
+ return Arrays.equals(array, byteArray.array);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(array);
+ }
+ }
+}
diff --git a/src/main/java/edu/hw8/task3/ParallelPasswordsDecoder.java b/src/main/java/edu/hw8/task3/ParallelPasswordsDecoder.java
new file mode 100644
index 0000000..4e28f00
--- /dev/null
+++ b/src/main/java/edu/hw8/task3/ParallelPasswordsDecoder.java
@@ -0,0 +1,38 @@
+package edu.hw8.task3;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import lombok.SneakyThrows;
+
+public class ParallelPasswordsDecoder extends AbstractPasswordsDecoder {
+ private final ExecutorService executorService;
+ private final int threadCount;
+
+ public ParallelPasswordsDecoder(Map usersMap, int threadCount) {
+ super(new ConcurrentHashMap<>(usersMap));
+ this.executorService = Executors.newFixedThreadPool(threadCount);
+ this.threadCount = threadCount;
+ }
+
+ @SneakyThrows
+ @Override
+ public List decode() {
+ int offset = -1;
+ int passwordsPerThread = ALPHABET.length / threadCount;
+ for (int thread = 0; thread < threadCount - 1; thread++) {
+ final int start = offset;
+ executorService.execute(() -> decodeInRange(start, start + passwordsPerThread));
+ offset += passwordsPerThread;
+ }
+ final int start = offset;
+ executorService.execute(() -> decodeInRange(start, ALPHABET.length));
+ executorService.shutdown();
+ executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
+ return decoded;
+ }
+
+}
diff --git a/src/main/java/edu/hw8/task3/PasswordsDecoder.java b/src/main/java/edu/hw8/task3/PasswordsDecoder.java
new file mode 100644
index 0000000..d7359a7
--- /dev/null
+++ b/src/main/java/edu/hw8/task3/PasswordsDecoder.java
@@ -0,0 +1,7 @@
+package edu.hw8.task3;
+
+import java.util.List;
+
+public interface PasswordsDecoder {
+ List decode();
+}
diff --git a/src/main/java/edu/hw8/task3/SingleThreadPasswordDecoder.java b/src/main/java/edu/hw8/task3/SingleThreadPasswordDecoder.java
new file mode 100644
index 0000000..fe23550
--- /dev/null
+++ b/src/main/java/edu/hw8/task3/SingleThreadPasswordDecoder.java
@@ -0,0 +1,16 @@
+package edu.hw8.task3;
+
+import java.util.List;
+import java.util.Map;
+
+public class SingleThreadPasswordDecoder extends AbstractPasswordsDecoder {
+ public SingleThreadPasswordDecoder(Map usersMap) {
+ super(usersMap);
+ }
+
+ @Override
+ public List decode() {
+ decodeInRange(-1, ALPHABET.length);
+ return decoded;
+ }
+}
diff --git a/src/main/java/edu/hw8/task3/User.java b/src/main/java/edu/hw8/task3/User.java
new file mode 100644
index 0000000..d40a72f
--- /dev/null
+++ b/src/main/java/edu/hw8/task3/User.java
@@ -0,0 +1,4 @@
+package edu.hw8.task3;
+
+public record User(String name, String password) {
+}
diff --git a/src/test/java/edu/hw8/task1/QuotesTest.java b/src/test/java/edu/hw8/task1/QuotesTest.java
new file mode 100644
index 0000000..a38f52f
--- /dev/null
+++ b/src/test/java/edu/hw8/task1/QuotesTest.java
@@ -0,0 +1,48 @@
+package edu.hw8.task1;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import lombok.SneakyThrows;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class QuotesTest {
+
+ @SneakyThrows
+ @Test
+ @DisplayName("Тестирование работы QuotesServer и QuotesClient")
+ public void quotes_shouldCorrectlyWork() {
+ List userRequests = new ArrayList<>();
+ List serverResponses = new ArrayList<>();
+ QuotesStorage quotesStorage = QuotesStorage.createDefault();
+ QuotesServer quotesServer = new QuotesServer(12345, quotesStorage, 2);
+ QuotesClient quotesClient = new QuotesClient("localhost", 12345);
+
+ quotesServer.setMessageConsumer(userRequests::add);
+ Executors.newSingleThreadExecutor().execute(quotesServer::start);
+ Thread.sleep(1000);
+
+ // Imitate multiconnnection
+ quotesClient.start();
+ quotesClient.close();
+ quotesClient.start();
+ quotesClient.close();
+ quotesClient.start();
+
+ serverResponses.add(quotesClient.requestQuote("личности"));
+ serverResponses.add(quotesClient.requestQuote("оскорбления"));
+
+ Thread.sleep(1000);
+ quotesServer.stop();
+ quotesClient.close();
+
+ Assertions.assertThat(userRequests).containsExactly("личности", "оскорбления");
+ Assertions.assertThat(serverResponses).containsExactly(
+ "Не переходи на личности там, где их нет",
+ "Если твои противники перешли на личные оскорбления, будь уверена — твоя победа не за горами"
+ );
+ }
+
+}
diff --git a/src/test/java/edu/hw8/task2/FibonacciComputerTest.java b/src/test/java/edu/hw8/task2/FibonacciComputerTest.java
new file mode 100644
index 0000000..4af45cb
--- /dev/null
+++ b/src/test/java/edu/hw8/task2/FibonacciComputerTest.java
@@ -0,0 +1,20 @@
+package edu.hw8.task2;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class FibonacciComputerTest {
+
+ @Test
+ @DisplayName("Тестирование FibonacciComputer#computeList")
+ public void computeList_shouldReturnCorrectResult() {
+ List list = IntStream.range(0, 13).boxed().toList();
+ List result = FibonacciComputer.computeList(list, 4);
+ Assertions.assertThat(result).containsExactlyInAnyOrder(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144);
+ }
+
+}
diff --git a/src/test/java/edu/hw8/task3/PasswordDecoderTest.java b/src/test/java/edu/hw8/task3/PasswordDecoderTest.java
new file mode 100644
index 0000000..d48f457
--- /dev/null
+++ b/src/test/java/edu/hw8/task3/PasswordDecoderTest.java
@@ -0,0 +1,47 @@
+package edu.hw8.task3;
+
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class PasswordDecoderTest {
+
+ @Test
+ @DisplayName("Тестирование ParallelPasswordsDecoder#decode")
+ public void decode_shouldReturnCorrectResult_whenMultiThread() {
+ PasswordsDecoder decoder = new ParallelPasswordsDecoder(
+ Map.of(
+ "k.p.maslov", "18e84fa2f549f87a4a3ee3a46e63c3e2",
+ "k.p.mdfaslov", "9a32f4a86b3bcd053642f4c708be27f8",
+ "k.p.в", "0765c0f7c8e3df239f846c4f78ed1da6",
+ "kh.p.в", "2354b5f68a4ce8a030eee955639fdd16"
+ ), Runtime.getRuntime().availableProcessors()
+ );
+ Assertions.assertThat(decoder.decode()).containsExactly(
+ new User("k.p.maslov", "abbba"),
+ new User("k.p.mdfaslov", "czzzc"),
+ new User("k.p.в", "gvvvg"),
+ new User("kh.p.в", "zbbbz")
+ );
+ }
+
+ @Test
+ @DisplayName("Тестирование SingleThreadPasswordDecoder#decode")
+ public void decode_shouldReturnCorrectResult_whenSingleThread() {
+ PasswordsDecoder decoder = new SingleThreadPasswordDecoder(
+ Map.of(
+ "k.p.maslov", "18e84fa2f549f87a4a3ee3a46e63c3e2",
+ "k.p.mdfaslov", "9a32f4a86b3bcd053642f4c708be27f8",
+ "k.p.в", "0765c0f7c8e3df239f846c4f78ed1da6",
+ "kh.p.в", "2354b5f68a4ce8a030eee955639fdd16"
+ )
+ );
+ Assertions.assertThat(decoder.decode()).containsExactly(
+ new User("k.p.maslov", "abbba"),
+ new User("k.p.mdfaslov", "czzzc"),
+ new User("k.p.в", "gvvvg"),
+ new User("kh.p.в", "zbbbz")
+ );
+ }
+}