blob: 3b66a1285647cda84b974051169cf74cfe47f7b0 [file] [log] [blame]
/*
* Copyright 2018 The Bazel Authors. All rights reserved.
*
* 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.bazel.kotlin.builder;
import io.bazel.kotlin.builder.toolchain.CompilationException;
import io.bazel.kotlin.builder.toolchain.CompilationStatusException;
import io.bazel.kotlin.builder.utils.CompilationTaskContext;
import io.bazel.kotlin.model.CompilationTaskInfo;
import io.bazel.kotlin.model.KotlinToolchainInfo;
import io.bazel.kotlin.model.Platform;
import io.bazel.kotlin.model.RuleKind;
import org.junit.rules.ExternalResource;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
public abstract class KotlinBuilderResource<T> extends ExternalResource {
public enum DirectoryType {
INSTANCE_ROOT("test root", null),
EXTERNAL("bazel external directory", null),
/** The rest of the paths are instance relative. */
SOURCES("sources", Paths.get("sources")),
CLASSES("compiled classes", Paths.get("classes")),
GENERATED_CLASSES("generated classes", Paths.get("generated_classes")),
TEMP("temp directory", Paths.get("temp")),
SOURCE_GEN("generated sources directory", Paths.get("generated_sources"));
private static final EnumSet<DirectoryType> INSTANCE_TYPES =
EnumSet.of(SOURCES, CLASSES, SOURCE_GEN, GENERATED_CLASSES, TEMP);
final String name;
private final Path relativePath;
DirectoryType(String name, Path relativePath) {
this.name = name;
this.relativePath = relativePath;
}
}
private static final Path
BAZEL_TEST_DIR = Paths.get(Objects.requireNonNull(System.getenv("TEST_TMPDIR"))),
EXTERNAL_PATH = Paths.get("external");
private static final AtomicInteger counter = new AtomicInteger(0);
private static final int DEFAULT_TIMEOUT = 10;
private Path instanceRoot = null;
private String label = null;
private int timeoutSeconds = DEFAULT_TIMEOUT;
private List<String> outLines = null;
KotlinBuilderResource() {}
abstract CompilationTaskInfo.Builder infoBuilder();
abstract T buildTask();
final String label() {
return Objects.requireNonNull(label);
}
final Path instanceRoot() {
return Objects.requireNonNull(instanceRoot);
}
@SuppressWarnings("WeakerAccess")
public final List<String> outLines() {
return outLines;
}
@Override
protected void before() throws Throwable {
outLines = null;
setTimeout(DEFAULT_TIMEOUT);
label = "a_test_" + counter.incrementAndGet();
infoBuilder()
.setLabel("//some/bogus:" + label())
.setModuleName("some_bogus_module")
.setPlatform(Platform.JVM)
.setRuleKind(RuleKind.LIBRARY)
.setToolchainInfo(
KotlinToolchainInfo.newBuilder()
.setCommon(
KotlinToolchainInfo.Common.newBuilder()
.setApiVersion("1.2")
.setCoroutines("enabled")
.setLanguageVersion("1.2"))
.setJvm(KotlinToolchainInfo.Jvm.newBuilder().setJvmTarget("1.8")));
try {
this.instanceRoot = Files.createDirectory(BAZEL_TEST_DIR.resolve(Paths.get(label)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
for (DirectoryType instanceType : DirectoryType.INSTANCE_TYPES) {
try {
Files.createDirectory(instanceRoot.resolve(instanceType.relativePath));
} catch (IOException e) {
throw new RuntimeException("could not create instance directory: " + instanceType.name, e);
}
}
}
final Path directory(DirectoryType type) {
switch (type) {
case INSTANCE_ROOT:
return instanceRoot;
case EXTERNAL:
return KotlinBuilderResource.EXTERNAL_PATH;
case SOURCES:
case CLASSES:
case GENERATED_CLASSES:
case TEMP:
case SOURCE_GEN:
return instanceRoot.resolve(type.relativePath);
default:
throw new IllegalStateException(type.toString());
}
}
@SuppressWarnings("unused")
public final void setDebugTags(String... tags) {
infoBuilder().addAllDebug(Arrays.asList(tags));
}
final Path writeSourceFile(String filename, String[] lines) {
Path path = directory(DirectoryType.SOURCES).resolve(filename).toAbsolutePath();
try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
fos.write(String.join("\n", lines).getBytes(UTF_8));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return path;
}
/**
* sets the timeout for the builder tasks.
*
* @param timeoutSeconds a timeout in seconds. For debugging purposes it can be set to <= 0 which
* means wait indefinitely.
*/
@SuppressWarnings("WeakerAccess")
public final void setTimeout(int timeoutSeconds) {
this.timeoutSeconds = timeoutSeconds;
}
private <R> R runCompileTask(
CompilationTaskInfo info, T task, BiFunction<CompilationTaskContext, T, R> operation) {
String curDir = System.getProperty("user.dir");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (PrintStream outputStream = new PrintStream(byteArrayOutputStream)) {
System.setProperty("user.dir", instanceRoot().toAbsolutePath().toString());
CompletableFuture<R> future =
CompletableFuture.supplyAsync(
() -> operation.apply(new CompilationTaskContext(info, outputStream), task));
return timeoutSeconds > 0 ? future.get(timeoutSeconds, TimeUnit.SECONDS) : future.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof CompilationStatusException) {
throw (CompilationStatusException) e.getCause();
} else if (e.getCause() instanceof CompilationException) {
throw (CompilationException) e.getCause();
} else {
throw new RuntimeException(e.getCause());
}
} catch (TimeoutException e) {
throw new AssertionError("did not complete in: " + timeoutSeconds);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
System.setProperty("user.dir", curDir);
outLines =
Collections.unmodifiableList(
new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(byteArrayOutputStream.toByteArray())))
.lines()
.peek(System.err::println)
.collect(Collectors.toList()));
}
}
final <R> R runCompileTask(BiFunction<CompilationTaskContext, T, R> operation) {
CompilationTaskInfo info = infoBuilder().build();
T task = buildTask();
return runCompileTask(info, task, (ctx, t) -> operation.apply(ctx, task));
}
/**
* Run a compilation task expecting it to fail with a {@link CompilationStatusException}.
*
* @param task the compilation task
* @param validator a consumer for the output produced by the task.
*/
public final void runFailingCompileTaskAndValidateOutput(
Runnable task, Consumer<List<String>> validator) {
try {
task.run();
} catch (CompilationStatusException ex) {
validator.accept(outLines());
return;
}
throw new RuntimeException("compilation task should have failed.");
}
public final void assertFilesExist(DirectoryType dir, String... paths) {
assertFileExistence(resolved(dir, paths), true);
}
final void assertFilesExist(String... paths) {
assertFileExistence(Stream.of(paths).map(Paths::get), true);
}
@SuppressWarnings("unused")
public final void assertFilesDoNotExist(DirectoryType dir, String... filePath) {
assertFileExistence(resolved(dir, filePath), false);
}
private static void assertFileExistence(Stream<Path> pathStream, boolean shouldExist) {
pathStream.forEach(
path -> {
if (shouldExist)
assertWithMessage("file did not exist: " + path).that(path.toFile().exists()).isTrue();
else assertWithMessage("file existed: " + path).that(path.toFile().exists()).isFalse();
});
}
private Stream<Path> resolved(DirectoryType dir, String... filePath) {
Path directory = directory(dir);
return Stream.of(filePath).map(f -> directory.resolve(toPlatformPath(f)));
}
/**
* Normalize a path string.
*
* @param path a path using '/' as the separator.
* @return a path string suitable for the target platform.
*/
private static Path toPlatformPath(String path) {
assert !path.startsWith("/") : path + " is an absolute path";
String[] parts = path.split("/");
return parts.length == 1
? Paths.get(parts[0])
: Paths.get(parts[0], Arrays.copyOfRange(parts, 1, parts.length));
}
public final String toPlatform(String path) {
return KotlinBuilderResource.toPlatformPath(path).toString();
}
@SuppressWarnings("unused")
private Stream<Path> directoryContents(DirectoryType type) {
try {
return Files.walk(directory(type)).map(p -> directory(type).relativize(p));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@SuppressWarnings("unused")
public final void logDirectoryContents(DirectoryType type) {
System.out.println(
directoryContents(type)
.map(Path::toString)
.collect(Collectors.joining("\n", "directory " + type.name + " contents:\n", "")));
}
}