/* * Copyright 2012-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.jvm.java; import com.facebook.buck.io.ExecutableFinder; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.shell.ShellStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.facebook.buck.util.environment.Platform; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.lang.reflect.Field; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; public class JUnitStep extends ShellStep { private static final Logger LOG = Logger.get(JUnitStep.class); private final ProjectFilesystem filesystem; private final JavaRuntimeLauncher javaRuntimeLauncher; private final ImmutableMap<String, String> nativeLibsEnvironment; private final Optional<Long> testRuleTimeoutMs; private final Optional<Long> testCaseTimeoutMs; private final ImmutableMap<String, String> env; private final JUnitJvmArgs junitJvmArgs; // Set when the junit command times out. private boolean hasTimedOut = false; public JUnitStep( ProjectFilesystem filesystem, Map<String, String> nativeLibsEnvironment, Optional<Long> testRuleTimeoutMs, Optional<Long> testCaseTimeoutMs, ImmutableMap<String, String> env, JavaRuntimeLauncher javaRuntimeLauncher, JUnitJvmArgs junitJvmArgs) { super(filesystem.getRootPath()); this.filesystem = filesystem; this.javaRuntimeLauncher = javaRuntimeLauncher; this.nativeLibsEnvironment = ImmutableMap.copyOf(nativeLibsEnvironment); this.testRuleTimeoutMs = testRuleTimeoutMs; this.testCaseTimeoutMs = testCaseTimeoutMs; this.env = env; this.junitJvmArgs = junitJvmArgs; } @Override public String getShortName() { return "junit"; } @Override protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) { ImmutableList.Builder<String> args = ImmutableList.builder(); args.add(javaRuntimeLauncher.getCommand()); junitJvmArgs.formatCommandLineArgsToList( args, filesystem, context.getVerbosity(), testCaseTimeoutMs.orElse(context.getDefaultTestTimeoutMillis())); if (junitJvmArgs.isDebugEnabled()) { warnUser( context, "Debugging. Suspending JVM. Connect a JDWP debugger to port 5005 to proceed."); } return args.build(); } @Override public ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) { ImmutableMap.Builder<String, String> env = ImmutableMap.builder(); env.putAll(this.env); env.putAll(nativeLibsEnvironment); return env.build(); } private void warnUser(ExecutionContext context, String message) { context.getStdErr().println(context.getAnsi().asWarningText(message)); } @Override protected Optional<Long> getTimeout() { return testRuleTimeoutMs; } @Override protected Optional<Consumer<Process>> getTimeoutHandler(final ExecutionContext context) { return Optional.of( process -> { Optional<Long> pid = Optional.empty(); Platform platform = context.getPlatform(); try { switch (platform) { case LINUX: case FREEBSD: case MACOS: { Field field = process.getClass().getDeclaredField("pid"); field.setAccessible(true); try { pid = Optional.of((long) field.getInt(process)); } catch (IllegalAccessException e) { LOG.error(e, "Failed to access `pid`."); } break; } case WINDOWS: { Field field = process.getClass().getDeclaredField("handle"); field.setAccessible(true); try { pid = Optional.of(field.getLong(process)); } catch (IllegalAccessException e) { LOG.error(e, "Failed to access `handle`."); } break; } case UNKNOWN: LOG.info("Unknown platform; unable to obtain the process id!"); break; } } catch (NoSuchFieldException e) { LOG.error(e); } Optional<Path> jstack = new ExecutableFinder(context.getPlatform()) .getOptionalExecutable(Paths.get("jstack"), context.getEnvironment()); if (!pid.isPresent() || !jstack.isPresent()) { LOG.info("Unable to print a stack trace for timed out test!"); return; } context .getStdErr() .println("Test has timed out! Here is a trace of what it is currently doing:"); try { context .getProcessExecutor() .launchAndExecute( /* command */ ProcessExecutorParams.builder() .addCommand(jstack.get().toString(), "-l", pid.get().toString()) .setEnvironment(context.getEnvironment()) .build(), /* options */ ImmutableSet.<ProcessExecutor.Option>builder() .add(ProcessExecutor.Option.PRINT_STD_OUT) .add(ProcessExecutor.Option.PRINT_STD_ERR) .build(), /* stdin */ Optional.empty(), /* timeOutMs */ Optional.of(TimeUnit.SECONDS.toMillis(30)), /* timeOutHandler */ Optional.of( input -> { context .getStdErr() .print( "Printing the stack took longer than 30 seconds. No longer trying."); })); } catch (Exception e) { LOG.error(e); } }); } @Override protected int getExitCodeFromResult(ExecutionContext context, ProcessExecutor.Result result) { int exitCode = result.getExitCode(); // If we timed out, force the exit code to 0 just so that the step itself doesn't fail, // allowing us to interpret any test cases that finished before the bad test. We signify // this special case by setting `hasTimedOut` which the result interpreter will query to // properly format its results. if (result.isTimedOut()) { exitCode = 0; hasTimedOut = true; } return exitCode; } public boolean hasTimedOut() { return hasTimedOut; } }