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 was 161 * found. 162 * @throws IllegalArgumentException if the location was 163 * {@link Location#isModuleOrientedLocation() module-oriented} or 164 * {@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 was 197 * 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 was 232 * 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 package 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 package outputs. 259 * @throws AssertionError if the compilation was null, or no group for the location was found. 260 */ 261 @API(since = "0.6.4", status = Status.STABLE) 262 public PackageContainerGroupAssert classOutputPackages() { 263 return outputGroup(StandardLocation.CLASS_OUTPUT).packages(); 264 } 265 266 /** 267 * Get assertions on the path containing class module outputs, if it exists. 268 * 269 * <p>If not configured, the value being asserted on will be {@code null} in value. 270 * 271 * @return the assertions to perform on the class module outputs. 272 * @throws AssertionError if the compilation was null, or no group for the location was found. 273 */ 274 @API(since = "0.6.4", status = Status.STABLE) 275 public ModuleContainerGroupAssert classOutputModules() { 276 return outputGroup(StandardLocation.CLASS_OUTPUT).modules(); 277 } 278 279 /** 280 * Get assertions on the path containing generated source package outputs, if it exists. 281 * 282 * <p>If not configured, the value being asserted on will be {@code null} in value. 283 * 284 * @return the assertions to perform on the source package outputs. 285 * @throws AssertionError if the compilation was null, or no group for the location was found. 286 */ 287 @API(since = "0.6.4", status = Status.STABLE) 288 public PackageContainerGroupAssert sourceOutputPackages() { 289 return outputGroup(StandardLocation.SOURCE_OUTPUT).packages(); 290 } 291 292 /** 293 * Get assertions on the path containing generated source module outputs, if it exists. 294 * 295 * <p>If not configured, the value being asserted on will be {@code null} in value. 296 * 297 * @return the assertions to perform on the source module outputs. 298 * @throws AssertionError if the compilation was null, or no group for the location was found. 299 */ 300 @API(since = "0.6.4", status = Status.STABLE) 301 public ModuleContainerGroupAssert sourceOutputModules() { 302 return outputGroup(StandardLocation.SOURCE_OUTPUT).modules(); 303 } 304 305 /** 306 * Get assertions on the path containing the class path, if it exists. 307 * 308 * <p>If not configured, the value being asserted on will be {@code null} in value. 309 * 310 * @return the assertions to perform on the class path. 311 * @throws AssertionError if the compilation was null, or no group for the location was found. 312 */ 313 @API(since = "0.6.4", status = Status.STABLE) 314 public PackageContainerGroupAssert classPathPackages() { 315 return packageGroup(StandardLocation.CLASS_PATH); 316 } 317 318 /** 319 * Get assertions on the path containing the source path, if it exists. 320 * 321 * <p>If not configured, the value being asserted on will be {@code null} in value. 322 * 323 * @return the assertions to perform on the source path. 324 * @throws AssertionError if the compilation was null, or no group for the location was found. 325 */ 326 @API(since = "0.6.4", status = Status.STABLE) 327 public PackageContainerGroupAssert sourcePathPackages() { 328 return packageGroup(StandardLocation.SOURCE_PATH); 329 } 330 331 /** 332 * Get assertions on the path containing the source path, if it exists. 333 * 334 * <p>If not configured, the value being asserted on will be {@code null} in value. 335 * 336 * @return the assertions to perform on the source path. 337 * @throws AssertionError if the compilation was null, or no group for the location was found. 338 */ 339 @API(since = "0.6.4", status = Status.STABLE) 340 public ModuleContainerGroupAssert moduleSourcePathModules() { 341 return moduleGroup(StandardLocation.MODULE_SOURCE_PATH); 342 } 343 344 /** 345 * Get assertions on the path containing the module path, if it exists. 346 * 347 * <p>If not configured, the value being asserted on will be {@code null} in value. 348 * 349 * @return the assertions to perform on the module path. 350 * @throws AssertionError if the compilation was null, or no group for the location was found. 351 */ 352 @API(since = "0.6.4", status = Status.STABLE) 353 public ModuleContainerGroupAssert modulePathModules() { 354 return moduleGroup(StandardLocation.MODULE_PATH); 355 } 356 357 358 private AssertionError failWithDiagnostics( 359 Collection<? extends Kind> kindsToDisplay, 360 String message 361 ) { 362 var diagnostics = actual 363 .getDiagnostics() 364 .stream() 365 .filter(diagnostic -> kindsToDisplay.contains(diagnostic.getKind())) 366 .collect(toUnmodifiableList()); 367 368 return failure( 369 "%s\n\nDiagnostics:\n%s", 370 message, 371 TraceDiagnosticListRepresentation.getInstance().toStringOf(diagnostics) 372 ); 373 } 374 375 private void assertLocationExists(Location location, @Nullable ContainerGroup group) { 376 if (group == null) { 377 throw failure("No location named %s exists", location.getName()); 378 } 379 } 380}