/* * Copyright 2016-present Facebook, Inc. * * 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 com.facebook.buck.rust; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.AddToRuleKey; import com.facebook.buck.rules.BinaryBuildRule; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.ExternalTestRunnerRule; import com.facebook.buck.rules.ExternalTestRunnerTestSpec; import com.facebook.buck.rules.ForwardingBuildTargetSourcePath; import com.facebook.buck.rules.HasRuntimeDeps; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TestRule; import com.facebook.buck.rules.Tool; import com.facebook.buck.step.AbstractTestStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.fs.MakeCleanDirectoryStep; import com.facebook.buck.test.TestCaseSummary; import com.facebook.buck.test.TestResultSummary; import com.facebook.buck.test.TestResults; import com.facebook.buck.test.TestRunningOptions; import com.facebook.buck.test.result.type.ResultType; import com.facebook.buck.util.MoreCollectors; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.Callable; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @SuppressWarnings("PMD.TestClassWithoutTestCases") public class RustTest extends AbstractBuildRule implements BinaryBuildRule, TestRule, ExternalTestRunnerRule, HasRuntimeDeps { private final ImmutableSet<String> labels; private final ImmutableSet<String> contacts; @AddToRuleKey private final BinaryBuildRule testExeBuild; private static final Pattern TEST_STDOUT_PATTERN = Pattern.compile("^---- (?<name>.+) stdout ----$"); private static final Pattern FAILURES_LIST_PATTERN = Pattern.compile("^failures:$"); private final Path testOutputFile; private final Path testStdoutFile; private final SourcePathRuleFinder ruleFinder; protected RustTest( BuildRuleParams params, SourcePathRuleFinder ruleFinder, BinaryBuildRule testExeBuild, ImmutableSet<String> labels, ImmutableSet<String> contacts) { super(params); this.testExeBuild = testExeBuild; this.ruleFinder = ruleFinder; this.labels = labels; this.contacts = contacts; this.testOutputFile = getProjectFilesystem().resolve(getPathToTestResults()); this.testStdoutFile = getProjectFilesystem().resolve(getPathToTestStdout()); } @Override public ImmutableList<Step> runTests( ExecutionContext executionContext, TestRunningOptions options, SourcePathResolver pathResolver, TestReportingCallback testReportingCallback) { Path workingDirectory = getProjectFilesystem().resolve(getPathToTestOutputDirectory()); return new ImmutableList.Builder<Step>() .addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), workingDirectory)) .add( new AbstractTestStep( "rust test", getProjectFilesystem(), Optional.of(workingDirectory), getTestCommand(pathResolver, "--logfile", testOutputFile.toString()), Optional.empty(), // TODO(stash): environment workingDirectory.resolve("exitcode"), Optional.empty(), testStdoutFile) {}) .build(); } @Override public Callable<TestResults> interpretTestResults( ExecutionContext executionContext, boolean isUsingTestSelectors) { return () -> { ImmutableList<TestCaseSummary> summaries = ImmutableList.of(); summaries = ImmutableList.of( new TestCaseSummary(getBuildTarget().getFullyQualifiedName(), parseTestResults())); return TestResults.of( getBuildTarget(), summaries, getContacts(), getLabels().stream().map(Object::toString).collect(MoreCollectors.toImmutableSet())); }; } @Override public ImmutableSet<String> getLabels() { return labels; } @Override public ImmutableSet<String> getContacts() { return contacts; } @Override public Path getPathToTestOutputDirectory() { return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s"); } @Override public boolean runTestSeparately() { return false; } @Override public boolean supportsStreamingTests() { return false; } @Override public ExternalTestRunnerTestSpec getExternalTestRunnerSpec( ExecutionContext executionContext, TestRunningOptions testRunningOptions, SourcePathResolver pathResolver) { return ExternalTestRunnerTestSpec.builder() .setTarget(getBuildTarget()) .setType("rust") .addAllCommand(getTestCommand(pathResolver)) .addAllLabels(getLabels()) .addAllContacts(getContacts()) .build(); } private ImmutableList<String> getTestCommand( SourcePathResolver pathResolver, String... additionalArgs) { ImmutableList.Builder<String> args = ImmutableList.builder(); args.addAll(testExeBuild.getExecutableCommand().getCommandPrefix(pathResolver)); args.add(additionalArgs); return args.build(); } private Path getPathToTestResults() { return getPathToTestOutputDirectory().resolve("output"); } private Path getPathToTestStdout() { return getPathToTestOutputDirectory().resolve("stdout"); } private ImmutableList<TestResultSummary> parseTestResults() throws IOException { Map<String, ResultType> testToResult = new HashMap<>(); try (BufferedReader reader = Files.newBufferedReader(testOutputFile, Charsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { String[] resultAndTestName = line.split(" "); if (resultAndTestName.length != 2) { throw new RuntimeException(String.format("Unknown test output format %s", line)); } ResultType result; switch (resultAndTestName[0]) { case "ok": result = ResultType.SUCCESS; break; case "failed": result = ResultType.FAILURE; break; case "ignored": result = ResultType.DISABLED; break; default: throw new RuntimeException(String.format("Unknown test status %s", line)); } testToResult.put(resultAndTestName[1], result); } } Map<String, String> testToStdout = new HashMap<>(); try (BufferedReader reader = Files.newBufferedReader(testStdoutFile, Charsets.UTF_8)) { StringBuilder stdout = new StringBuilder(); String currentStdoutTestName = null; BiConsumer<String, String> addTestStdout = (key, value) -> { testToStdout.put(key, value); stdout.setLength(0); }; String line; while ((line = reader.readLine()) != null) { Matcher matcher; if ((matcher = TEST_STDOUT_PATTERN.matcher(line)).matches()) { if (currentStdoutTestName != null) { // Start of stdout output for new test // Save current stdout addTestStdout.accept(currentStdoutTestName, stdout.toString()); } currentStdoutTestName = matcher.group("name"); } else if (FAILURES_LIST_PATTERN.matcher(line).matches()) { if (currentStdoutTestName != null) { addTestStdout.accept(currentStdoutTestName, stdout.toString()); } } else if (currentStdoutTestName != null) { stdout.append("\n"); stdout.append(line); } } } ImmutableList.Builder<TestResultSummary> summariesBuilder = ImmutableList.builder(); for (Map.Entry<String, ResultType> entry : testToResult.entrySet()) { summariesBuilder.add( new TestResultSummary( "rust_test", entry.getKey(), entry.getValue(), 0, // TODO(stash) time "", // message "", // stack trace, testToStdout.get(entry.getKey()), "" // stderr )); } return summariesBuilder.build(); } @Override public Tool getExecutableCommand() { return testExeBuild.getExecutableCommand(); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, BuildableContext buildableContext) { return ImmutableList.of(); } @Override public SourcePath getSourcePathToOutput() { return new ForwardingBuildTargetSourcePath( getBuildTarget(), testExeBuild.getSourcePathToOutput()); } @Override public Stream<BuildTarget> getRuntimeDeps() { return Stream.concat( getDeclaredDeps().stream(), getExecutableCommand().getDeps(ruleFinder).stream()) .map(BuildRule::getBuildTarget); } }