diff --git a/src/tests/gitlab/ckpt2/HashTrieMapTests.java b/src/tests/gitlab/ckpt2/HashTrieMapTests.java new file mode 100644 index 0000000000000000000000000000000000000000..a66e25a6b1b5edb199d107fdf3ab93c574ace3d2 --- /dev/null +++ b/src/tests/gitlab/ckpt2/HashTrieMapTests.java @@ -0,0 +1,592 @@ +package tests.gitlab.duedate; + +import cse332.types.AlphabeticString; +import datastructures.dictionaries.HashTrieMap; +import tests.TestsUtility; + +import java.util.HashMap; +import java.util.Map; + +public class HashTrieMapTests extends TestsUtility { + protected static HashTrieMap<Character, AlphabeticString, String> STUDENT; + + public static void main(String[] args) { + new HashTrieMapTests().run(); + } + + public static void init() { + STUDENT = new HashTrieMap<>(AlphabeticString.class); + } + + @Override + protected void run() { + SHOW_TESTS = true; + PRINT_TESTERR = true; + DEBUG = true; + + test("testBasic"); + test("testBasicDelete"); + + test("testFindPrefixes"); + test("testFindNonexistentDoesNotCrash"); + test("testFindingNullEntriesCausesError"); + + test("testInsertReplacesOldValue"); + test("testInsertingNullEntriesCausesError"); + + test("testDeleteAll"); + test("testDeleteNothing"); + test("testDeleteAndInsertSingleChars"); + test("testDeleteWorksWhenTrieHasNoBranches"); + test("testDeletingAtRoot"); + test("testDeletingEmptyString"); + test("testDeletingNullEntriesCausesError"); + + test("testClear"); + test("checkUnderlyingStructure"); + test("stressTest"); + + finish(); + } + + /** + * Tests if insert, find, and findPrefix work in general. + */ + public static int testBasic() { + String[] words = {"dog", "doggy", "doge", "dragon", "cat", "draggin"}; + String[] invalid = {"d", "cataract", "", "do"}; + addAll(STUDENT, words); + return (containsAllPaths(STUDENT, words) && doesNotContainAll(STUDENT, invalid)) ? 1 : 0; + } + + /** + * Checks to see if basic delete functionality works. + */ + public static int testBasicDelete() { + String[] words = {"dog", "doggy", "dreamer", "cat"}; + addAll(STUDENT, words); + if (!containsAllPaths(STUDENT, words)) { + return 0; + } + + STUDENT.delete(a("I don't exist")); + STUDENT.delete(a("dreamer")); + + if (!containsAllPaths(STUDENT, "dog", "doggy", "cat") && + !containsAllPrefixes(STUDENT, "dreamer", "dreame", "dream", "drea", "dre", "dr") && + !STUDENT.findPrefix(a("d"))) { + return 0; + } + + STUDENT.delete(a("dog")); + if (!containsAllPaths(STUDENT, "doggy", "cat")) { + return 0; + } + + STUDENT.delete(a("doggy")); + return containsAllPaths(STUDENT, "cat") ? 1 : 0; + } + + /** + * Test findPrefix more rigorously. + */ + public static int testFindPrefixes() { + String[] words = {"dog", "doggy", "doge", "dragon", "cat", "draggin"}; + addAll(STUDENT, words); + + boolean allPrefixesFound = containsAllPrefixes(STUDENT, "d", "", "do"); + boolean invalidPrefixesIgnored = doesNotContainAllPrefixes(STUDENT, "batarang", "dogee", "dragging"); + + return allPrefixesFound && invalidPrefixesIgnored ? 1 : 0; + } + + /** + * Tests that trying to find a non-existent entity does the correct thing + */ + public static int testFindNonexistentDoesNotCrash() { + addAll(STUDENT, "foo", "bar", "baz"); + return STUDENT.find(a("orangutan")) == null && STUDENT.find(a("z")) == null && + STUDENT.find(a("ba")) == null && STUDENT.find(a("bazz")) == null && + !STUDENT.findPrefix(a("boor")) && !STUDENT.findPrefix(a("z")) ? 1 : 0; + } + + public static int testFindingNullEntriesCausesError() { + try { + STUDENT.find(null); + return 0; + } catch (IllegalArgumentException ex) { + // Do nothing + } + try { + STUDENT.findPrefix(null); + return 0; + } catch (IllegalArgumentException ex) { + // Do nothing + } + return 1; + } + + /** + * Tests that inserts correctly wipe out old values. + */ + public static int testInsertReplacesOldValue() { + AlphabeticString key = a("myKey"); + boolean initialValueIsNull = STUDENT.insert(key, "foo") == null; + boolean originalValueReturned = STUDENT.insert(key, "bar").equals("foo"); + boolean replacementValueReturned = STUDENT.insert(key, "baz").equals("bar"); + + return initialValueIsNull && originalValueReturned && replacementValueReturned ? 1 : 0; + } + + public static int testInsertingNullEntriesCausesError() { + try { + STUDENT.insert(null, "foo"); + return 0; + } catch (IllegalArgumentException ex) { + // Do nothing + } + + try { + STUDENT.insert(a("foo"), null); + return 0; + } catch (IllegalArgumentException ex) { + // Do nothing + } + + return 1; + } + + /** + * Checks to see the trie correctly handles the case where you delete + * absolutely everything. + */ + public static int testDeleteAll() { + AlphabeticString keyA = a("keyboard"); + AlphabeticString keyB = a("keyesian"); + AlphabeticString keyC = a("bayesian"); + + if (STUDENT.size() != 0 || !STUDENT.isEmpty()) { + return 0; + } + + STUDENT.insert(keyA, "KEYBOARD"); + STUDENT.insert(keyB, "KEYESIAN"); + STUDENT.insert(keyC, "BAYESIAN"); + + if (!containsAllPaths(STUDENT, "keyboard", "keyesian", "bayesian")) { + return 0; + } + if (STUDENT.size() != 3 || STUDENT.isEmpty()) { + return 0; + } + + STUDENT.delete(keyA); + STUDENT.delete(keyB); + STUDENT.delete(keyC); + + if (STUDENT.size() != 0 || !STUDENT.isEmpty()) { + return 0; + } + + return doesNotContainAll(STUDENT, "keyboard", "keyesian", "bayesian") ? 1 : 0; + } + + /** + * Tests what happens if you attempt deleting something that doesn't exist + * in the trie (but _does_ partially overlap). + */ + public static int testDeleteNothing() { + STUDENT.insert(a("aaaa"), "foo"); + + if (!containsPath(STUDENT, "aaaa", "foo") || STUDENT.size() != 1 || STUDENT.isEmpty()) { + return 0; + } + + // Should not change the trie + STUDENT.delete(a("aa")); + STUDENT.delete(a("a")); + STUDENT.delete(a("abc")); + STUDENT.delete(a("aaaaa")); + STUDENT.delete(a("")); + STUDENT.delete(a("foobar")); + + if (!containsPath(STUDENT, "aaaa", "foo") || STUDENT.size() != 1 || STUDENT.isEmpty()) { + return 0; + } + + return 1; + } + + /** + * Tests what happens if you try deleting and inserting single characters + */ + public static int testDeleteAndInsertSingleChars() { + STUDENT.insert(a("a"), "A"); + STUDENT.insert(a("b"), "B"); + + STUDENT.delete(a("a")); + STUDENT.insert(a("b"), "BB"); + + MockNode expected = node() + .branch('b', node("BB")); + + return equals(expected, getField(STUDENT, "root")) ? 1 : 0; + } + + /** + * Tests to see if HashTrieMap correctly handles a trie where everything is in a straight + * line/the trie has no branching. + */ + public static int testDeleteWorksWhenTrieHasNoBranches() { + AlphabeticString keyA = a("ghost"); + AlphabeticString keyB = a("gh"); + STUDENT.insert(keyA, "A"); + STUDENT.insert(keyB, "B"); + + // Trie should still contain "ghost -> A" + STUDENT.delete(keyB); + if (STUDENT.find(keyB) != null || !STUDENT.findPrefix(keyB) || !STUDENT.find(keyA).equals("A")) { + return 0; + } + + // Trie should now contain "gh -> C", but not "ghost -> A" + STUDENT.insert(keyB, "C"); + STUDENT.delete(keyA); + + return (STUDENT.find(keyB).equals("C") && STUDENT.find(keyA) == null && !STUDENT.findPrefix(a("gho"))) ? 1 : 0; + } + + /** + * A slight variation of the previous test. + */ + public static int testDeletingAtRoot() { + STUDENT.insert(a(""), "foo"); + STUDENT.insert(a("a"), "bar"); + STUDENT.delete(a("a")); + STUDENT.insert(a("b"), "baz"); + if (STUDENT.find(a("a")) != null) { + return 0; + } + if (!"foo".equals(STUDENT.find(a("")))) { + return 0; + } + if (!"baz".equals(STUDENT.find(a("b")))) { + return 0; + } + MockNode expected = new MockNode("foo") + .branch('b', new MockNode("baz")); + return equals(expected, getField(STUDENT, "root")) ? 1 : 0; + } + + /** + * Tests that just working with empty strings does the correct thing. + */ + public static int testDeletingEmptyString() { + STUDENT.insert(a(""), "Foo"); + if (!"Foo".equals(STUDENT.find(a(""))) || STUDENT.size() != 1 || STUDENT.isEmpty()) { + return 0; + } + + STUDENT.delete(a("")); + + if (STUDENT.find(a("")) != null || STUDENT.size() > 0 && !STUDENT.isEmpty()) { + return 0; + } + + STUDENT.insert(a(""), "Bar"); + return "Bar".equals(STUDENT.find(a(""))) && STUDENT.size() == 1 && !STUDENT.isEmpty() ? 1 : 0; + } + + public static int testDeletingNullEntriesCausesError() { + try { + STUDENT.delete((AlphabeticString) null); + } catch (IllegalArgumentException ex) { + return 1; + } + return 0; + } + + public static int testClear() { + addAll(STUDENT, "keyboard", "keyesian", "bayesian"); + STUDENT.clear(); + + return (STUDENT.size() == 0 && + STUDENT.isEmpty() && + doesNotContainAll(STUDENT, "keyboard", "keyesian", "bayesian")) ? 1 : 0; + } + + public static int checkUnderlyingStructure() { + STUDENT.insert(a(""), "A"); + STUDENT.insert(a("foo"), "B"); + STUDENT.insert(a("fez"), "C"); + STUDENT.insert(a("fezzy"), "D"); + STUDENT.insert(a("jazz"), "E"); + STUDENT.insert(a("jazzy"), "F"); + + MockNode fullExpected = node("A") + .branch('f', node() + .branch('o', node() + .branch('o', node("B"))) + .branch('e', node() + .branch('z', node("C") + .branch('z', node() + .branch('y', node("D")))))) + .branch('j', node() + .branch('a', node() + .branch('z', node() + .branch('z', node("E") + .branch('y', node("F")))))); + + if (!equals(fullExpected, getField(STUDENT, "root"))) { + return 0; + } + + STUDENT.delete(a("fezzy")); + STUDENT.delete(a("jazz")); + + MockNode delete1 = node("A") + .branch('f', node() + .branch('o', node() + .branch('o', node("B"))) + .branch('e', node() + .branch('z', node("C")))) + .branch('j', node() + .branch('a', node() + .branch('z', node() + .branch('z', node() + .branch('y', node("F")))))); + + if (!equals(delete1, getField(STUDENT, "root"))) { + return 0; + } + + STUDENT.delete(a("")); + STUDENT.delete(a("foo")); + STUDENT.delete(a("jazz")); // should do nothing + + MockNode delete2 = node() + .branch('f', node() + .branch('e', node() + .branch('z', node("C")))) + .branch('j', node() + .branch('a', node() + .branch('z', node() + .branch('z', node() + .branch('y', node("F")))))); + + if (!equals(delete2, getField(STUDENT, "root"))) { + return 0; + } + + STUDENT.insert(a("f"), "Z"); + STUDENT.delete(a("jazzy")); + STUDENT.delete(a("fez")); + + MockNode delete3 = node().branch('f', node("Z")); + + if (!equals(delete3, getField(STUDENT, "root"))) { + return 0; + } + + STUDENT.delete(a("f")); + + boolean rootIsSingleNode = equals(node(), getField(STUDENT, "root")); + boolean rootIsNull = equals(null, getField(STUDENT, "root")); + if (!(rootIsSingleNode || rootIsNull)) { + return 0; + } + + return 1; + + } + + protected static boolean equals(MockNode expected, HashTrieMap<Character, AlphabeticString, String>.HashTrieNode student) { + if (expected == null && student == null) { + return true; + } else if (expected == null || student == null) { + // If only one of the two is null + return false; + } else if (expected.value != null && !expected.value.equals(student.value)) { + // If values don't match + return false; + } else if (expected.value == null && student.value != null) { + // If only one of the values are null + return false; + } else if (expected.pointers.size() != student.pointers.size()) { + // If number of pointers is not the same + return false; + } else { + // If student doesn't contain the given char, 'equals' will fail one level down + // in one of the base cases + for (char c : expected.pointers.keySet()) { + boolean result = equals(expected.pointers.get(c), student.pointers.get(c)); + if (!result) { + return false; + } + } + return true; + } + } + + protected static MockNode node() { + return new MockNode(); + } + + protected static MockNode node(String value) { + return new MockNode(value); + } + + protected static class MockNode { + public Map<Character, MockNode> pointers; + public String value; + + public MockNode() { + this(null); + } + + public MockNode(String value) { + this.pointers = new HashMap<>(); + this.value = value; + } + + public MockNode branch(char c, MockNode child) { + this.pointers.put(c, child); + return this; + } + } + + public static int stressTest() { + // Should contain 30 characters + char[] symbols = "abcdefghijklmnopqrstuvwxyz!@#$".toCharArray(); + long i = 0; + for (char a : symbols) { + for (char b : symbols) { + for (char c : symbols) { + for (char d : symbols) { + Character[] word = new Character[]{a, b, c, d}; + STUDENT.insert(new AlphabeticString(word), "" + i); + i += 1; + } + } + } + } + + for (char a : symbols) { + for (char b : symbols) { + if (!STUDENT.findPrefix(new AlphabeticString(new Character[]{a, b}))) { + return 0; + } + } + } + + i = 0; + for (char a : symbols) { + for (char b : symbols) { + for (char c : symbols) { + for (char d : symbols) { + Character[] word = new Character[]{a, b, c, d}; + if (!STUDENT.find(new AlphabeticString(word)).equals("" + i)) { + return 0; + } + i += 1; + } + } + } + } + + return 1; + } + + /** + * Converts a String into an AlphabeticString + */ + private static AlphabeticString a(String s) { + return new AlphabeticString(s); + } + + /** + * Checks if the trie contains the word and the expected value, and that all prefixes of + * the word exist in the trie. + */ + private static boolean containsPath(HashTrieMap<Character, AlphabeticString, String> trie, String word, String expectedValue) { + AlphabeticString key = a(word); + + boolean valueCorrect = expectedValue.equals(trie.find(key)); + boolean fullWordIsPrefix = trie.findPrefix(key); + boolean invalidWordDoesNotExist = trie.find(a(word + "$")) == null; + + if (!valueCorrect || !fullWordIsPrefix || !invalidWordDoesNotExist) { + return false; + } + + return allPrefixesExist(trie, word); + } + + /** + * Checks if the trie contains the word, and that all prefixes of the word exist in the trie. + * + * Assumes that the expected value is word.toUpperCase(). + */ + private static boolean containsPath(HashTrieMap<Character, AlphabeticString, String> trie, String word) { + return containsPath(trie, word, word.toUpperCase()); + } + + /** + * Returns true if all prefixes of a word exist in the trie. + * + * That is, if we do `trie.insert(new AlphabeticString("dog"), "some-value")`, this method + * would check to see if "dog", "do", "d", and "" are all prefixes of the trie. + */ + private static boolean allPrefixesExist(HashTrieMap<Character, AlphabeticString, String> trie, String word) { + String accum = ""; + for (char c : word.toCharArray()) { + accum += c; + if (!trie.findPrefix(a(accum))) { + return false; + } + } + return true; + } + + private static boolean containsAllPaths(HashTrieMap<Character, AlphabeticString, String> trie, String... words) { + for (String word : words) { + if (!containsPath(trie, word)) { + return false; + } + } + return true; + } + + private static boolean doesNotContainAll(HashTrieMap<Character, AlphabeticString, String> trie, String... words) { + for (String word : words) { + if (trie.find(a(word)) != null) { + return false; + } + } + return true; + } + + private static boolean containsAllPrefixes(HashTrieMap<Character, AlphabeticString, String> trie, String... words) { + for (String word : words) { + if (!trie.findPrefix(a(word))) { + return false; + } + } + return true; + } + + private static boolean doesNotContainAllPrefixes(HashTrieMap<Character, AlphabeticString, String> trie, String... words) { + for (String word : words) { + if (trie.findPrefix(a(word))) { + return false; + } + } + return true; + } + + private static void addAll(HashTrieMap<Character, AlphabeticString, String> trie, String... words) { + for (String word : words) { + trie.insert(a(word), word.toUpperCase()); + } + } +}