001/* 002 * Copyright (C) 2022 - 2023, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.assertions; 017 018import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo; 019import static io.github.ascopes.jct.utils.IterableUtils.combineOneOrMore; 020import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; 021import static java.util.Objects.requireNonNull; 022import static java.util.function.Predicate.not; 023import static org.assertj.core.api.Assertions.assertThat; 024 025import io.github.ascopes.jct.containers.PackageContainerGroup; 026import io.github.ascopes.jct.repr.LocationRepresentation; 027import java.io.File; 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.util.stream.Collectors; 031import me.xdrop.fuzzywuzzy.FuzzySearch; 032import me.xdrop.fuzzywuzzy.model.BoundExtractedResult; 033import org.apiguardian.api.API; 034import org.apiguardian.api.API.Status; 035import org.assertj.core.api.AbstractPathAssert; 036import org.jspecify.annotations.Nullable; 037 038/** 039 * Assertions for package container groups. 040 * 041 * @author Ashley Scopes 042 * @since 0.0.1 043 */ 044@API(since = "0.0.1", status = Status.STABLE) 045public final class PackageContainerGroupAssert 046 extends AbstractContainerGroupAssert<PackageContainerGroupAssert, PackageContainerGroup> { 047 048 /** 049 * Initialize the container group assertions. 050 * 051 * @param containerGroup the container group to assert upon. 052 */ 053 public PackageContainerGroupAssert(@Nullable PackageContainerGroup containerGroup) { 054 super(containerGroup, PackageContainerGroupAssert.class); 055 } 056 057 /** 058 * Assert that all given files exist. 059 * 060 * @param path the first path to check for 061 * @param paths additional paths to check for. 062 * @throws AssertionError if the container group is null, or if any of the files do not 063 * exist. 064 * @throws NullPointerException if any of the paths are null. 065 */ 066 public void allFilesExist(String path, String... paths) { 067 requireNonNull(path, "path must not be null"); 068 requireNonNullValues(paths, "paths"); 069 070 allFilesExist(combineOneOrMore(path, paths)); 071 } 072 073 /** 074 * Assert that all given files exist. 075 * 076 * @param paths paths to check for. 077 * @throws AssertionError if the container group is null, or if any of the files do not 078 * exist. 079 * @throws NullPointerException if any of the paths are null. 080 */ 081 public void allFilesExist(Iterable<String> paths) { 082 requireNonNullValues(paths, "paths"); 083 084 isNotNull(); 085 086 assertThat(paths).allSatisfy(this::fileExists); 087 } 088 089 /** 090 * Get assertions to perform on the class loader associated with this container group. 091 * 092 * @return the assertions to perform. 093 * @throws AssertionError if the container group is null. 094 */ 095 public ClassLoaderAssert classLoader() { 096 isNotNull(); 097 098 return new ClassLoaderAssert(actual.getClassLoader()); 099 } 100 101 /** 102 * Assert that the given file does not exist. 103 * 104 * <pre><code> 105 * // Using platform-specific separators. 106 * assertions.fileDoesNotExist("foo/bar/baz.txt")...; 107 * 108 * // Letting JCT infer the correct path separators to use (recommended). 109 * assertions.fileDoesNotExist("foo", "bar", "baz.txt"); 110 * </code></pre> 111 * 112 * @param fragment the first part of the path. 113 * @param fragments any additional parts of the path. 114 * @return this assertion object for further assertions. 115 * @throws AssertionError if the file exists, or if the container group is null. 116 * @throws NullPointerException if any of the fragments are null. 117 */ 118 public PackageContainerGroupAssert fileDoesNotExist(String fragment, String... fragments) { 119 requireNonNull(fragment, "fragment must not be null"); 120 requireNonNullValues(fragments, "fragments"); 121 122 isNotNull(); 123 124 var expectedFile = fragmentPathToString(fragment, fragments); 125 var actualFile = actual.getFile(fragment, fragments); 126 127 if (actualFile == null) { 128 return this; 129 } 130 131 var locationName = LocationRepresentation.getInstance().toStringOf(actual.getLocation()); 132 133 throw failure( 134 "Expected path \"%s\" to not exist in \"%s\" but it was found at \"%s\"", 135 expectedFile, 136 locationName, 137 actualFile 138 ); 139 } 140 141 142 /** 143 * Assert that the given file exists. 144 * 145 * <pre><code> 146 * // Using platform-specific separators. 147 * assertions.fileExists("foo/bar/baz.txt")...; 148 * 149 * // Letting JCT infer the correct path separators to use (recommended). 150 * assertions.fileExists("foo", "bar", "baz.txt"); 151 * </code></pre> 152 * 153 * @param fragment the first part of the path. 154 * @param fragments any additional parts of the path. 155 * @return assertions to perform on the path of the file that exists. 156 * @throws AssertionError if the file does not exist, or if the container group is null. 157 * @throws NullPointerException if any of the fragments are null. 158 */ 159 public AbstractPathAssert<?> fileExists(String fragment, String... fragments) { 160 requireNonNull(fragment, "fragment must not be null"); 161 requireNonNullValues(fragments, "fragments"); 162 163 isNotNull(); 164 165 var expectedFile = fragmentPathToString(fragment, fragments); 166 var actualFile = actual.getFile(fragment, fragments); 167 168 if (actualFile != null) { 169 return assertThat(actualFile); 170 } 171 172 var closestMatches = FuzzySearch 173 .extractSorted( 174 expectedFile, 175 actual 176 .getPackages() 177 .stream() 178 .flatMap(container -> uncheckedIo(() -> container 179 .listAllFiles() 180 .stream() 181 .filter(not(Files::isDirectory)) 182 .map(container.getPathRoot().getPath()::relativize) 183 )) 184 .collect(Collectors.toList()), 185 Path::toString 186 ) 187 .stream() 188 .limit(FUZZY_CUTOFF) 189 .map(BoundExtractedResult::getReferent) 190 .sorted() 191 .collect(Collectors.toList()); 192 193 var locationName = LocationRepresentation.getInstance().toStringOf(actual.getLocation()); 194 195 if (closestMatches.isEmpty()) { 196 throw failure("No file in %s files found named \"%s\"", locationName, expectedFile); 197 } else { 198 throw failure( 199 "No file named \"%s\" found in %s. Did you mean...%s", 200 expectedFile, 201 locationName, 202 closestMatches 203 .stream() 204 .map(Path::toString) 205 .map("\n\t - "::concat) 206 .collect(Collectors.joining()) 207 ); 208 } 209 } 210 211 private static String fragmentPathToString(String fragment, String... fragments) { 212 // Path#toString uses the default separator if we use Path#of 213 return String.join(File.separator, combineOneOrMore(fragment, fragments)); 214 } 215}