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}