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