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