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 072 .assertThat(arguments, StringAssert::new) 073 .as("Compiler arguments %s", StringUtils.quotedIterable(arguments)); 074 } 075 076 /** 077 * Assert that the compilation was successful. 078 * 079 * @return this assertion object. 080 * @throws AssertionError if the compilation was null, or if the compilation was not successful. 081 */ 082 public JctCompilationAssert isSuccessful() { 083 isNotNull(); 084 085 if (actual.isFailure()) { 086 throw failWithDiagnostics( 087 // If we have error diagnostics, add them to the error message to provide helpful 088 // debugging information. If we are treating warnings as errors, then we want to include 089 // those in this as well. 090 actual.isFailOnWarnings() 091 ? DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS 092 : DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS, 093 "Expected a successful compilation, but it failed." 094 ); 095 } 096 097 return myself; 098 } 099 100 /** 101 * Assert that the compilation was successful and had no warnings. 102 * 103 * <p>If warnings were treated as errors by the compiler, then this is identical to calling 104 * {@link #isSuccessful()}. 105 * 106 * @return this assertion object. 107 * @throws AssertionError if the compilation was null, if the compilation was not successful, or 108 * if the compilation was successful but had one or more warning 109 * diagnostics. 110 */ 111 public JctCompilationAssert isSuccessfulWithoutWarnings() { 112 isSuccessful(); 113 diagnostics().hasNoErrorsOrWarnings(); 114 return myself; 115 } 116 117 /** 118 * Assert that the compilation was a failure. 119 * 120 * @return this assertion object. 121 * @throws AssertionError if the compilation was null, or if the compilation succeeded. 122 */ 123 public JctCompilationAssert isFailure() { 124 isNotNull(); 125 126 // If we fail due to failOnWarnings, we expect the compiler itself to have failed the 127 // build because of this. If the compiler ignores this flag and succeeds, then this method will 128 // follow that behaviour and treat the compilation as a success. 129 130 if (actual.isSuccessful()) { 131 // If we have any warnings, we should show them in the error message as it might be useful 132 // to the user. 133 throw failWithDiagnostics( 134 DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS, 135 "Expected compilation to fail, but it succeeded." 136 ); 137 } 138 139 return myself; 140 } 141 142 /** 143 * Get assertions for diagnostics. 144 * 145 * @return assertions for the diagnostics. 146 * @throws AssertionError if the compilation was null. 147 */ 148 public TraceDiagnosticListAssert diagnostics() { 149 isNotNull(); 150 return new TraceDiagnosticListAssert(actual.getDiagnostics()); 151 } 152 153 /** 154 * Perform assertions on the given package group, if it has been configured. 155 * 156 * <p>If not configured, this will return assertions on a {@code null} value instead. 157 * 158 * @param location the location to configure. 159 * @return the assertions to perform. 160 * @throws AssertionError if the compilation was null, or no group for the location 161 * was found. 162 * @throws IllegalArgumentException if the location was 163 * {@link Location#isModuleOrientedLocation() module-oriented} 164 * or {@link Location#isOutputLocation() an output location}. 165 * @throws NullPointerException if the provided location object is null. 166 */ 167 public PackageContainerGroupAssert packageGroup(Location location) { 168 requireNonNull(location, "location must not be null"); 169 170 if (location.isModuleOrientedLocation()) { 171 throw new IllegalArgumentException( 172 "Expected location " + location + " to not be module-oriented" 173 ); 174 } 175 176 if (location.isOutputLocation()) { 177 throw new IllegalArgumentException( 178 "Expected location " + location + " to not be an output location" 179 ); 180 } 181 182 isNotNull(); 183 184 var group = actual.getFileManager().getPackageContainerGroup(location); 185 assertLocationExists(location, group); 186 return new PackageContainerGroupAssert(group); 187 } 188 189 /** 190 * Perform assertions on the given module group, if it has been configured. 191 * 192 * <p>If not configured, the value being asserted on will be {@code null} in value. 193 * 194 * @param location the location to configure. 195 * @return the assertions to perform. 196 * @throws AssertionError if the compilation was null, or no group for the location 197 * was found. 198 * @throws IllegalArgumentException if the location is not 199 * {@link Location#isModuleOrientedLocation() module-oriented}. 200 * @throws NullPointerException if the provided location object is null. 201 */ 202 public ModuleContainerGroupAssert moduleGroup(Location location) { 203 requireNonNull(location, "location must not be null"); 204 205 if (!location.isModuleOrientedLocation()) { 206 throw new IllegalArgumentException( 207 "Expected location " + location.getName() + " to be module-oriented" 208 ); 209 } 210 211 if (location.isOutputLocation()) { 212 throw new IllegalArgumentException( 213 "Expected location " + location.getName() + " to not be an output location" 214 ); 215 } 216 217 isNotNull(); 218 219 var group = actual.getFileManager().getModuleContainerGroup(location); 220 assertLocationExists(location, group); 221 return new ModuleContainerGroupAssert(group); 222 } 223 224 /** 225 * Perform assertions on the given output group, if it has been configured. 226 * 227 * <p>If not configured, the value being asserted on will be {@code null} in value. 228 * 229 * @param location the location to configure. 230 * @return the assertions to perform. 231 * @throws AssertionError if the compilation was null, or no group for the location 232 * was found. 233 * @throws IllegalArgumentException if the location is not 234 * {@link Location#isOutputLocation() an output location}. 235 * @throws NullPointerException if the provided location object is null. 236 */ 237 public OutputContainerGroupAssert outputGroup(Location location) { 238 requireNonNull(location, "location must not be null"); 239 240 if (!location.isOutputLocation()) { 241 throw new IllegalArgumentException( 242 "Expected location " + location.getName() + " to be an output location" 243 ); 244 } 245 246 isNotNull(); 247 248 var group = actual.getFileManager().getOutputContainerGroup(location); 249 assertLocationExists(location, group); 250 return new OutputContainerGroupAssert(group); 251 } 252 253 /** 254 * Get assertions on the path containing class outputs, if it exists. 255 * 256 * <p>If not configured, the value being asserted on will be {@code null} in value. 257 * 258 * @return the assertions to perform on the class outputs. 259 * @throws AssertionError if the compilation was null, or no group for the location 260 * was found. 261 * @deprecated use {@link #classOutputPackages()} or {@link #classOutputModules()} instead. 262 */ 263 @Deprecated(since = "0.6.4", forRemoval = true) 264 @SuppressWarnings("DeprecatedIsStillUsed") 265 public OutputContainerGroupAssert classOutput() { 266 return outputGroup(StandardLocation.CLASS_OUTPUT); 267 } 268 269 /** 270 * Get assertions on the path containing class package outputs, if it exists. 271 * 272 * <p>If not configured, the value being asserted on will be {@code null} in value. 273 * 274 * @return the assertions to perform on the class package outputs. 275 * @throws AssertionError if the compilation was null, or no group for the location 276 * was found. 277 */ 278 @API(since = "0.6.4", status = Status.STABLE) 279 public PackageContainerGroupAssert classOutputPackages() { 280 return outputGroup(StandardLocation.CLASS_OUTPUT).packages(); 281 } 282 283 /** 284 * Get assertions on the path containing class module outputs, if it exists. 285 * 286 * <p>If not configured, the value being asserted on will be {@code null} in value. 287 * 288 * @return the assertions to perform on the class module outputs. 289 * @throws AssertionError if the compilation was null, or no group for the location 290 * was found. 291 */ 292 @API(since = "0.6.4", status = Status.STABLE) 293 public ModuleContainerGroupAssert classOutputModules() { 294 return outputGroup(StandardLocation.CLASS_OUTPUT).modules(); 295 } 296 297 /** 298 * Get assertions on the path containing generated source outputs, if it exists. 299 * 300 * <p>If not configured, the value being asserted on will be {@code null} in value. 301 * 302 * @return the assertions to perform on the source outputs. 303 * @throws AssertionError if the compilation was null, or no group for the location 304 * was found. 305 * @deprecated use {@link #sourceOutputPackages()} or {@link #sourceOutputModules()} instead. 306 */ 307 @Deprecated(since = "0.6.4", forRemoval = true) 308 @SuppressWarnings("DeprecatedIsStillUsed") 309 public OutputContainerGroupAssert sourceOutput() { 310 return outputGroup(StandardLocation.SOURCE_OUTPUT); 311 } 312 313 /** 314 * Get assertions on the path containing generated source package outputs, if it exists. 315 * 316 * <p>If not configured, the value being asserted on will be {@code null} in value. 317 * 318 * @return the assertions to perform on the source package outputs. 319 * @throws AssertionError if the compilation was null, or no group for the location 320 * was found. 321 */ 322 @API(since = "0.6.4", status = Status.STABLE) 323 public PackageContainerGroupAssert sourceOutputPackages() { 324 return outputGroup(StandardLocation.SOURCE_OUTPUT).packages(); 325 } 326 327 /** 328 * Get assertions on the path containing generated source module outputs, if it exists. 329 * 330 * <p>If not configured, the value being asserted on will be {@code null} in value. 331 * 332 * @return the assertions to perform on the source module outputs. 333 * @throws AssertionError if the compilation was null, or no group for the location 334 * was found. 335 */ 336 @API(since = "0.6.4", status = Status.STABLE) 337 public ModuleContainerGroupAssert sourceOutputModules() { 338 return outputGroup(StandardLocation.SOURCE_OUTPUT).modules(); 339 } 340 341 /** 342 * Get assertions on the path containing header outputs, if it exists. 343 * 344 * <p>If not configured, the value being asserted on will be {@code null} in value. 345 * 346 * @return the assertions to perform on the header outputs. 347 * @throws AssertionError if the compilation was null, or no group for the location 348 * was found. 349 * @deprecated this method is rarely needed, so is being removed in v1.0.0. Use 350 * {@link #outputGroup(Location)}, passing {@link StandardLocation#NATIVE_HEADER_OUTPUT} as the 351 * first parameter instead. 352 */ 353 @Deprecated(since = "0.6.0", forRemoval = true) 354 @SuppressWarnings("DeprecatedIsStillUsed") 355 public OutputContainerGroupAssert generatedHeaders() { 356 return outputGroup(StandardLocation.NATIVE_HEADER_OUTPUT); 357 } 358 359 /** 360 * Get assertions on the path containing the class path, if it exists. 361 * 362 * <p>If not configured, the value being asserted on will be {@code null} in value. 363 * 364 * @return the assertions to perform on the class path. 365 * @throws AssertionError if the compilation was null, or no group for the location 366 * was found. 367 * @deprecated use {@link #classPathPackages()} instead. 368 */ 369 @Deprecated(since = "0.6.4", forRemoval = true) 370 @SuppressWarnings("DeprecatedIsStillUsed") 371 public PackageContainerGroupAssert classPath() { 372 return packageGroup(StandardLocation.CLASS_PATH); 373 } 374 375 /** 376 * Get assertions on the path containing the class path, if it exists. 377 * 378 * <p>If not configured, the value being asserted on will be {@code null} in value. 379 * 380 * @return the assertions to perform on the class path. 381 * @throws AssertionError if the compilation was null, or no group for the location 382 * was found. 383 */ 384 @API(since = "0.6.4", status = Status.STABLE) 385 public PackageContainerGroupAssert classPathPackages() { 386 return packageGroup(StandardLocation.CLASS_PATH); 387 } 388 389 /** 390 * Get assertions on the path containing the source path, if it exists. 391 * 392 * <p>If not configured, the value being asserted on will be {@code null} in value. 393 * 394 * @return the assertions to perform on the source path. 395 * @throws AssertionError if the compilation was null, or no group for the location 396 * was found. 397 * @deprecated use {@link #sourcePathPackages()} instead. 398 */ 399 @Deprecated(since = "0.6.4", forRemoval = true) 400 @SuppressWarnings("DeprecatedIsStillUsed") 401 public PackageContainerGroupAssert sourcePath() { 402 return packageGroup(StandardLocation.SOURCE_PATH); 403 } 404 405 /** 406 * Get assertions on the path containing the source path, if it exists. 407 * 408 * <p>If not configured, the value being asserted on will be {@code null} in value. 409 * 410 * @return the assertions to perform on the source path. 411 * @throws AssertionError if the compilation was null, or no group for the location 412 * was found. 413 */ 414 @API(since = "0.6.4", status = Status.STABLE) 415 public PackageContainerGroupAssert sourcePathPackages() { 416 return packageGroup(StandardLocation.SOURCE_PATH); 417 } 418 419 /** 420 * Get assertions on the path containing the source path, if it exists. 421 * 422 * <p>If not configured, the value being asserted on will be {@code null} in value. 423 * 424 * @return the assertions to perform on the source path. 425 * @throws AssertionError if the compilation was null, or no group for the location 426 * was found. 427 * @deprecated use {@link #moduleSourcePathModules()} instead. 428 */ 429 @Deprecated(since = "0.6.4", forRemoval = true) 430 @SuppressWarnings("DeprecatedIsStillUsed") 431 public ModuleContainerGroupAssert moduleSourcePath() { 432 return moduleGroup(StandardLocation.MODULE_SOURCE_PATH); 433 } 434 435 /** 436 * Get assertions on the path containing the source path, if it exists. 437 * 438 * <p>If not configured, the value being asserted on will be {@code null} in value. 439 * 440 * @return the assertions to perform on the source path. 441 * @throws AssertionError if the compilation was null, or no group for the location 442 * was found. 443 */ 444 @API(since = "0.6.4", status = Status.STABLE) 445 public ModuleContainerGroupAssert moduleSourcePathModules() { 446 return moduleGroup(StandardLocation.MODULE_SOURCE_PATH); 447 } 448 449 /** 450 * Get assertions on the path containing the module path, if it exists. 451 * 452 * <p>If not configured, the value being asserted on will be {@code null} in value. 453 * 454 * @return the assertions to perform on the module path. 455 * @throws AssertionError if the compilation was null, or no group for the location 456 * was found. 457 * @deprecated use {@link #modulePathModules()} instead. 458 */ 459 @Deprecated(since = "0.6.4", forRemoval = true) 460 @SuppressWarnings("DeprecatedIsStillUsed") 461 public ModuleContainerGroupAssert modulePath() { 462 return moduleGroup(StandardLocation.MODULE_PATH); 463 } 464 465 /** 466 * Get assertions on the path containing the module path, if it exists. 467 * 468 * <p>If not configured, the value being asserted on will be {@code null} in value. 469 * 470 * @return the assertions to perform on the module path. 471 * @throws AssertionError if the compilation was null, or no group for the location 472 * was found. 473 */ 474 @API(since = "0.6.4", status = Status.STABLE) 475 public ModuleContainerGroupAssert modulePathModules() { 476 return moduleGroup(StandardLocation.MODULE_PATH); 477 } 478 479 480 private AssertionError failWithDiagnostics( 481 Collection<? extends Kind> kindsToDisplay, 482 String message 483 ) { 484 var diagnostics = actual 485 .getDiagnostics() 486 .stream() 487 .filter(diagnostic -> kindsToDisplay.contains(diagnostic.getKind())) 488 .collect(toUnmodifiableList()); 489 490 return failure( 491 "%s\n\nDiagnostics:\n%s", 492 message, 493 TraceDiagnosticListRepresentation.getInstance().toStringOf(diagnostics) 494 ); 495 } 496 497 private void assertLocationExists(Location location, ContainerGroup group) { 498 if (group == null) { 499 throw failure("No location named %s exists", location.getName()); 500 } 501 } 502}