diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 8c507688..d9a5bdfc 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -36,13 +36,99 @@ jobs: - name: Copy test files run: | - echo "Copying test data" - mkdir -p src/test/resources + echo "Copying hidden tests" + mkdir -p src/test/java src/test/resources + cp -R hidden-tests/src/test/java/. src/test/java/ cp -R hidden-tests/src/test/resources/. src/test/resources/ echo "" echo "Скопированы файлы:" - find src/test/resources -type f | sort | while read f; do echo " $f"; done + find src/test/java src/test/resources -type f | sort | while read f; do echo " $f"; done rm -rf hidden-tests - + + - name: Select test pattern from commit message + id: select_tests + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.setOutput('pattern', ''); + return; + } + const repo = { owner: context.repo.owner, repo: context.repo.repo }; + const { data: commit } = await github.rest.repos.getCommit({ + ...repo, + ref: pr.head.sha + }); + const msg = (commit.commit.message || '').toLowerCase(); + + const patterns = []; + const add = (p) => { if (!patterns.includes(p)) patterns.push(p); }; + + if (msg.startsWith('atm:')) add('**/atm/*Test'); + if (msg.startsWith('html:')) add('**/html/*Test'); + if (msg.startsWith('randomset:')) add('**/randomSet/*Test'); + if (msg.startsWith('cube:')) add('**/CubeSimpleTest'); + + core.setOutput('pattern', patterns.length ? patterns.join(',') : ''); + + - name: Run honors no-collections test + id: honors_test + continue-on-error: true + if: steps.select_tests.outputs.pattern == '**/randomSet/*Test' + run: mvn -B -Dtest=hse.java.lectures.lecture3.practice.randomSet.RandomSetBytecodeTest test + + - name: Comment on PR for honors solution + if: steps.honors_test.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.info('No PR context, skipping comment.'); + return; + } + const body = [ + 'Поздравляем! 🎉', + '', + 'Вы решили сложную версию: решение прошло проверку «без коллекций».' + ].join('\n'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + - name: Run tests - run: mvn -B test + run: | + if [ -n "${{ steps.select_tests.outputs.pattern }}" ]; then + echo "Running selected tests: ${{ steps.select_tests.outputs.pattern }}" + mvn -B -Dtest='${{ steps.select_tests.outputs.pattern }},!**/RandomSetBytecodeTest' test + else + echo "No task prefix found. Failing." + exit 1 + fi + + - name: Comment on PR when no task prefix + if: steps.select_tests.outputs.pattern == '' + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.info('No PR context, skipping comment.'); + return; + } + const body = [ + 'Тесты не запускались.', + '', + 'Укажите задачу в начале сообщения коммита:', + 'atm: ..., html: ..., randomset: ...' + ].join('\n'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); diff --git a/README-commit-format.md b/README-commit-format.md new file mode 100644 index 00000000..24c170a4 --- /dev/null +++ b/README-commit-format.md @@ -0,0 +1,20 @@ +# Формат коммитов для запуска тестов + +## Зачем это нужно +CI запускает тесты выборочно. Чтобы он знал, какие тесты запускать, сообщение коммита должно начинаться с названия задачи. + +## Формат +Используйте префикс в начале сообщения коммита: + +- `atm: ...` +- `html: ...` +- `randomset: ...` + +Тег задачи указывается в описании каждой задачи + +Пример: +``` +randomset: implement getRandom and contains +``` + +Если префикс не указан, CI **не запускает тесты** и оставляет комментарий в PR. diff --git a/commander/commander.iml b/commander/commander.iml deleted file mode 100644 index 68a9707e..00000000 --- a/commander/commander.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index ade43fe2..648f8788 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,13 @@ ${junit.version} test + + + org.ow2.asm + asm + 9.8 + test + com.fasterxml.jackson.core @@ -65,4 +72,4 @@ - \ No newline at end of file + diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Box.java b/src/main/java/hse/java/lectures/lecture3/examples/Box.java new file mode 100644 index 00000000..6c3ccf57 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Box.java @@ -0,0 +1,14 @@ +package hse.java.lectures.lecture3.examples; + +public class Box { + + private T item; + + public void put(T item) { + this.item = item; + } + + public T get() { + return item; + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Generics.java b/src/main/java/hse/java/lectures/lecture3/examples/Generics.java new file mode 100644 index 00000000..4fe8a270 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Generics.java @@ -0,0 +1,43 @@ +package hse.java.lectures.lecture3.examples; + +import java.util.ArrayList; +import java.util.List; + +public class Generics { + + + + public T getType(T type) { + System.out.println(type.getClass()); + return type; + } + + public static void write(List list, T value) { + list.add(value); + } + + public static > T get(T value) { + return value; + } + + public static void testGenerics() { + // get(1.).compareTo() + write(List.of(1), 1); + write(List.of(), 1.2); + List ints = new ArrayList<>(); + ints.add(1); + ints.add(2); + // PECS + List nums = ints; + List si = ints; + si.add(1); + si.get(1); + } + + public static void main(String[] args) { + List il = new ArrayList<>(); + write(il, 5); + System.out.println(il); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Main.java b/src/main/java/hse/java/lectures/lecture3/examples/Main.java new file mode 100644 index 00000000..116ff543 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Main.java @@ -0,0 +1,19 @@ +package hse.java.lectures.lecture3.examples; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +public class Main { + public static void main(String[] args) { + Box box = new Box<>(); + Box dBox = new Box<>(); + Callable callable; + + int x = new Integer(3); + Integer y = 5; + + Box nBox = box; + new Methods().get(new ArrayList()).add(1); + + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/Methods.java b/src/main/java/hse/java/lectures/lecture3/examples/Methods.java new file mode 100644 index 00000000..cac365d7 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/Methods.java @@ -0,0 +1,10 @@ +package hse.java.lectures.lecture3.examples; + +public class Methods { + + + public T get(T type) { + return type; + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java b/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java new file mode 100644 index 00000000..0d972ac3 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/examples/TryCatchThrows.java @@ -0,0 +1,79 @@ +package hse.java.lectures.lecture3.examples; + +import java.io.*; + +public class TryCatchThrows { + + public void testRuntime() throws RuntimeException { + throw new RuntimeException("Test runtime"); + } + + public void testException() throws Exception { + throw new Exception("test exception"); + } + + public void testError() throws Error { + throw new Error("test error"); + } + + public void testTryRuntime() { + try { + testRuntime(); + } catch (RuntimeException e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public void testTryException() { + try { + testException(); + } catch (Exception e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public void testTryError() { + // testError(); + try { + testError(); + } catch (Error e) { + System.out.println("Catch " + e.getMessage()); + } finally { + System.out.println("finally"); + } + } + + public static void foo() { + throw new RuntimeException("My runtime"); + } + + public static void bar() throws Exception { + throw new Exception("My exception"); + } + + public static void main(String[] args) { + // foo(); +// try { +// bar(); +// } catch (Exception e) { +// System.err.println("Hello exception"); +// } finally { +// +// } + +// try (BufferedReader br = new BufferedReader(new FileReader(""))) { +// +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + + // int x = 1/ 0; + int[] a = new int[1]; + System.out.println(a[2]); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java new file mode 100644 index 00000000..c7820ac5 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/EmptySetException.java @@ -0,0 +1,7 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +public class EmptySetException extends RuntimeException { + public EmptySetException(String message) { + super(message); + } +} diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java new file mode 100644 index 00000000..8af477b5 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/RandomSet.java @@ -0,0 +1,21 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +public class RandomSet { + + public boolean insert(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean remove(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean contains(T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + public T getRandom() { + throw new UnsupportedOperationException("Not implemented"); + } + +} diff --git a/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md new file mode 100644 index 00000000..fb987780 --- /dev/null +++ b/src/main/java/hse/java/lectures/lecture3/practice/randomSet/task.md @@ -0,0 +1,52 @@ +# Случайное множество (RandomSet) + +Tag: randomset + +## Условие + +Реализовать класс `RandomSet`, который хранит множество целых чисел и поддерживает операции вставки, удаления и получения случайного элемента. + +## Требования к асимптотике + +- `insert(x)` — **O(log n) or O(1)** +- `remove(x)` — **O(log n) or O(1)** +- `contains(x)` — **(log n) or O(1)** +- `getRandom()` — **O(1)** + +Не допускается использование стандартных коллекций Java. + +## Публичный API + +1. `RandomSet()` — создаёт пустое множество. +1. `boolean insert(T value)` — добавляет `value` в множество. + - Возвращает `true`, если элемент добавлен. + - Возвращает `false`, если элемент уже был в множестве. +1. `boolean remove(T value)` — удаляет `value` из множества. + - Возвращает `true`, если элемент был удалён. + - Возвращает `false`, если элемента не было. +1. `boolean contains(T value)` — проверяет наличие `value` в множестве. + - Возвращает `true`, если элемент есть. + - Возвращает `false`, если элемента нет. +1. `T getRandom()` — возвращает случайный элемент из множества. + - Если множество пустое, выбрасывает `EmptySetException`. + +## Исключения + +- `EmptySetException` — попытка получить случайный элемент из пустого множества. + +## Пример + +```java +RandomSet set = new RandomSet<>(); +set.insert(10); // true +set.insert(20); // true +set.insert(10); // false + +int x = set.getRandom(); // 10 или 20 + +set.remove(10); // true +set.remove(10); // false + +set.contains(20); // true +set.contains(30); // false +``` diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java index 3fa91909..af541449 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/Atm.java @@ -8,7 +8,7 @@ import java.util.TreeSet; public class Atm { - private enum Denomination { + public enum Denomination { D50(50), D100(100), D500(500), @@ -26,19 +26,85 @@ int value() { } } - private final Map banknotes = new EnumMap<>(Denomination.class); + private Map banknotes = new EnumMap<>(Denomination.class); + + private Denomination getDenominationByValue(int value) { + for (Denomination denomination : Denomination.values()) { + if (denomination.value() == value) { + return denomination; + } + } + throw new IllegalArgumentException("Invalid denomination value: " + value); + } public Atm() { } - public void deposit(Map banknotes){} + public void deposit(Map bills) { + if (bills == null) { + throw new InvalidDepositException("Bills map cannot be null"); + } + + for (Map.Entry entry : bills.entrySet()) { + int denominationValue = entry.getKey(); + int count = entry.getValue(); + + if (count <= 0) { + throw new InvalidDepositException("Count must be positive for denomination: " + denominationValue); + } + + Denomination denomination; + try { + denomination = getDenominationByValue(denominationValue); + } catch (IllegalArgumentException e) { + throw new InvalidDepositException("Invalid denomination: " + denominationValue); + } + + banknotes.put(denomination, banknotes.getOrDefault(denomination, 0) + count); + } + } public Map withdraw(int amount) { - return Map.of(); + if (amount <= 0) { + throw new InvalidAmountException("Amount must be positive"); + } + + if (amount > getBalance()) { + throw new InsufficientFundsException("Not enough funds in the ATM"); + } + + Map result = new HashMap<>(); + var banknoteCopy = new EnumMap<>(banknotes); + + + for (int i = Denomination.values().length - 1; i >= 0; i--) { + Denomination denomination = Denomination.values()[i]; + int availableCount = banknotes.getOrDefault(denomination, 0); + int neededCount = amount / denomination.value(); + int countToDispense = Math.min(availableCount, neededCount); + + if (countToDispense > 0) { + result.put(denomination.value(), countToDispense); + amount -= countToDispense * denomination.value(); + banknotes.put(denomination, availableCount - countToDispense); + } + } + + if (amount != 0) { + banknotes = banknoteCopy; + throw new CannotDispenseException("Not enough funds in the ATM"); + } + + return result; } public int getBalance() { - return 0; + int result = 0; + for (Map.Entry entry : banknotes.entrySet()) { + Denomination denomination = entry.getKey(); + int count = entry.getValue(); + result += count * denomination.value(); + } + return result; } - -} +} \ No newline at end of file diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md b/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md index 3845672e..6d574e19 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md +++ b/src/main/java/hse/java/lectures/lecture3/tasks/atm/task.md @@ -1,5 +1,7 @@ # Банкомат (ATM) +Tag: atm + ## Условие Реализовать класс `Atm`, который хранит купюры фиксированных номиналов и выполняет операции пополнения и выдачи наличных. При ошибках операции должны завершаться выбросом исключения без изменения состояния банкомата. diff --git a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java index 1ddf27bf..b82e54fc 100644 --- a/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java +++ b/src/main/java/hse/java/lectures/lecture3/tasks/html/HtmlDocument.java @@ -4,12 +4,18 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Set; +import java.util.*; public class HtmlDocument { + private static final Map allowedTags = Map.of( + "", "", + "", "", + "", "", + "
", "
", + "

