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}