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