/* * 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.python; import com.facebook.buck.shell.ShellStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.test.selectors.TestDescription; import com.facebook.buck.test.selectors.TestSelectorList; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Path; import java.util.EnumSet; import java.util.Optional; import java.util.function.Consumer; public class PythonRunTestsStep implements Step { private static final CharMatcher PYTHON_RE_REGULAR_CHARACTERS = CharMatcher.anyOf("_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"); public static final int TEST_FAILURES_EXIT_CODE = 70; private final Path workingDirectory; private final String testName; private final ImmutableList<String> commandPrefix; private final ImmutableMap<String, String> environment; private final TestSelectorList testSelectorList; private final Optional<Long> testRuleTimeoutMs; private final Path resultsOutputPath; private final Consumer<Process> timeoutHandler = input -> { timedOut = true; }; private boolean timedOut; public PythonRunTestsStep( Path workingDirectory, String testName, ImmutableList<String> commandPrefix, ImmutableMap<String, String> environment, TestSelectorList testSelectorList, Optional<Long> testRuleTimeoutMs, Path resultsOutputPath) { this.workingDirectory = workingDirectory; this.testName = testName; this.commandPrefix = commandPrefix; this.environment = environment; this.testSelectorList = testSelectorList; this.testRuleTimeoutMs = testRuleTimeoutMs; this.resultsOutputPath = resultsOutputPath; this.timedOut = false; } @Override public StepExecutionResult execute(ExecutionContext context) throws IOException, InterruptedException { StepExecutionResult result = doExecute(context); if (timedOut) { throw new HumanReadableException( "Following test case timed out: " + testName + ", with exitCode: " + result.getExitCode()); } return result; } private StepExecutionResult doExecute(ExecutionContext context) throws IOException, InterruptedException { if (testSelectorList.isEmpty()) { return getShellStepWithArgs("-o", resultsOutputPath.toString()).execute(context); } ProcessExecutorParams params = ProcessExecutorParams.builder() .setCommand( ImmutableList.<String>builder() .addAll(commandPrefix) .add("-l", "-L", "buck") .build()) .setDirectory(workingDirectory) .setEnvironment(environment) .build(); ProcessExecutor.Result result = context .getProcessExecutor() .launchAndExecute( params, EnumSet.of(ProcessExecutor.Option.EXPECTING_STD_OUT), Optional.empty(), testRuleTimeoutMs, Optional.of(timeoutHandler)); if (timedOut) { return StepExecutionResult.ERROR; } else if (result.getExitCode() != 0) { return StepExecutionResult.of(result); } Preconditions.checkState(result.getStdout().isPresent()); String testsToRunRegex = getTestsToRunRegexFromListOutput(result.getStdout().get()); return getShellStepWithArgs( "--hide-output", "-o", resultsOutputPath.toString(), "-r", testsToRunRegex) .execute(context); } private String getTestsToRunRegexFromListOutput(String listOutput) { ImmutableList.Builder<String> testsToRunPatternComponents = ImmutableList.builder(); for (String strTestCase : CharMatcher.whitespace().trimFrom(listOutput).split("\n")) { String[] testCase = CharMatcher.whitespace().trimFrom(strTestCase).split("#", 2); if (testCase.length != 2) { throw new RuntimeException( String.format("Bad test case name from python runner: '%s'", strTestCase)); } TestDescription testDescription = new TestDescription(testCase[0], testCase[1]); if (testSelectorList.isIncluded(testDescription)) { testsToRunPatternComponents.add(escapeForPythonRegex(strTestCase)); } } return "^" + Joiner.on('|').join(testsToRunPatternComponents.build()) + "$"; } // Escapes a string for python's re module. Note Pattern.quote uses \Q and \E which do not exist // in python. // This is based on https://github.com/python/cpython/blob/2.7/Lib/re.py#L208 . private String escapeForPythonRegex(String s) { StringBuilder result = new StringBuilder((int) (s.length() * 1.3)); for (char c : s.toCharArray()) { if (!PYTHON_RE_REGULAR_CHARACTERS.matches(c)) { result.append('\\'); } result.append(c); } return result.toString(); } @Override public String getShortName() { return "pyunit"; } @Override public String getDescription(ExecutionContext context) { if (testSelectorList.isEmpty()) { return getShellStepWithArgs("-o", resultsOutputPath.toString()).getDescription(context); } return getShellStepWithArgs("-o", resultsOutputPath.toString(), "-r", "<matching tests>") .getDescription(context); } private ShellStep getShellStepWithArgs(final String... args) { return new ShellStep(workingDirectory) { @Override public StepExecutionResult execute(ExecutionContext context) throws InterruptedException, IOException { StepExecutionResult executionResult = super.execute(context); // The test runner returns 0 if all tests passed, or // TEST_FAILURES_EXIT_CODE if some tests failed. Either of these // return codes indicates that we succeeded in running the tests. if (executionResult.getExitCode() == 0 || executionResult.getExitCode() == TEST_FAILURES_EXIT_CODE) { return StepExecutionResult.SUCCESS; } return executionResult; } @Override protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) { return ImmutableList.<String>builder().addAll(commandPrefix).add(args).build(); } @Override public String getShortName() { throw new UnsupportedOperationException(); } @Override public ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) { return environment; } @Override protected Optional<Long> getTimeout() { return testRuleTimeoutMs; } @Override protected Optional<Consumer<Process>> getTimeoutHandler(ExecutionContext context) { return Optional.of(timeoutHandler); } }; } }