Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,34 @@ public PackageContainerGroupAssert(@Nullable PackageContainerGroup containerGrou
*
* @param path the first path to check for
* @param paths additional paths to check for.
* @return this object for further call chaining.
* @throws AssertionError if the container group is null, or if any of the files do not
* exist.
* @throws NullPointerException if any of the paths are null.
*/
public void allFilesExist(String path, String... paths) {
public PackageContainerGroupAssert allFilesExist(String path, String... paths) {
requireNonNull(path, "path must not be null");
requireNonNullValues(paths, "paths");

allFilesExist(combineOneOrMore(path, paths));
return allFilesExist(combineOneOrMore(path, paths));
}

/**
* Assert that all given files exist.
*
* @param paths paths to check for.
* @return this object for further call chaining.
* @throws AssertionError if the container group is null, or if any of the files do not
* exist.
* @throws NullPointerException if any of the paths are null.
*/
public void allFilesExist(Iterable<String> paths) {
public PackageContainerGroupAssert allFilesExist(Iterable<String> paths) {
requireNonNullValues(paths, "paths");

isNotNull();

assertThat(paths).allSatisfy(this::fileExists);
return this;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public interface JctCompilationFactory {
* should be discovered automatically.
* @return the compilation result that contains whether the compiler succeeded or failed, amongst
* other information.
* @throws JctCompilerException if compiler raises an unhandled exception and cannot complete.
* @throws JctCompilerException if any prerequisites fail, such as no compilation units being
* found, or if the underlying JSR-199 compiler raises an unhandled exception and cannot
* complete when invoked.
*/
JctCompilation createCompilation(
List<String> flags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,21 @@
import io.github.ascopes.jct.ex.JctCompilerException;
import io.github.ascopes.jct.filemanagers.JctFileManager;
import io.github.ascopes.jct.filemanagers.LoggingMode;
import io.github.ascopes.jct.filemanagers.PathFileObject;
import io.github.ascopes.jct.utils.IterableUtils;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardLocation;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -69,9 +71,11 @@ public JctCompilation createCompilation(
) {
try {
return createCheckedCompilation(flags, fileManager, jsr199Compiler, classNames);

} catch (JctCompilerException ex) {
// Fall through, do not rewrap these.
// Rethrow JctCompilerExceptions -- we don't want to wrap these again.
throw ex;

} catch (Exception ex) {
throw new JctCompilerException(
"Failed to perform compilation, an unexpected exception was raised", ex
Expand All @@ -85,10 +89,14 @@ private JctCompilation createCheckedCompilation(
JavaCompiler jsr199Compiler,
Collection<String> classNames
) throws Exception {
var compilationUnits = findCompilationUnits(fileManager);
var compilationUnits = findFilteredCompilationUnits(fileManager, classNames);

if (compilationUnits.isEmpty()) {
throw new JctCompilerException("No compilation units were found in the given workspace");
}

// Do not close stdout, it breaks test engines, especially IntellIJ.
var writer = new TeeWriter(new OutputStreamWriter(System.out, compiler.getLogCharset()));
var writer = TeeWriter.wrapOutputStream(System.out, compiler.getLogCharset());

var diagnosticListener = new TracingDiagnosticListener<>(
compiler.getDiagnosticLoggingMode() != LoggingMode.DISABLED,
Expand All @@ -100,7 +108,7 @@ private JctCompilation createCheckedCompilation(
fileManager,
diagnosticListener,
flags,
classNames,
null,
compilationUnits
);

Expand All @@ -109,17 +117,13 @@ private JctCompilation createCheckedCompilation(
task.setProcessors(processors);
}

task.setLocale(compiler.getLocale());

LOGGER
.atInfo()
.setMessage(
"Starting compilation with {} (found {} compilation units, {} user-provided class names)"
)
.setMessage("Starting compilation with {} (found {} compilation units)")
.addArgument(compiler::getName)
.addArgument(compilationUnits::size)
.addArgument(classNames == null
? () -> "no"
: classNames::size
)
.log();

var start = System.nanoTime();
Expand All @@ -129,6 +133,9 @@ private JctCompilation createCheckedCompilation(
);
var delta = (System.nanoTime() - start) / 1_000_000L;

// Ensure we commit the writer contents to the wrapped output stream in full.
writer.flush();

LOGGER
.atInfo()
.setMessage("Compilation with {} {} after approximately {}ms (roughly {} classes/sec)")
Expand All @@ -145,16 +152,35 @@ private JctCompilation createCheckedCompilation(

return JctCompilationImpl
.builder()
.compilationUnits(compilationUnits)
.compilationUnits(Set.copyOf(compilationUnits))
.fileManager(fileManager)
.outputLines(writer.toString().lines().collect(toList()))
.outputLines(writer.getContent().lines().collect(toList()))
.diagnostics(diagnosticListener.getDiagnostics())
.success(success)
.failOnWarnings(compiler.isFailOnWarnings())
.build();
}

private Set<JavaFileObject> findCompilationUnits(JctFileManager fileManager) throws IOException {
private Collection<JavaFileObject> findFilteredCompilationUnits(
JctFileManager fileManager,
@Nullable Collection<String> classNames
) throws IOException {
var compilationUnits = findCompilationUnits(fileManager);

if (classNames == null) {
return compilationUnits;
}

if (classNames.isEmpty()) {
throw new JctCompilerException("The list of explicit class names to compile is empty");
}

return filterCompilationUnitsByBinaryNames(compilationUnits, classNames);
}

private Collection<JavaFileObject> findCompilationUnits(
JctFileManager fileManager
) throws IOException {
var locations = IterableUtils
.flatten(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH));

Expand All @@ -175,12 +201,40 @@ private Set<JavaFileObject> findCompilationUnits(JctFileManager fileManager) thr
objects.addAll(fileManager.list(location, "", Set.of(Kind.SOURCE), true));
}

if (objects.isEmpty()) {
throw new JctCompilerException(
"No compilation units were found. Did you forget to add something?"
);
return objects;
}

private Collection<JavaFileObject> filterCompilationUnitsByBinaryNames(
Collection<JavaFileObject> compilationUnits,
Collection<String> classNames
) {
var binaryNamesToCompilationUnits = compilationUnits
.stream()
// Assumption that we always use this class internally. Technically unsafe, but we don't
// care too much as the implementation should conform to this anyway. We just cannot enforce
// it due to covariance rules.
.map(PathFileObject.class::cast)
.filter(fo -> classNames.contains(fo.getBinaryName()))
.collect(Collectors.toMap(
PathFileObject::getBinaryName,
JavaFileObject.class::cast
));

for (var className : classNames) {
var compilationUnit = binaryNamesToCompilationUnits.get(className);
if (compilationUnit == null) {
throw new JctCompilerException(
"No compilation unit matching " + className + " found in the provided sources"
);
}
}

return objects;
LOGGER.atDebug()
.setMessage("Filtered {} candidate compilation units down to {} final compilation units")
.addArgument(compilationUnits::size)
.addArgument(binaryNamesToCompilationUnits::size)
.log();

return binaryNamesToCompilationUnits.values();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
package io.github.ascopes.jct.diagnostics;

import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

Expand Down Expand Up @@ -80,13 +84,31 @@ public void flush() throws IOException {
}
}

@Override
public String toString() {
/**
* Get the content of the internal buffer.
*
* @return the content.
* @since 0.2.1
*/
@API(since = "0.2.1", status = Status.STABLE)
public String getContent() {
synchronized (lock) {
return builder.toString();
}
}

/**
* Get the content of the internal buffer.
*
* <p>This calls {@link #getContent()} internally as of 0.2.1.
*
* @return the content.
*/
@Override
public String toString() {
return getContent();
}

@Override
public void write(char[] cbuf, int off, int len) throws IOException {
synchronized (lock) {
Expand All @@ -104,4 +126,23 @@ private void ensureOpen() {
throw new IllegalStateException("TeeWriter is closed");
}
}

/**
* Create a tee writer for the given output stream.
*
* <p>Remember you may need to manually flush the tee writer for all contents to be committed to
* the output stream.
*
* @param outputStream the output stream.
* @param charset the charset.
* @return the Tee Writer.
* @since 0.2.1
*/
@API(since = "0.2.1", status = Status.STABLE)
public static TeeWriter wrapOutputStream(OutputStream outputStream, Charset charset) {
requireNonNull(outputStream, "outputStream");
requireNonNull(charset, "charset");
var writer = new OutputStreamWriter(outputStream, charset);
return new TeeWriter(writer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ public Modifier getAccessLevel() {
return unknown();
}

/**
* Get the inferred binary name of the file object.
*
* @return the inferred binary name.
* @since 0.2.1
*/
@API(since = "0.2.1", status = Status.STABLE)
public String getBinaryName() {
return FileUtils.pathToBinaryName(relativePath);
}

/**
* Read the character content of the file into memory and decode it using the default character
* set.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2022 - 2023, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.ascopes.jct.tests.helpers;

import static org.mockito.ArgumentMatchers.argThat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import org.mockito.ArgumentMatcher;

/**
* Extra Mockito argument matchers.
*
* @author Ashley Scopes
*/
public final class ExtraArgumentMatchers {

private ExtraArgumentMatchers() {
throw new UnsupportedOperationException("static-only class");
}

@SafeVarargs
public static <E, T extends Iterable<E>> T containsExactlyElements(E... expected) {
return containsExactlyElements(Set.of(expected));
}

public static <E, T extends Iterable<E>> T containsExactlyElements(Collection<E> expected) {
return argThat(new ArgumentMatcher<>() {
@Override
public String toString() {
return "containsExactlyElements(" + expected + ")";
}

@Override
public boolean matches(T actualIterable) {
if (actualIterable == null) {
return false;
}

var actualElements = new ArrayList<E>();
actualIterable.forEach(actualElements::add);

// All expected are in actual
for (var expectedElement : expected) {
if (!actualElements.contains(expectedElement)) {
throw new IllegalArgumentException(
"Expected element " + expectedElement + " was not in the actual collection"
);
}
}

// All actual are in expected.
var expectedSet = Set.copyOf(expected);
for (var actualElement : actualElements) {
if (!expectedSet.contains(actualElement)) {
throw new IllegalArgumentException(
"Actual element " + actualElement + " was not in the expected elements array"
);
}
}

return true;
}
});
}
}
Loading