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 java.util.Objects.requireNonNull; 019import static java.util.stream.Collectors.toUnmodifiableList; 020 021import io.github.ascopes.jct.compilers.JctCompilation; 022import io.github.ascopes.jct.containers.ContainerGroup; 023import io.github.ascopes.jct.repr.TraceDiagnosticListRepresentation; 024import io.github.ascopes.jct.utils.StringUtils; 025import java.util.Collection; 026import java.util.List; 027import javax.tools.Diagnostic.Kind; 028import javax.tools.JavaFileManager.Location; 029import javax.tools.StandardLocation; 030import org.apiguardian.api.API; 031import org.apiguardian.api.API.Status; 032import org.assertj.core.api.AbstractAssert; 033import org.assertj.core.api.AbstractListAssert; 034import org.assertj.core.api.AbstractStringAssert; 035import org.assertj.core.api.FactoryBasedNavigableListAssert; 036import org.assertj.core.api.StringAssert; 037import org.jspecify.annotations.Nullable; 038 039/** 040 * Assertions that apply to a {@link JctCompilation}. 041 * 042 * @author Ashley Scopes 043 * @since 0.0.1 044 */ 045@API(since = "0.0.1", status = Status.STABLE) 046public final class JctCompilationAssert extends 047 AbstractAssert<JctCompilationAssert, JctCompilation> { 048 049 /** 050 * Initialize this compilation assertion. 051 * 052 * @param value the value to assert on. 053 */ 054 public JctCompilationAssert(@Nullable JctCompilation value) { 055 super(value, JctCompilationAssert.class); 056 } 057 058 /** 059 * Assert that the arguments passed to the compiler were the expected values. 060 * 061 * @return a list assertion object to perform assertions on the arguments with. 062 * @throws AssertionError if the compilation was null. 063 */ 064 public AbstractListAssert<?, List<? extends String>, String, ? extends AbstractStringAssert<?>> arguments() { 065 isNotNull(); 066 067 var arguments = actual.getArguments(); 068 069 // TODO(ascopes): find a way to use Assertions::assertThat here instead of passing the 070 // StringAssert constructor around. 071 return FactoryBasedNavigableListAssert.assertThat(arguments, StringAssert::new) 072 .as("Compiler arguments %s", StringUtils.quotedIterable(arguments)); 073 } 074 075 /** 076 * Assert that the compilation was successful. 077 * 078 * @return this assertion object. 079 * @throws AssertionError if the compilation was null, or if the compilation was not successful. 080 */ 081 public JctCompilationAssert isSuccessful() { 082 isNotNull(); 083 084 if (actual.isFailure()) { 085 // If we have error diagnostics, add them to the error message to provide helpful debugging 086 // information. If we are treating warnings as errors, then we want to include those in this 087 // as well. 088 var diagnosticKinds = actual.isFailOnWarnings() 089 ? DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS 090 : DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS; 091 092 failWithDiagnostics(diagnosticKinds, "Expected a successful compilation, but it failed."); 093 } 094 095 return myself; 096 } 097 098 /** 099 * Assert that the compilation was successful and had no warnings. 100 * 101 * <p>If warnings were treated as errors by the compiler, then this is identical to calling 102 * {@link #isSuccessful()}. 103 * 104 * @return this assertion object. 105 * @throws AssertionError if the compilation was null, if the compilation was not successful, or 106 * if the compilation was successful but had one or more warning 107 * diagnostics. 108 */ 109 public JctCompilationAssert isSuccessfulWithoutWarnings() { 110 isSuccessful(); 111 diagnostics().hasNoErrorsOrWarnings(); 112 return myself; 113 } 114 115 /** 116 * Assert that the compilation was a failure. 117 * 118 * @return this assertion object. 119 * @throws AssertionError if the compilation was null, or if the compilation succeeded. 120 */ 121 public JctCompilationAssert isFailure() { 122 isNotNull(); 123 124 // If we fail due to failOnWarnings, we expect the compiler itself to have failed the 125 // build because of this. If the compiler ignores this flag and succeeds, then this method will 126 // follow that behaviour and treat the compilation as a success. 127 128 if (actual.isSuccessful()) { 129 // If we have any warnings, we should show them in the error message as it might be useful 130 // to the user. 131 failWithDiagnostics( 132 DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS, 133 "Expected compilation to fail, but it succeeded." 134 ); 135 } 136 137 return myself; 138 } 139 140 /** 141 * Get assertions for diagnostics. 142 * 143 * @return assertions for the diagnostics. 144 * @throws AssertionError if the compilation was null. 145 */ 146 public TraceDiagnosticListAssert diagnostics() { 147 isNotNull(); 148 return new TraceDiagnosticListAssert(actual.getDiagnostics()); 149 } 150 151 /** 152 * Perform assertions on the given package group, if it has been configured. 153 * 154 * <p>If not configured, this will return assertions on a {@code null} value instead. 155 * 156 * @param location the location to configure. 157 * @return the assertions to perform. 158 * @throws AssertionError if the compilation was null, or no group for the location 159 * was found. 160 * @throws IllegalArgumentException if the location was 161 * {@link Location#isModuleOrientedLocation() module-oriented} 162 * or {@link Location#isOutputLocation() an output location}. 163 * @throws NullPointerException if the provided location object is null. 164 */ 165 public PackageContainerGroupAssert packageGroup(Location location) { 166 requireNonNull(location, "location must not be null"); 167 168 if (location.isModuleOrientedLocation()) { 169 throw new IllegalArgumentException( 170 "Expected location " + location + " to not be module-oriented" 171 ); 172 } 173 174 if (location.isOutputLocation()) { 175 throw new IllegalArgumentException( 176 "Expected location " + location + " to not be an output location" 177 ); 178 } 179 180 isNotNull(); 181 182 var group = actual.getFileManager().getPackageContainerGroup(location); 183 assertLocationExists(location, group); 184 return new PackageContainerGroupAssert(group); 185 } 186 187 /** 188 * Perform assertions on the given module group, if it has been configured. 189 * 190 * <p>If not configured, the value being asserted on will be {@code null} in value. 191 * 192 * @param location the location to configure. 193 * @return the assertions to perform. 194 * @throws AssertionError if the compilation was null, or no group for the location 195 * was found. 196 * @throws IllegalArgumentException if the location is not 197 * {@link Location#isModuleOrientedLocation() module-oriented}. 198 * @throws NullPointerException if the provided location object is null. 199 */ 200 public ModuleContainerGroupAssert moduleGroup(Location location) { 201 requireNonNull(location, "location must not be null"); 202 203 if (!location.isModuleOrientedLocation()) { 204 throw new IllegalArgumentException( 205 "Expected location " + location.getName() + " to be module-oriented" 206 ); 207 } 208 209 if (location.isOutputLocation()) { 210 throw new IllegalArgumentException( 211 "Expected location " + location.getName() + " to not be an output location" 212 ); 213 } 214 215 isNotNull(); 216 217 var group = actual.getFileManager().getModuleContainerGroup(location); 218 assertLocationExists(location, group); 219 return new ModuleContainerGroupAssert(group); 220 } 221 222 /** 223 * Perform assertions on the given output group, if it has been configured. 224 * 225 * <p>If not configured, the value being asserted on will be {@code null} in value. 226 * 227 * @param location the location to configure. 228 * @return the assertions to perform. 229 * @throws AssertionError if the compilation was null, or no group for the location 230 * was found. 231 * @throws IllegalArgumentException if the location is not 232 * {@link Location#isOutputLocation() an output location}. 233 * @throws NullPointerException if the provided location object is null. 234 */ 235 public OutputContainerGroupAssert outputGroup(Location location) { 236 requireNonNull(location, "location must not be null"); 237 238 if (!location.isOutputLocation()) { 239 throw new IllegalArgumentException( 240 "Expected location " + location.getName() + " to be an output location" 241 ); 242 } 243 244 isNotNull(); 245 246 var group = actual.getFileManager().getOutputContainerGroup(location); 247 assertLocationExists(location, group); 248 return new OutputContainerGroupAssert(group); 249 } 250 251 /** 252 * Get assertions on the path containing class outputs, if it exists. 253 * 254 * <p>If not configured, the value being asserted on will be {@code null} in value. 255 * 256 * @return the assertions to perform on the class outputs. 257 * @throws AssertionError if the compilation was null, or no group for the location 258 * was found. 259 */ 260 public OutputContainerGroupAssert classOutput() { 261 return outputGroup(StandardLocation.CLASS_OUTPUT); 262 } 263 264 /** 265 * Get assertions on the path containing generated source outputs, if it exists. 266 * 267 * <p>If not configured, the value being asserted on will be {@code null} in value. 268 * 269 * @return the assertions to perform on the source outputs. 270 * @throws AssertionError if the compilation was null, or no group for the location 271 * was found. 272 */ 273 public OutputContainerGroupAssert sourceOutput() { 274 return outputGroup(StandardLocation.SOURCE_OUTPUT); 275 } 276 277 /** 278 * Get assertions on the path containing header outputs, if it exists. 279 * 280 * <p>If not configured, the value being asserted on will be {@code null} in value. 281 * 282 * @return the assertions to perform on the header outputs. 283 * @throws AssertionError if the compilation was null, or no group for the location 284 * was found. 285 * @deprecated this method is rarely needed, so is being removed in v1.0.0. Use 286 * {@link #outputGroup(Location)}, passing {@link StandardLocation#NATIVE_HEADER_OUTPUT} as the 287 * first parameter instead. 288 */ 289 @Deprecated(since = "0.6.0", forRemoval = true) 290 @SuppressWarnings("DeprecatedIsStillUsed") 291 public OutputContainerGroupAssert generatedHeaders() { 292 return outputGroup(StandardLocation.NATIVE_HEADER_OUTPUT); 293 } 294 295 /** 296 * Get assertions on the path containing the class path, if it exists. 297 * 298 * <p>If not configured, the value being asserted on will be {@code null} in value. 299 * 300 * @return the assertions to perform on the class path. 301 * @throws AssertionError if the compilation was null, or no group for the location 302 * was found. 303 */ 304 public PackageContainerGroupAssert classPath() { 305 return packageGroup(StandardLocation.CLASS_PATH); 306 } 307 308 /** 309 * Get assertions on the path containing the source path, if it exists. 310 * 311 * <p>If not configured, the value being asserted on will be {@code null} in value. 312 * 313 * @return the assertions to perform on the source path. 314 * @throws AssertionError if the compilation was null, or no group for the location 315 * was found. 316 */ 317 public PackageContainerGroupAssert sourcePath() { 318 return packageGroup(StandardLocation.SOURCE_PATH); 319 } 320 321 /** 322 * Get assertions on the path containing the source path, if it exists. 323 * 324 * <p>If not configured, the value being asserted on will be {@code null} in value. 325 * 326 * @return the assertions to perform on the source path. 327 * @throws AssertionError if the compilation was null, or no group for the location 328 * was found. 329 */ 330 public ModuleContainerGroupAssert moduleSourcePath() { 331 return moduleGroup(StandardLocation.MODULE_SOURCE_PATH); 332 } 333 334 /** 335 * Get assertions on the path containing the module path, if it exists. 336 * 337 * <p>If not configured, the value being asserted on will be {@code null} in value. 338 * 339 * @return the assertions to perform on the module path. 340 * @throws AssertionError if the compilation was null, or no group for the location 341 * was found. 342 */ 343 public ModuleContainerGroupAssert modulePath() { 344 return moduleGroup(StandardLocation.MODULE_PATH); 345 } 346 347 private void failWithDiagnostics( 348 Collection<? extends Kind> kindsToDisplay, 349 String message, 350 Object... args 351 ) { 352 var diagnostics = actual 353 .getDiagnostics() 354 .stream() 355 .filter(diagnostic -> kindsToDisplay.contains(diagnostic.getKind())) 356 .collect(toUnmodifiableList()); 357 358 failWithMessage( 359 "%s\n\nDiagnostics:\n%s", 360 message, 361 TraceDiagnosticListRepresentation.getInstance().toStringOf(diagnostics) 362 ); 363 } 364 365 private void assertLocationExists(Location location, ContainerGroup group) { 366 if (group == null) { 367 throw new AssertionError("No location named " + location.getName() + " exists"); 368 } 369 } 370}