/* * Copyright 2016 ThoughtWorks, 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.thoughtworks.go.buildsession; import com.googlecode.junit.ext.JunitExtRunner; import com.googlecode.junit.ext.RunIf; import com.jezhumble.javasysmon.JavaSysMon; import com.jezhumble.javasysmon.OsProcess; import com.jezhumble.javasysmon.ProcessVisitor; import com.thoughtworks.go.domain.BuildCommand; import com.thoughtworks.go.junitext.EnhancedOSChecker; import com.thoughtworks.go.utils.Assertions; import com.thoughtworks.go.utils.Timeout; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import static com.google.common.collect.Iterables.getLast; import static com.thoughtworks.go.domain.BuildCommand.*; import static com.thoughtworks.go.domain.JobResult.Cancelled; import static com.thoughtworks.go.junitext.EnhancedOSChecker.DO_NOT_RUN_ON; import static com.thoughtworks.go.junitext.EnhancedOSChecker.WINDOWS; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.utils.Assertions.waitUntil; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.number.OrderingComparison.greaterThan; import static org.junit.Assert.assertTrue; @RunWith(JunitExtRunner.class) public class BuildSessionCancelingTest extends BuildSessionBasedTestCase { @Test public void cancelLongRunningBuild() throws InterruptedException { final BuildSession buildSession = newBuildSession(); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { buildSession.build(compose( execSleepScript(50), echo("build done"))); } }); buildingThread.start(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), true); assertTrue(buildInfo(), buildSession.cancel(30, TimeUnit.SECONDS)); waitUntilSubProcessExists(execSleepScriptProcessCommand(), false); assertThat(buildInfo(), getLast(statusReporter.results()), is(Cancelled)); assertThat(buildInfo(), console.output(), not(containsString("build done"))); buildingThread.join(); } @Test public void cancelLongRunningTestCommand() throws InterruptedException { final BuildSession buildSession = newBuildSession(); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { buildSession.build(compose( echo("after sleep").setTest(execSleepScript(50)))); } }); buildingThread.start(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), true); assertTrue(buildInfo(), buildSession.cancel(30, TimeUnit.SECONDS)); waitUntilSubProcessExists(execSleepScriptProcessCommand(), false); assertThat(buildInfo(), getLast(statusReporter.results()), is(Cancelled)); assertThat(buildInfo(), console.output(), not(containsString("after sleep"))); buildingThread.join(); } @Test public void doubleCancelDoNothing() throws InterruptedException { final BuildSession buildSession = newBuildSession(); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { buildSession.build(execSleepScript(50)); } }); Runnable cancel = new Runnable() { @Override public void run() { try { buildSession.cancel(30, TimeUnit.SECONDS); } catch (InterruptedException e) { throw bomb(e); } } }; Thread cancelThread1 = new Thread(cancel); Thread cancelThread2 = new Thread(cancel); buildingThread.start(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), true); cancelThread1.start(); cancelThread2.start(); cancelThread1.join(); cancelThread2.join(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), false); assertThat(buildInfo(), getLast(statusReporter.results()), is(Cancelled)); assertThat(buildInfo(), console.output(), not(containsString("after sleep"))); buildingThread.join(); } @Test public void cancelShouldProcessOnCancelCommandOfCommandThatIsRunning() throws InterruptedException { final BuildSession buildSession = newBuildSession(); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { buildSession.build(compose( compose( execSleepScript(50).setOnCancel(echo("exec canceled")), echo("after sleep")) .setOnCancel(echo("inner oncancel")) ).setOnCancel(echo("outter oncancel"))); } }); buildingThread.start(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), true); assertTrue(buildInfo(), buildSession.cancel(30, TimeUnit.SECONDS)); waitUntilSubProcessExists(execSleepScriptProcessCommand(), false); JavaSysMon javaSysMon = new JavaSysMon(); final boolean[] exists = {false}; javaSysMon.visitProcessTree(javaSysMon.currentPid(), new ProcessVisitor() { @Override public boolean visit(OsProcess osProcess, int i) { String command = osProcess.processInfo().getName(); if (execSleepScriptProcessCommand().equals(command)) { exists[0] = true; } return false; } }); assertThat(exists[0], is(false)); assertThat(buildInfo(), getLast(statusReporter.results()), is(Cancelled)); assertThat(buildInfo(), console.output(), not(containsString("after sleep"))); assertThat(buildInfo(), console.output(), containsString("exec canceled")); assertThat(buildInfo(), console.output(), containsString("inner oncancel")); assertThat(buildInfo(), console.output(), containsString("outter oncancel")); buildingThread.join(); } @Test @RunIf(value = EnhancedOSChecker.class, arguments = {DO_NOT_RUN_ON, WINDOWS}) public void cancelTaskShouldBeProcessedBeforeKillChildProcess() throws InterruptedException { final BuildSession buildSession = newBuildSession(); final BuildCommand printSubProcessCount = exec("/bin/bash", "-c", "pgrep -P " + new JavaSysMon().currentPid() + " | wc -l"); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { buildSession.build(compose( compose(execSleepScript(50), echo("after sleep")) .setOnCancel(printSubProcessCount))); } }); buildingThread.start(); waitUntilSubProcessExists(execSleepScriptProcessCommand(), true); assertTrue(buildInfo(), buildSession.cancel(30, TimeUnit.SECONDS)); waitUntilSubProcessExists(execSleepScriptProcessCommand(), false); assertThat(Integer.parseInt(console.lastLine().trim()), greaterThan(0)); buildingThread.join(); } private void waitUntilSubProcessExists(final String processName, final boolean expectExist) { try { waitUntil(Timeout.FIVE_SECONDS, new Assertions.Predicate() { @Override public boolean call() throws Exception { return subProcessNames().contains(processName) == expectExist; } }, 250); } catch (RuntimeException e) { throw new RuntimeException("timeout waiting for subprocess " + (expectExist ? "exists" : "not exists") + ", current sub processes are: " + subProcessNames()); } } private List<String> subProcessNames() { JavaSysMon javaSysMon = new JavaSysMon(); final List<String> names = new ArrayList<>(); final int currentPid = javaSysMon.currentPid(); javaSysMon.visitProcessTree(currentPid, new ProcessVisitor() { @Override public boolean visit(OsProcess osProcess, int i) { if(osProcess.processInfo().getPid() != currentPid) { names.add(osProcess.processInfo().getName()); } return false; } }); return names; } }