", "

" + ); + public HtmlDocument(String filePath) { this(Path.of(filePath)); } @@ -27,6 +33,135 @@ private String readFile(Path filePath) { } } - private void validate(String content){} + private String ignoreAttributes(String tag) { + String lowerTag = tag.toLowerCase(); + for (String allowed : allowedTags.keySet()) { + var expected = allowed.substring(1, allowed.length() - 1); + if (lowerTag.startsWith("<" + expected)) { + return "<" + expected + ">"; + } + if (lowerTag.startsWith(""; + } + } + return null; + } + + private List extractRootTags(String content) { + List rootTags = new ArrayList<>(); + int pos = 0; + int depth = 0; + + while (pos < content.length()) { + int open = content.indexOf('<', pos); + if (open == -1) break; + + int close = content.indexOf('>', open); + if (close == -1) break; + + String tag = content.substring(open, close + 1).toLowerCase(); + pos = close + 1; + + if (tag.startsWith(""); + int htmlClosePos = content.indexOf(""); + + if (htmlOpenPos == -1 || htmlClosePos == -1 || htmlOpenPos >= htmlClosePos) { + throw new InvalidStructureException("Html tag is needed and must be properly opened before closed."); + } + + for (String openTag : allowedTags.keySet()) { + checkTagPosition(content, openTag, htmlOpenPos, htmlClosePos); + } + for (String closeTag : allowedTags.values()) { + checkTagPosition(content, closeTag, htmlOpenPos, htmlClosePos); + } + + String innerHtml = content.substring(htmlOpenPos + "".length(), htmlClosePos); + List rootTags = extractRootTags(innerHtml); + + if (rootTags.size() != 2) { + throw new InvalidStructureException("Inside there must be exactly followed by ."); + } + + if (!"".equals(rootTags.get(0))) { + throw new InvalidStructureException(" must be the first child of ."); + } + + if (!"".equals(rootTags.get(1))) { + throw new InvalidStructureException(" must follow as second child of ."); + } + } + + private void checkTagPosition(String content, String tag, int htmlOpenPos, int htmlClosePos) { + int index = -1; + while ((index = content.indexOf(tag, index + 1)) != -1) { + if (index < htmlOpenPos || index > htmlClosePos) { + throw new InvalidStructureException( + "Tag '" + tag + "' must be inside ..., but found at position " + index); + } + } + } + + private void validate(String content) { + Stack stack = new Stack<>(); + int pos = 0; + + while (pos < content.length()) { + int tagStart = content.indexOf('<', pos); + int tagEnd = content.indexOf('>', tagStart); + if (tagStart == -1 || tagEnd == -1) break; + + String tag = content.substring(tagStart, tagEnd + 1); + + var normalizedTag = ignoreAttributes(tag); + + if (normalizedTag == null) { + throw new UnsupportedTagException(tag); + } + + if (allowedTags.containsKey(normalizedTag)) { + stack.push(normalizedTag.toLowerCase()); + } else if (normalizedTag.startsWith(" 0; i--) { + setSticker(cycle[i], getSticker(cycle[i - 1])); + } + setSticker(cycle[0], temp); + } + + private CubeColor getSticker(int stickerIndex) { + var index = stickerIndexToPartsIndex(stickerIndex); + return edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()]; + } + + private void setSticker(int stickerIndex, CubeColor color) { + var index = stickerIndexToPartsIndex(stickerIndex); + edges[getEdgeIndexByStickerIndex(stickerIndex)].getParts()[index.row()][index.column()] = color; + } + + private void rotate(RotateDirection direction, int[][] clockwiseRotation, int[][] counterClockwiseRotation) { + int[][] rotation = (direction == RotateDirection.CLOCKWISE) ? clockwiseRotation : counterClockwiseRotation; + for (int[] cycle : rotation) { + applyCycle(cycle); + } + } + + @Override + public void up(RotateDirection direction) { + rotate(direction, EdgeRotation.UP_CLOCKWISE, EdgeRotation.UP_COUNTERCLOCKWISE); + } + + @Override + public void down(RotateDirection direction) { + rotate(direction, EdgeRotation.DOWN_CLOCKWISE, EdgeRotation.DOWN_COUNTERCLOCKWISE); + } + + @Override + public void left(RotateDirection direction) { + rotate(direction, EdgeRotation.LEFT_CLOCKWISE, EdgeRotation.LEFT_COUNTERCLOCKWISE); + } + + @Override + public void right(RotateDirection direction) { + rotate(direction, EdgeRotation.RIGHT_CLOCKWISE, EdgeRotation.RIGHT_COUNTERCLOCKWISE); + } + + @Override public void front(RotateDirection direction) { + rotate(direction, EdgeRotation.FRONT_CLOCKWISE, EdgeRotation.FRONT_COUNTERCLOCKWISE); + } + @Override + public void back(RotateDirection direction) { + rotate(direction, EdgeRotation.BACK_CLOCKWISE, EdgeRotation.BACK_COUNTERCLOCKWISE); } - + public Edge[] getEdges() { return edges; } diff --git a/src/main/java/hse/java/practice/task1/task.md b/src/main/java/hse/java/practice/task1/task.md index 235cc660..60dc4533 100644 --- a/src/main/java/hse/java/practice/task1/task.md +++ b/src/main/java/hse/java/practice/task1/task.md @@ -1,5 +1,7 @@ Необходимо реализовать методы поворота кубика Рубика. +Tag: cube + Кубик представляет собой классический куб 3×3×3: - 6 граней; - каждая грань состоит из 9 элементов (стикеров); diff --git a/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java b/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java new file mode 100644 index 00000000..37c38ab1 --- /dev/null +++ b/src/test/java/hse/java/lectures/lecture3/practice/randomSet/RandomSetBaseTest.java @@ -0,0 +1,28 @@ +package hse.java.lectures.lecture3.practice.randomSet; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RandomSetBaseTest { + + @Test + void insertRemoveContains() { + RandomSet set = new RandomSet<>(); + + assertTrue(set.insert(10)); + assertTrue(set.insert(20)); + assertFalse(set.insert(10)); + + assertTrue(set.contains(10)); + assertTrue(set.contains(20)); + assertFalse(set.contains(30)); + + assertTrue(set.remove(10)); + assertFalse(set.remove(10)); + + assertFalse(set.contains(10)); + assertTrue(set.contains(20)); + } + +} diff --git a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java index 9e3994a6..c74a91c0 100644 --- a/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java +++ b/src/test/java/hse/java/lectures/lecture3/tasks/atm/AtmTest.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.stream.Stream; +import static hse.java.lectures.lecture3.tasks.atm.Atm.Denomination.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -34,14 +35,7 @@ void depositIncreasesBalance() { assertEquals(3500, atm.getBalance()); } - @Test - void depositRejectsInvalidDenominationAndKeepsState() { - Atm atm = new Atm(); - atm.deposit(Map.of(100, 1)); - assertThrows(InvalidDepositException.class, () -> atm.deposit(Map.of(30, 1))); - assertEquals(100, atm.getBalance()); - } - + @Test void depositRejectsNullMap() { Atm atm = new Atm(); @@ -96,7 +90,7 @@ void additionalTests(AtmCase atmCase) { Atm atm = new Atm(); for (Map deposit : atmCase.deposits) { - atm.deposit(toIntMap(deposit)); + atm.deposit(toMap(deposit)); } if (atmCase.expect.exception != null) { @@ -105,8 +99,8 @@ void additionalTests(AtmCase atmCase) { "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } else { - Map result = atm.withdraw(atmCase.withdraw); - assertEquals(toIntMap(atmCase.expect.dispense), result, "Case: " + atmCase.name); + var result = atm.withdraw(atmCase.withdraw); + assertEquals(toMap(atmCase.expect.dispense), result, "Case: " + atmCase.name); assertEquals(atmCase.expect.balance, atm.getBalance(), "Case: " + atmCase.name); } } @@ -126,7 +120,7 @@ private static List loadCases() throws IOException { } } - private Map toIntMap(Map source) { + private Map toMap(Map source) { Map result = new HashMap<>(); for (Map.Entry entry : source.entrySet()) { result.put(Integer.parseInt(entry.getKey()), entry.getValue()); diff --git a/src/test/java/hse/java/practice/task1/CubeSimpleTest.java b/src/test/java/hse/java/practice/task1/CubeSimpleTest.java new file mode 100644 index 00000000..b13c4af7 --- /dev/null +++ b/src/test/java/hse/java/practice/task1/CubeSimpleTest.java @@ -0,0 +1,277 @@ +package hse.java.practice.task1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public class CubeSimpleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void printTest() { + RubiksCube cube = new RubiksCube(); + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(color, colors[i]); + } + } + } + } + + @Test + void frontClockwise() { + RubiksCube cube = new RubiksCube(); + cube.front(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("frontClockwieseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void frontCounterclockwise() { + RubiksCube cube = new RubiksCube(); + // Поворот против часовой стрелки = 3 поворота по часовой стрелке + cube.front(RotateDirection.COUNTERCLOCKWISE); + + // Проверяем, что 4 поворота против часовой возвращают в исходное состояние + cube.front(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void upClockwise() { + RubiksCube cube = new RubiksCube(); + cube.up(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("upClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void upCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.up(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void downClockwise() { + RubiksCube cube = new RubiksCube(); + cube.down(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("downClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void downCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + cube.down(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void leftClockwise() { + RubiksCube cube = new RubiksCube(); + cube.left(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("leftClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void leftCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + cube.left(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void rightClockwise() { + RubiksCube cube = new RubiksCube(); + cube.right(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("rightClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void rightCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void backClockwise() { + RubiksCube cube = new RubiksCube(); + cube.back(RotateDirection.CLOCKWISE); + + CubeColor[][][] state = readStateFromFile("backClockwiseState.json"); + + CubeColor[][][] actuallyState = Arrays.stream(cube.getEdges()) + .map(Edge::getParts) + .toArray(CubeColor[][][]::new); + + Assertions.assertArrayEquals(state, actuallyState); + } + + @Test + void backCounterclockwise() { + RubiksCube cube = new RubiksCube(); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + cube.back(RotateDirection.COUNTERCLOCKWISE); + + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + @Test + void combinedRotations() { + RubiksCube cube = new RubiksCube(); + + // Выполняем последовательность поворотов + cube.front(RotateDirection.CLOCKWISE); + cube.right(RotateDirection.CLOCKWISE); + cube.up(RotateDirection.CLOCKWISE); + + // Выполняем обратную последовательность + cube.up(RotateDirection.COUNTERCLOCKWISE); + cube.right(RotateDirection.COUNTERCLOCKWISE); + cube.front(RotateDirection.COUNTERCLOCKWISE); + + // Должны вернуться в исходное состояние + CubeColor[] colors = CubeColor.values(); + for (int i = 0; i < 6; i++) { + Edge edge = cube.getEdges()[i]; + CubeColor[][] edgeColors = edge.getParts(); + for (CubeColor[] row : edgeColors) { + for (CubeColor color : row) { + Assertions.assertEquals(colors[i], color); + } + } + } + } + + private CubeColor[][][] readStateFromFile(String fileName) { + String resourcePath = "hse/java/practice/task1/" + fileName; + try (InputStream is = CubeSimpleTest.class.getClassLoader().getResourceAsStream(resourcePath)) { + if (is == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + return MAPPER.readValue(json, CubeColor[][][].class); + } catch (IOException e) { + throw new RuntimeException("Failed to read/parse state file: " + fileName, e); + } + } +}