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