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 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 outputs.
259   * @throws AssertionError if the compilation was null, or no group for the location
260   *                        was found.
261   * @deprecated use {@link #classOutputPackages()} or {@link #classOutputModules()} instead.
262   */
263  @Deprecated(since = "0.6.4", forRemoval = true)
264  @SuppressWarnings("DeprecatedIsStillUsed")
265  public OutputContainerGroupAssert classOutput() {
266    return outputGroup(StandardLocation.CLASS_OUTPUT);
267  }
268
269  /**
270   * Get assertions on the path containing class package outputs, if it exists.
271   *
272   * <p>If not configured, the value being asserted on will be {@code null} in value.
273   *
274   * @return the assertions to perform on the class package outputs.
275   * @throws AssertionError if the compilation was null, or no group for the location
276   *                        was found.
277   */
278  @API(since = "0.6.4", status = Status.STABLE)
279  public PackageContainerGroupAssert classOutputPackages() {
280    return outputGroup(StandardLocation.CLASS_OUTPUT).packages();
281  }
282
283  /**
284   * Get assertions on the path containing class module outputs, 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 class module outputs.
289   * @throws AssertionError if the compilation was null, or no group for the location
290   *                        was found.
291   */
292  @API(since = "0.6.4", status = Status.STABLE)
293  public ModuleContainerGroupAssert classOutputModules() {
294    return outputGroup(StandardLocation.CLASS_OUTPUT).modules();
295  }
296
297  /**
298   * Get assertions on the path containing generated source outputs, 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 source outputs.
303   * @throws AssertionError if the compilation was null, or no group for the location
304   *                        was found.
305   * @deprecated use {@link #sourceOutputPackages()} or {@link #sourceOutputModules()} instead.
306   */
307  @Deprecated(since = "0.6.4", forRemoval = true)
308  @SuppressWarnings("DeprecatedIsStillUsed")
309  public OutputContainerGroupAssert sourceOutput() {
310    return outputGroup(StandardLocation.SOURCE_OUTPUT);
311  }
312
313  /**
314   * Get assertions on the path containing generated source package outputs, if it exists.
315   *
316   * <p>If not configured, the value being asserted on will be {@code null} in value.
317   *
318   * @return the assertions to perform on the source package outputs.
319   * @throws AssertionError if the compilation was null, or no group for the location
320   *                        was found.
321   */
322  @API(since = "0.6.4", status = Status.STABLE)
323  public PackageContainerGroupAssert sourceOutputPackages() {
324    return outputGroup(StandardLocation.SOURCE_OUTPUT).packages();
325  }
326
327  /**
328   * Get assertions on the path containing generated source module outputs, if it exists.
329   *
330   * <p>If not configured, the value being asserted on will be {@code null} in value.
331   *
332   * @return the assertions to perform on the source module outputs.
333   * @throws AssertionError if the compilation was null, or no group for the location
334   *                        was found.
335   */
336  @API(since = "0.6.4", status = Status.STABLE)
337  public ModuleContainerGroupAssert sourceOutputModules() {
338    return outputGroup(StandardLocation.SOURCE_OUTPUT).modules();
339  }
340
341  /**
342   * Get assertions on the path containing header outputs, if it exists.
343   *
344   * <p>If not configured, the value being asserted on will be {@code null} in value.
345   *
346   * @return the assertions to perform on the header outputs.
347   * @throws AssertionError if the compilation was null, or no group for the location
348   *                        was found.
349   * @deprecated this method is rarely needed, so is being removed in v1.0.0. Use
350   * {@link #outputGroup(Location)}, passing {@link StandardLocation#NATIVE_HEADER_OUTPUT} as the
351   * first parameter instead.
352   */
353  @Deprecated(since = "0.6.0", forRemoval = true)
354  @SuppressWarnings("DeprecatedIsStillUsed")
355  public OutputContainerGroupAssert generatedHeaders() {
356    return outputGroup(StandardLocation.NATIVE_HEADER_OUTPUT);
357  }
358
359  /**
360   * Get assertions on the path containing the class path, if it exists.
361   *
362   * <p>If not configured, the value being asserted on will be {@code null} in value.
363   *
364   * @return the assertions to perform on the class path.
365   * @throws AssertionError if the compilation was null, or no group for the location
366   *                        was found.
367   * @deprecated use {@link #classPathPackages()} instead.
368   */
369  @Deprecated(since = "0.6.4", forRemoval = true)
370  @SuppressWarnings("DeprecatedIsStillUsed")
371  public PackageContainerGroupAssert classPath() {
372    return packageGroup(StandardLocation.CLASS_PATH);
373  }
374
375  /**
376   * Get assertions on the path containing the class path, if it exists.
377   *
378   * <p>If not configured, the value being asserted on will be {@code null} in value.
379   *
380   * @return the assertions to perform on the class path.
381   * @throws AssertionError if the compilation was null, or no group for the location
382   *                        was found.
383   */
384  @API(since = "0.6.4", status = Status.STABLE)
385  public PackageContainerGroupAssert classPathPackages() {
386    return packageGroup(StandardLocation.CLASS_PATH);
387  }
388
389  /**
390   * Get assertions on the path containing the source path, if it exists.
391   *
392   * <p>If not configured, the value being asserted on will be {@code null} in value.
393   *
394   * @return the assertions to perform on the source path.
395   * @throws AssertionError if the compilation was null, or no group for the location
396   *                        was found.
397   * @deprecated use {@link #sourcePathPackages()} instead.
398   */
399  @Deprecated(since = "0.6.4", forRemoval = true)
400  @SuppressWarnings("DeprecatedIsStillUsed")
401  public PackageContainerGroupAssert sourcePath() {
402    return packageGroup(StandardLocation.SOURCE_PATH);
403  }
404
405  /**
406   * Get assertions on the path containing the source path, if it exists.
407   *
408   * <p>If not configured, the value being asserted on will be {@code null} in value.
409   *
410   * @return the assertions to perform on the source path.
411   * @throws AssertionError if the compilation was null, or no group for the location
412   *                        was found.
413   */
414  @API(since = "0.6.4", status = Status.STABLE)
415  public PackageContainerGroupAssert sourcePathPackages() {
416    return packageGroup(StandardLocation.SOURCE_PATH);
417  }
418
419  /**
420   * Get assertions on the path containing the source path, if it exists.
421   *
422   * <p>If not configured, the value being asserted on will be {@code null} in value.
423   *
424   * @return the assertions to perform on the source path.
425   * @throws AssertionError if the compilation was null, or no group for the location
426   *                        was found.
427   * @deprecated use {@link #moduleSourcePathModules()} instead.
428   */
429  @Deprecated(since = "0.6.4", forRemoval = true)
430  @SuppressWarnings("DeprecatedIsStillUsed")
431  public ModuleContainerGroupAssert moduleSourcePath() {
432    return moduleGroup(StandardLocation.MODULE_SOURCE_PATH);
433  }
434
435  /**
436   * Get assertions on the path containing the source path, if it exists.
437   *
438   * <p>If not configured, the value being asserted on will be {@code null} in value.
439   *
440   * @return the assertions to perform on the source path.
441   * @throws AssertionError if the compilation was null, or no group for the location
442   *                        was found.
443   */
444  @API(since = "0.6.4", status = Status.STABLE)
445  public ModuleContainerGroupAssert moduleSourcePathModules() {
446    return moduleGroup(StandardLocation.MODULE_SOURCE_PATH);
447  }
448
449  /**
450   * Get assertions on the path containing the module path, if it exists.
451   *
452   * <p>If not configured, the value being asserted on will be {@code null} in value.
453   *
454   * @return the assertions to perform on the module path.
455   * @throws AssertionError if the compilation was null, or no group for the location
456   *                        was found.
457   * @deprecated use {@link #modulePathModules()} instead.
458   */
459  @Deprecated(since = "0.6.4", forRemoval = true)
460  @SuppressWarnings("DeprecatedIsStillUsed")
461  public ModuleContainerGroupAssert modulePath() {
462    return moduleGroup(StandardLocation.MODULE_PATH);
463  }
464
465  /**
466   * Get assertions on the path containing the module path, if it exists.
467   *
468   * <p>If not configured, the value being asserted on will be {@code null} in value.
469   *
470   * @return the assertions to perform on the module path.
471   * @throws AssertionError if the compilation was null, or no group for the location
472   *                        was found.
473   */
474  @API(since = "0.6.4", status = Status.STABLE)
475  public ModuleContainerGroupAssert modulePathModules() {
476    return moduleGroup(StandardLocation.MODULE_PATH);
477  }
478
479
480  private AssertionError failWithDiagnostics(
481      Collection<? extends Kind> kindsToDisplay,
482      String message
483  ) {
484    var diagnostics = actual
485        .getDiagnostics()
486        .stream()
487        .filter(diagnostic -> kindsToDisplay.contains(diagnostic.getKind()))
488        .collect(toUnmodifiableList());
489
490    return failure(
491        "%s\n\nDiagnostics:\n%s",
492        message,
493        TraceDiagnosticListRepresentation.getInstance().toStringOf(diagnostics)
494    );
495  }
496
497  private void assertLocationExists(Location location, ContainerGroup group) {
498    if (group == null) {
499      throw failure("No location named %s exists", location.getName());
500    }
501  }
502}