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