diff --git a/.editorconfig b/.editorconfig index 2375dc151..ec8a7ef51 100644 --- a/.editorconfig +++ b/.editorconfig @@ -392,7 +392,6 @@ ij_groovy_method_parameters_right_paren_on_new_line = false ij_groovy_method_parameters_wrap = off ij_groovy_modifier_list_wrap = false ij_groovy_names_count_to_use_import_on_demand = 999999 -ij_groovy_packages_to_use_import_on_demand = empty ij_groovy_parameter_annotation_wrap = off ij_groovy_parentheses_expression_new_line_after_left_paren = false ij_groovy_parentheses_expression_right_paren_on_new_line = false diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java index a459dadcf..3076fea81 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java @@ -15,25 +15,27 @@ */ package io.github.ascopes.jct.assertions; -import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo; -import static io.github.ascopes.jct.utils.IterableUtils.combineOneOrMore; -import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; -import static java.util.Objects.requireNonNull; -import static java.util.function.Predicate.not; -import static org.assertj.core.api.Assertions.assertThat; - import io.github.ascopes.jct.containers.PackageContainerGroup; import io.github.ascopes.jct.repr.LocationRepresentation; import io.github.ascopes.jct.utils.StringUtils; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractPathAssert; +import org.jspecify.annotations.Nullable; import java.nio.file.Path; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.assertj.core.api.AbstractPathAssert; -import org.jspecify.annotations.Nullable; + +import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo; +import static io.github.ascopes.jct.utils.IterableUtils.combineOneOrMore; +import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; +import static io.github.ascopes.jct.utils.StringUtils.quoted; +import static io.github.ascopes.jct.utils.StringUtils.quotedIterable; +import static java.util.Objects.requireNonNull; +import static java.util.function.Predicate.not; +import static org.assertj.core.api.Assertions.assertThat; /** * Assertions for package container groups. @@ -65,10 +67,11 @@ public PackageContainerGroupAssert(@Nullable PackageContainerGroup containerGrou * @throws NullPointerException if any of the paths are null. */ public PackageContainerGroupAssert allFilesExist(String path, String... paths) { - requireNonNull(path, "path must not be null"); + requireNonNull(path, "path"); requireNonNullValues(paths, "paths"); - return allFilesExist(combineOneOrMore(path, paths)); + allFilesExist(combineOneOrMore(path, paths)); + return this; } /** @@ -85,7 +88,12 @@ public PackageContainerGroupAssert allFilesExist(Iterable paths) { isNotNull(); - assertThat(paths).allSatisfy(this::fileExists); + assertThat(paths) + .withFailMessage( + "Expected all paths in %s to exist but one or more did not", + quotedIterable(paths) + ) + .allSatisfy(this::fileExists); return this; } @@ -119,7 +127,7 @@ public ClassLoaderAssert classLoader() { * @throws NullPointerException if any of the fragments are null. */ public PackageContainerGroupAssert fileDoesNotExist(String fragment, String... fragments) { - requireNonNull(fragment, "fragment must not be null"); + requireNonNull(fragment, "fragment"); requireNonNullValues(fragments, "fragments"); isNotNull(); @@ -131,10 +139,10 @@ public PackageContainerGroupAssert fileDoesNotExist(String fragment, String... f } throw failure( - "Expected path \"%s\" to not exist in \"%s\" but it was found at \"%s\"", - userProvidedPath(combineOneOrMore(fragment, fragments)), + "Expected path %s to not exist in %s but it was found at %s", + quotedUserProvidedPath(combineOneOrMore(fragment, fragments)), LocationRepresentation.getInstance().toStringOf(actual.getLocation()), - actualFile + quoted(actualFile) ); } @@ -160,7 +168,7 @@ public PackageContainerGroupAssert fileDoesNotExist(String fragment, String... f */ public AbstractPathAssert fileExists(String fragment, String... fragments) { - requireNonNull(fragment, "fragment must not be null"); + requireNonNull(fragment, "fragment"); requireNonNullValues(fragments, "fragments"); isNotNull(); @@ -175,11 +183,11 @@ public AbstractPathAssert fileExists(String fragment, String... fragments) { var expected = combineOneOrMore(fragment, fragments); throw failure(StringUtils.resultNotFoundWithFuzzySuggestions( fuzzySafePath(expected), - userProvidedPath(expected), + quotedUserProvidedPath(expected), listAllUniqueFilesForAllContainers(), this::fuzzySafePath, - this::userProvidedPath, - "path" + this::quotedUserProvidedPath, + "file with relative path" )); } @@ -198,11 +206,14 @@ private Set listAllUniqueFilesForAllContainers() { .collect(Collectors.toSet()); } - private String userProvidedPath(Iterable parts) { + private String quotedUserProvidedPath(Iterable parts) { return StreamSupport .stream(parts.spliterator(), false) .map(Objects::toString) - .collect(Collectors.joining("/")); + .collect(Collectors.collectingAndThen( + Collectors.joining("/"), + StringUtils::quoted + )); } private String fuzzySafePath(Iterable parts) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java index f86fbb7a4..018104910 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java @@ -18,7 +18,6 @@ import static org.mockito.ArgumentMatchers.argThat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Set; import org.mockito.ArgumentMatcher; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/assertions/PackageContainerGroupAssertTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/assertions/PackageContainerGroupAssertTest.java new file mode 100644 index 000000000..0a55d025c --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/assertions/PackageContainerGroupAssertTest.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.unit.assertions; + +import io.github.ascopes.jct.assertions.PackageContainerGroupAssert; +import io.github.ascopes.jct.containers.Container; +import io.github.ascopes.jct.containers.PackageContainerGroup; +import io.github.ascopes.jct.repr.LocationRepresentation; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.quality.Strictness; +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePathRoot; +import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo; +import static io.github.ascopes.jct.utils.StringUtils.quoted; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * {@link PackageContainerGroupAssert} tests. + * + * @author Ashley Scopes + */ +@DisplayName("PackageContainerGroupAssert tests") +class PackageContainerGroupAssertTest { + + @DisplayName("PackageContainerGroupAssert#allFilesExist(String, String...) tests") + @Nested + class AllFilesExistStringArrayTest { + + @DisplayName(".allFilesExist(String, String...) throws an exception if the string is null") + @Test + void allFilesExistThrowsExceptionIfFirstStringIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.allFilesExist(null, new String[0])) + .isInstanceOf(NullPointerException.class) + .hasMessage("path"); + } + + @DisplayName(".allFilesExist(String, String...) throws an exception if the array is null") + @Test + void allFilesExistThrowsExceptionIfArrayIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.allFilesExist("foo", (String[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("paths"); + } + + @DisplayName( + ".allFilesExist(String, String...) throws an exception if the array has null values" + ) + @Test + void allFilesExistThrowsExceptionIfArrayHasNullValues() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.allFilesExist("foo", "bar", "baz", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("paths[2]"); + } + + @DisplayName(".allFilesExist(String, String...) calls .allFilesExist(Iterable)") + @Test + void allFilesExistCallsIterableStringOverload() { + // Given + var assertions = mock(PackageContainerGroupAssert.class); + when(assertions.allFilesExist(any(), any(), any())).thenCallRealMethod(); + + // When + var result = assertions.allFilesExist("foo", "bar", "baz"); + + // Then + verify(assertions).allFilesExist("foo", "bar", "baz"); + verify(assertions).allFilesExist(List.of("foo", "bar", "baz")); + verifyNoMoreInteractions(assertions); + assertThat(result).isSameAs(assertions); + } + } + + // Not entirely sure what is defining the precedence between .allFilesExist(null) calling + // .allFilesExist(String, String...) or .allFilesExist(Iterable) here, so I am keeping + // this explicit to prevent the test behaviour changing in the future. + @SuppressWarnings("RedundantCast") + @DisplayName("PackageContainerGroupAssert#allFilesExist(Iterable) tests") + @Nested + class AllFilesExistStringIterableTest { + + @DisplayName(".allFilesExist(Iterable) throws an exception if the iterable is null") + @Test + void allFilesExistThrowsExceptionIfIterableIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.allFilesExist((Iterable) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("paths"); + } + + @DisplayName( + ".allFilesExist(Iterable) throws an exception if the iterable has null members" + ) + @Test + void allFilesExistThrowsExceptionIfIterableHasNullMembers() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + // Arrays#asList does not NPE if any members are null. List#of does throw NPE. + assertThatThrownBy(() -> assertions.allFilesExist(Arrays.asList("foo", "bar", null))) + .isInstanceOf(NullPointerException.class) + .hasMessage("paths[2]"); + } + + @DisplayName( + ".allFilesExist(Iterable) throws an exception if the container group is null" + ) + @Test + void allFilesExistThrowsExceptionIfContainerGroupIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(null); + + // Then + // Arrays#asList does not NPE if any members are null. List#of does throw NPE. + assertThatThrownBy(() -> assertions.allFilesExist(List.of())) + .isInstanceOf(AssertionError.class); + } + + @DisplayName(".allFilesExist(Iterable) evaluates all failures") + @Test + void allFilesExistEvaluatesAllFailures() { + // Given + var assertions = mock(PackageContainerGroupAssert.class); + when(assertions.allFilesExist(anyIterable())).thenCallRealMethod(); + when(assertions.fileExists(any())) + .then(ctx -> fail("%s", ctx.getArgument(0))); + + // Then + assertThatThrownBy(() -> assertions.allFilesExist(List.of("foo", "bar", "baz", "bork"))) + .isInstanceOf(AssertionError.class) + .message() + .satisfies( + message -> assertThat(message).contains("foo", "bar", "baz", "bork"), + message -> assertThat(message) + .containsPattern("Expected all paths in .*? to exist but one or more did not") + ); + + verify(assertions).allFilesExist(List.of("foo", "bar", "baz", "bork")); + verify(assertions).isNotNull(); + verify(assertions).fileExists("foo"); + verify(assertions).fileExists("bar"); + verify(assertions).fileExists("baz"); + verify(assertions).fileExists("bork"); + verifyNoMoreInteractions(assertions); + } + + @DisplayName(".allFilesExist(Iterable) returns the assertions on success") + @Test + void allFilesExistReturnsTheAssertionsOnSuccess() { + // Given + var assertions = mock(PackageContainerGroupAssert.class); + when(assertions.allFilesExist(anyIterable())).thenCallRealMethod(); + + // When + var result = assertions.allFilesExist(List.of("foo", "bar", "baz", "bork")); + + // Then + assertThat(result).isSameAs(assertions); + } + } + + @DisplayName("PackageContainerGroupAssert#classLoader tests") + @Nested + class ClassLoaderTest { + + @DisplayName(".classLoader() fails if the container group is null") + @Test + void classLoaderFailsIfContainerGroupIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(null); + + // Then + assertThatThrownBy(assertions::classLoader) + .isInstanceOf(AssertionError.class); + } + + @DisplayName(".classLoader() returns assertions for the class loader") + @Test + void classLoaderReturnsAssertionsForTheClassLoader() { + // Given + var containerGroup = mock(PackageContainerGroup.class); + var classLoader = mock(ClassLoader.class); + when(containerGroup.getClassLoader()).thenReturn(classLoader); + var assertions = new PackageContainerGroupAssert(containerGroup); + + // When + var result = assertions.classLoader(); + + // Then + assertThatCode(() -> result.isSameAs(classLoader)) + .doesNotThrowAnyException(); + } + } + + @DisplayName("PackageContainerGroupAssert#fileDoesNotExist tests") + @Nested + class FileDoesNotExistTest { + + @DisplayName(".fileDoesNotExist(String, String...) throws an exception if the string is null") + @Test + void fileDoesNotExistThrowsExceptionIfFirstStringIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileDoesNotExist(null, new String[0])) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragment"); + } + + @DisplayName(".fileDoesNotExist(String, String...) throws an exception if the array is null") + @Test + void fileDoesNotExistThrowsExceptionIfArrayIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileDoesNotExist("foo", (String[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragments"); + } + + @DisplayName( + ".fileDoesNotExist(String, String...) throws an exception if the array has null values" + ) + @Test + void fileDoesNotExistThrowsExceptionIfArrayHasNullValues() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileDoesNotExist("foo", "bar", "baz", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragments[2]"); + } + + @DisplayName( + ".fileDoesNotExist(String, String...) throws an exception if the container group is null" + ) + @Test + void fileDoesNotExistThrowsExceptionIfContainerGroupIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(null); + + // Then + // Arrays#asList does not NPE if any members are null. List#of does throw NPE. + assertThatThrownBy(() -> assertions.fileDoesNotExist("foo", "bar", "baz")) + .isInstanceOf(AssertionError.class); + } + + @DisplayName(".fileDoesNotExist(String, String...) fails if the file exists") + @Test + void fileDoesNotExistFailsIfFileExists() { + // Given + var actualFile = Path.of("foo", "bar", "baz"); + var location = someLocation(); + + var containerGroup = mock(PackageContainerGroup.class); + when(containerGroup.getFile(any(), any(), any())).thenReturn(actualFile); + when(containerGroup.getLocation()).thenReturn(location); + + var assertions = new PackageContainerGroupAssert(containerGroup); + + // Then + assertThatThrownBy(() -> assertions.fileDoesNotExist("foo", "bar", "baz")) + .isInstanceOf(AssertionError.class) + .hasMessageContaining( + "Expected path %s to not exist in %s but it was found at %s", + quoted("foo/bar/baz"), + LocationRepresentation.getInstance().toStringOf(location), + quoted(actualFile) + ); + } + + @DisplayName(".fileDoesNotExist(String, String...) succeeds if the file does not exist") + @Test + void fileDoesNotExistSucceedsIfFileDoesNotExist() { + // Given + var containerGroup = mock(PackageContainerGroup.class); + when(containerGroup.getFile(any(), any(), any())).thenReturn(null); + + var assertions = new PackageContainerGroupAssert(containerGroup); + + // When + var result = assertions.fileDoesNotExist("foo", "bar", "baz"); + + // Then + verify(containerGroup).getFile("foo", "bar", "baz"); + verifyNoMoreInteractions(containerGroup); + + assertThatCode(() -> result.isSameAs(containerGroup)) + .withFailMessage("Expected returned assertions to be for the same container group") + .doesNotThrowAnyException(); + } + } + + @DisplayName("PackageContainerGroupAssert#fileExists tests") + @Nested + class FileExistsTest { + + @DisplayName(".fileExists(String, String...) throws an exception if the string is null") + @Test + void fileExistsThrowsExceptionIfFirstStringIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileExists(null, new String[0])) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragment"); + } + + @DisplayName(".fileExists(String, String...) throws an exception if the array is null") + @Test + void fileExistsThrowsExceptionIfArrayIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileExists("foo", (String[]) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragments"); + } + + @DisplayName( + ".fileExists(String, String...) throws an exception if the array has null values" + ) + @Test + void fileExistsThrowsExceptionIfArrayHasNullValues() { + // Given + var assertions = new PackageContainerGroupAssert(mock()); + + // Then + assertThatThrownBy(() -> assertions.fileExists("foo", "bar", "baz", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("fragments[2]"); + } + + @DisplayName( + ".fileExists(String, String...) throws an exception if the container group is null" + ) + @Test + void fileExistsThrowsExceptionIfContainerGroupIsNull() { + // Given + var assertions = new PackageContainerGroupAssert(null); + + // Then + // Arrays#asList does not NPE if any members are null. List#of does throw NPE. + assertThatThrownBy(() -> assertions.fileExists("foo", "bar", "baz")) + .isInstanceOf(AssertionError.class); + } + + @DisplayName( + ".fileExists(String, String...) fails with fuzzy suggestions if the file does not exist" + ) + @Test + void fileExistsFailsWithFuzzySuggestionsIfTheFileDoesNotExist() throws IOException { + // Given + var container1 = ContainerBuilder + .withRootPath("", "home", "ashley") + .withRelativeFile("foo", "bar", "qux") + .withRelativeFile("baz", "bork") + .withRelativeFile("qux", "quxx") + .withRelativeFile("bork") + .withRelativeFile("elephant") + .withRelativeFile("bazz") + .buildMock(); + + var container2 = ContainerBuilder + .withRootPath("lorem", "ipsum") + .withRelativeFile("dolor", "sit") + .withRelativeFile("amet") + .withRelativeFile("foo", "bar", "bazz") + .buildMock(); + + var container3 = ContainerBuilder + .withRootPath("doh") + .withRelativeFile("ray", "me", "fah") + .withRelativeFile("so", "la") + .withRelativeFile("foo", "baz") + .withRelativeFile("foo", "bar") + .buildMock(); + + var containerGroup = mock(PackageContainerGroup.class); + when(containerGroup.getFile(any(), any(), any())).thenReturn(null); + + // Have to declare separately outside the stubbing or Mockito gets confused. + var listAllFilesResult = Map.of( + container1, container1.listAllFiles(), + container2, container2.listAllFiles(), + container3, container3.listAllFiles() + ); + + when(containerGroup.listAllFiles()) + .thenReturn(listAllFilesResult); + + var assertions = new PackageContainerGroupAssert(containerGroup); + + // Then + // Expect the fuzzy error message to contain certain phrases to indicate it is working as + // we expect and giving good estimates of close names. + assertThatThrownBy(() -> assertions.fileExists("foo", "bar", "baz")) + .isInstanceOf(AssertionError.class) + .message() + .satisfies( + message -> assertThat(message).contains("Maybe you meant:"), + message -> assertThat(message).contains(quoted("foo/bar")), + message -> assertThat(message).contains(quoted("foo/bar/bazz")), + message -> assertThat(message).contains(quoted("foo/baz")), + message -> assertThat(message) + .contains("No file with relative path matching \"foo/bar/baz\" was found.") + ); + + } + + @DisplayName(".fileExists(String, String...) succeeds if the file exists") + @Test + void fileExistsSucceedsIfFileExists() { + // Given + var actualFile = Path.of("foo", "bar", "baz"); + + var containerGroup = mock(PackageContainerGroup.class); + when(containerGroup.getFile(any(), any(), any())).thenReturn(actualFile); + + var assertions = new PackageContainerGroupAssert(containerGroup); + + // When + var result = assertions.fileExists("foo", "bar", "baz"); + + // Then + verify(containerGroup).getFile("foo", "bar", "baz"); + verifyNoMoreInteractions(containerGroup); + + assertThatCode(() -> result.isSameAs(actualFile)) + .withFailMessage("Expected returned assertions to be for the discovered path") + .doesNotThrowAnyException(); + } + + } + + private static class ContainerBuilder { + + private final Path innerRootPath; + private final Set paths; + + private ContainerBuilder(String innerRootFragment, String... innerRootFragments) { + innerRootPath = Path.of(innerRootFragment, innerRootFragments); + paths = new LinkedHashSet<>(); + } + + ContainerBuilder withRelativeFile(String fragment, String... fragments) { + paths.add(innerRootPath.resolve(Path.of(fragment, fragments))); + return this; + } + + Container buildMock() { + var innerPathRoot = somePathRoot(); + when(innerPathRoot.getPath()).thenReturn(innerRootPath); + var container = mock(Container.class, withSettings().strictness(Strictness.LENIENT)); + when(container.getInnerPathRoot()).thenReturn(innerPathRoot); + uncheckedIo(() -> when(container.listAllFiles()).thenReturn(paths)); + return container; + } + + static ContainerBuilder withRootPath(String fragment, String... fragments) { + return new ContainerBuilder(fragment, fragments); + } + } +}