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