-
Notifications
You must be signed in to change notification settings - Fork 1
Add test coverage for TypeClasses witness resolution #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
699b3d7
Initial plan
Copilot fd7d704
Add unit tests for ParsedType, FuncType, and Unification
Copilot 8514093
Add comprehensive tests for TypeClasses.witness() behavior
Copilot 8891085
Format test files with google-java-format
Copilot 68b29ba
Address PR feedback: use Ty<T> from api package and assertEquals for …
Copilot c2456dc
Address PR feedback: use custom test type classes, simplify FuncType …
Copilot 69e136b
Switch from JUnit assertions to AssertJ assertions
Copilot b64d0d4
Add test cases for type variables, wildcards, and inline unnecessary …
Copilot d167c72
Inline expected variables and import ParsedType subtypes in FuncTypeTest
Copilot ced34aa
Import ParsedType subtypes in ParsedTypeTest and UnificationTest
Copilot 43a45e3
Replace wildcard imports with explicit imports and inline expected va…
Copilot 72a4ec6
Inline remaining variables in ParsedTypeTest (listType, mapType, nest…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
src/test/java/com/garciat/typeclasses/TypeClassesTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| package com.garciat.typeclasses; | ||
|
|
||
| import static com.garciat.typeclasses.TypeClasses.witness; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| import com.garciat.typeclasses.api.Ctx; | ||
| import com.garciat.typeclasses.api.Ty; | ||
| import com.garciat.typeclasses.testclasses.TestEq; | ||
| import com.garciat.typeclasses.testclasses.TestShow; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| final class TypeClassesTest { | ||
|
|
||
| // ============================================ | ||
| // Basic witness resolution tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessSimpleTypeClass() { | ||
| TestShow<String> showString = witness(new Ty<>() {}); | ||
| assertThat(showString).isNotNull(); | ||
| assertThat(showString.show("test")).isEqualTo("string:test"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessWithDependency() { | ||
| TestShow<Optional<String>> showOptional = witness(new Ty<>() {}); | ||
| assertThat(showOptional).isNotNull(); | ||
| assertThat(showOptional.show(Optional.of("test"))).isEqualTo("opt(string:test)"); | ||
| assertThat(showOptional.show(Optional.empty())).isEqualTo("empty"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessWithMultipleDependencies() { | ||
| TestEq<List<String>> eqList = witness(new Ty<>() {}); | ||
| assertThat(eqList).isNotNull(); | ||
| assertThat(eqList.eq(List.of("a", "b"), List.of("a", "b"))).isTrue(); | ||
| assertThat(eqList.eq(List.of("a", "b"), List.of("a", "c"))).isFalse(); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Witness constructor lookup tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessLookupsInTypeClass() { | ||
| // Lookup should find witnesses in the type class interface (TestShow) | ||
| TestShow<String> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessLookupsInTypeArguments() { | ||
| // Lookup should find witnesses in type arguments (String has witness in TestShow) | ||
| TestShow<Optional<String>> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Public static @TypeClass.Witness annotation tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessRequiresPublicStaticMethod() { | ||
| // This should work - witness is public static | ||
| TestShow<String> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessNotFoundForUnannotatedTypes() { | ||
| // NoWitnessType has no @TypeClass.Witness methods | ||
| assertThatThrownBy(() -> witness(new Ty<TestShow<NoWitnessType>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Recursive dependency resolution tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessRecursiveDependencies() { | ||
| // List<Optional<String>> requires: | ||
| // 1. listShow(TestShow<Optional<String>>) | ||
| // 2. optionalShow(TestShow<String>) | ||
| // 3. stringShow() | ||
| TestShow<List<Optional<String>>> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| assertThat(show.show(List.of(Optional.of("a"), Optional.empty(), Optional.of("b")))) | ||
| .isEqualTo("[opt(string:a),empty,opt(string:b)]"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessDeepRecursion() { | ||
| // List<List<List<String>>> should resolve recursively | ||
| TestShow<List<List<List<String>>>> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Overlapping instances tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void overlappingInstancesMoreSpecificWins() { | ||
| // When we have both general and specific instances, | ||
| // the more specific (overlapping) one should win | ||
| com.garciat.typeclasses.testclasses.OverlapShow<Integer> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| // Should use the specific Integer instance | ||
| assertThat(show.show(42)).isEqualTo("Integer: 42"); | ||
| } | ||
|
|
||
| @Test | ||
| void overlappableInstanceCanBeOverridden() { | ||
| // OVERLAPPABLE instances can be overridden by more specific ones | ||
| com.garciat.typeclasses.testclasses.OverlapShow<String> show = witness(new Ty<>() {}); | ||
| assertThat(show).isNotNull(); | ||
| assertThat(show.show("test")).isEqualTo("String: test"); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Ambiguity detection tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void ambiguousWitnessesThrow() { | ||
| // AmbiguousShow has two witness constructors without overlap markers | ||
| assertThatThrownBy( | ||
| () -> witness(new Ty<com.garciat.typeclasses.testclasses.AmbiguousShow<String>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Not found error tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessNotFoundThrows() { | ||
| assertThatThrownBy(() -> witness(new Ty<TestShow<NoWitnessType>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessNotFoundNestedThrows() { | ||
| // List<NoWitnessType> - the dependency TestShow<NoWitnessType> cannot be found | ||
| assertThatThrownBy(() -> witness(new Ty<TestShow<List<NoWitnessType>>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Context/witness summoning tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessSummoningWithContext() { | ||
| // Provide a custom witness via context | ||
| CustomType customValue = new CustomType("test"); | ||
| TestShow<CustomType> customShow = c -> "custom:" + c.value; | ||
|
|
||
| TestShow<List<CustomType>> listShow = witness(new Ty<>() {}, new Ctx<>(customShow) {}); | ||
|
|
||
| assertThat(listShow).isNotNull(); | ||
| assertThat(listShow.show(List.of(new CustomType("a"), new CustomType("b")))) | ||
| .isEqualTo("[custom:a,custom:b]"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessSummoningBuildsTree() { | ||
| // Verify that the witness is actually constructed correctly | ||
| // by checking its behavior with nested types | ||
| TestShow<Optional<List<String>>> show = witness(new Ty<>() {}); | ||
|
|
||
| assertThat(show.show(Optional.of(List.of("a", "b", "c")))) | ||
| .isEqualTo("opt([string:a,string:b,string:c])"); | ||
| assertThat(show.show(Optional.empty())).isEqualTo("empty"); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // WitnessResolutionException tests | ||
| // ============================================ | ||
|
|
||
| @Test | ||
| void witnessResolutionExceptionHasMessage() { | ||
| assertThatThrownBy(() -> witness(new Ty<TestShow<NoWitnessType>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class) | ||
| .hasMessageContaining("NoWitnessType"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessResolutionExceptionForAmbiguous() { | ||
| assertThatThrownBy( | ||
| () -> witness(new Ty<com.garciat.typeclasses.testclasses.AmbiguousShow<String>>() {})) | ||
| .isInstanceOf(TypeClasses.WitnessResolutionException.class) | ||
| .hasMessageContaining("mbiguous"); | ||
| } | ||
|
|
||
| @Test | ||
| void witnessMapWithDependencies() { | ||
| // Map<String, Integer> requires TestEq<String> and TestEq<Integer> | ||
| TestEq<Map<String, Integer>> eqMap = witness(new Ty<>() {}); | ||
| assertThat(eqMap).isNotNull(); | ||
|
|
||
| Map<String, Integer> map1 = Map.of("a", 1, "b", 2); | ||
| Map<String, Integer> map2 = Map.of("a", 1, "b", 2); | ||
| Map<String, Integer> map3 = Map.of("a", 1, "b", 3); | ||
|
|
||
| assertThat(eqMap.eq(map1, map2)).isTrue(); | ||
| assertThat(eqMap.eq(map1, map3)).isFalse(); | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Test helper classes | ||
| // ============================================ | ||
|
|
||
| static class NoWitnessType { | ||
| String value; | ||
| } | ||
|
|
||
| static class CustomType { | ||
| String value; | ||
|
|
||
| CustomType(String value) { | ||
| this.value = value; | ||
| } | ||
| } | ||
| } |
114 changes: 114 additions & 0 deletions
114
src/test/java/com/garciat/typeclasses/impl/FuncTypeTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package com.garciat.typeclasses.impl; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| import com.garciat.typeclasses.impl.ParsedType.App; | ||
| import com.garciat.typeclasses.impl.ParsedType.Const; | ||
| import com.garciat.typeclasses.impl.ParsedType.Primitive; | ||
| import com.garciat.typeclasses.impl.ParsedType.Var; | ||
| import java.lang.reflect.Method; | ||
| import java.util.List; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| final class FuncTypeTest { | ||
| @Test | ||
| void parseSimpleStaticMethod() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("simple"); | ||
|
|
||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo(new FuncType(method, List.of(), new Const(String.class))); | ||
| } | ||
|
|
||
| @Test | ||
| void parseMethodWithParameters() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("withParams", Integer.class, String.class); | ||
|
|
||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo( | ||
| new FuncType( | ||
| method, | ||
| List.of(new Const(Integer.class), new Const(String.class)), | ||
| new Const(Boolean.class))); | ||
| } | ||
|
|
||
| @Test | ||
| void parseMethodWithGenericReturn() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("genericReturn"); | ||
|
|
||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo( | ||
| new FuncType( | ||
| method, List.of(), new App(new Const(List.class), new Const(String.class)))); | ||
| } | ||
|
|
||
| @Test | ||
| void parseMethodWithGenericParams() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("genericParams", List.class); | ||
|
|
||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo( | ||
| new FuncType( | ||
| method, | ||
| List.of(new App(new Const(List.class), new Const(Integer.class))), | ||
| new Primitive(void.class))); | ||
| } | ||
|
|
||
| @Test | ||
| void parseMethodWithPrimitives() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("withPrimitives", int.class, boolean.class); | ||
|
|
||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo( | ||
| new FuncType( | ||
| method, | ||
| List.of(new Primitive(int.class), new Primitive(boolean.class)), | ||
| new Primitive(void.class))); | ||
| } | ||
|
|
||
| @Test | ||
| void parseNonStaticMethodThrows() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("nonStatic"); | ||
| assertThatThrownBy(() -> FuncType.parse(method)).isInstanceOf(IllegalArgumentException.class); | ||
| } | ||
|
|
||
| @Test | ||
| void parseGenericMethodWithTypeParameters() throws Exception { | ||
| Method method = TestMethods.class.getDeclaredMethod("genericMethod", Object.class); | ||
|
|
||
| // The method has a type parameter T, so we expect a Var in the return type | ||
| assertThat(FuncType.parse(method)) | ||
| .isEqualTo( | ||
| new FuncType( | ||
| method, | ||
| List.of(new Var(method.getTypeParameters()[0])), | ||
| new Var(method.getTypeParameters()[0]))); | ||
| } | ||
|
|
||
| // Test helper class with various method signatures | ||
| private static class TestMethods { | ||
|
Garciat marked this conversation as resolved.
|
||
| public static String simple() { | ||
| return ""; | ||
| } | ||
|
|
||
| public static Boolean withParams(Integer i, String s) { | ||
| return true; | ||
| } | ||
|
|
||
| public static List<String> genericReturn() { | ||
| return List.of(); | ||
| } | ||
|
|
||
| public static void genericParams(List<Integer> list) {} | ||
|
|
||
| public static void withPrimitives(int i, boolean b) {} | ||
|
|
||
| public static <T> T genericMethod(T value) { | ||
| return value; | ||
| } | ||
|
|
||
| public String nonStatic() { | ||
| return ""; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.