/* * Copyright (C) 2010 The Android Open Source Project * * 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.android.tradefed.command; import com.android.tradefed.config.ConfigurationException; import com.android.tradefed.config.IConfiguration; import com.android.tradefed.config.IConfigurationFactory; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.DeviceSelectionOptions; import com.android.tradefed.device.IDeviceManager; import com.android.tradefed.device.IDeviceManager.FreeDeviceState; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.device.MockDeviceManager; import com.android.tradefed.invoker.IRescheduler; import com.android.tradefed.invoker.ITestInvocation; import junit.framework.TestCase; import org.easymock.EasyMock; import org.easymock.IAnswer; import java.lang.Thread.UncaughtExceptionHandler; /** * Unit tests for {@link CommandScheduler}. */ public class CommandSchedulerTest extends TestCase { private CommandScheduler mScheduler; private ITestInvocation mMockInvocation; private MockDeviceManager mMockManager; private IConfigurationFactory mMockConfigFactory; private IConfiguration mMockConfiguration; private CommandOptions mCommandOptions; private DeviceSelectionOptions mDeviceOptions; /** * {@inheritDoc} */ @Override protected void setUp() throws Exception { super.setUp(); mMockInvocation = EasyMock.createMock(ITestInvocation.class); mMockManager = new MockDeviceManager(0); mMockConfigFactory = EasyMock.createMock(IConfigurationFactory.class); mMockConfiguration = EasyMock.createMock(IConfiguration.class); mCommandOptions = new CommandOptions(); mDeviceOptions = new DeviceSelectionOptions(); mScheduler = new CommandScheduler() { @Override ITestInvocation createRunInstance() { return mMockInvocation; } @Override IDeviceManager getDeviceManager() { return mMockManager; } @Override IConfigurationFactory getConfigFactory() { return mMockConfigFactory; } @Override long getCommandPollTimeMs() { return 20; } @Override void initLogging() { // ignore } @Override void cleanUp() { // ignore } }; } /** * Switch all mock objects to replay mode */ private void replayMocks(Object... additionalMocks) { EasyMock.replay(mMockConfigFactory, mMockConfiguration, mMockInvocation); for (Object mock : additionalMocks) { EasyMock.replay(mock); } } /** * Verify all mock objects */ private void verifyMocks() { EasyMock.verify(mMockConfigFactory, mMockConfiguration, mMockInvocation); mMockManager.assertDevicesFreed(); } /** * Test {@link CommandScheduler#run()} when no configs have been added */ public void testRun_empty() throws InterruptedException { mMockManager.setNumDevices(1); replayMocks(); mScheduler.start(); while (!mScheduler.isAlive()) { Thread.sleep(10); } mScheduler.shutdown(); // expect run not to block mScheduler.join(); verifyMocks(); } /** * Test {@link CommandScheduler#addCommand(String[])} when help mode is specified */ public void testAddConfig_configHelp() throws ConfigurationException { String[] args = new String[] {}; mCommandOptions.setHelpMode(true); setCreateConfigExpectations(args, 1); // expect mMockConfigFactory.printHelpForConfig(EasyMock.aryEq(args), EasyMock.eq(true), EasyMock.eq(System.out)); replayMocks(); mScheduler.addCommand(args); verifyMocks(); } /** * Test {@link CommandScheduler#run()} when one config has been added */ public void testRun_oneConfig() throws Throwable { String[] args = new String[] {}; mMockManager.setNumDevices(2); setCreateConfigExpectations(args, 1); setExpectedInvokeCalls(1); mMockConfiguration.validateOptions(); replayMocks(); mScheduler.addCommand(args); mScheduler.start(); mScheduler.shutdownOnEmpty(); mScheduler.join(); verifyMocks(); } /** * Test {@link CommandScheduler#run()} when one config has been added in dry-run mode */ public void testRun_dryRun() throws Throwable { String[] dryRunArgs = new String[] {"--dry-run"}; mCommandOptions.setDryRunMode(true); mMockManager.setNumDevices(2); setCreateConfigExpectations(dryRunArgs, 1); // add a second command, to verify the first dry-run command did not get added String[] args2 = new String[] {}; setCreateConfigExpectations(args2, 1); setExpectedInvokeCalls(1); mMockConfiguration.validateOptions(); replayMocks(); assertFalse(mScheduler.addCommand(dryRunArgs)); // the same config object is being used, so clear its state mCommandOptions.setDryRunMode(false); assertTrue(mScheduler.addCommand(args2)); mScheduler.start(); mScheduler.shutdownOnEmpty(); mScheduler.join(); verifyMocks(); } /** * Sets the number of expected * {@link ITestInvocation#invoke(ITestDevice, IConfiguration, IRescheduler)} calls * * @param times */ private void setExpectedInvokeCalls(int times) throws Throwable { mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(), (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject()); EasyMock.expectLastCall().times(times); } /** * Sets up a object that will notify when the expected number of * {@link ITestInvocation#invoke(ITestDevice, IConfiguration, IRescheduler)} calls occurs * * @param times */ private Object waitForExpectedInvokeCalls(final int times) throws Throwable { IAnswer<Object> blockResult = new IAnswer<Object>() { private int mCalls = 0; @Override public Object answer() throws Throwable { synchronized(this) { mCalls++; if (times == mCalls) { notifyAll(); } } return null; } }; mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(), (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject()); EasyMock.expectLastCall().andAnswer(blockResult); EasyMock.expectLastCall().andAnswer(blockResult); return blockResult; } /** * Test {@link CommandScheduler#run()} when one config has been added in a loop */ public void testRun_oneConfigLoop() throws Throwable { String[] args = new String[] {}; // track if exception occurs on scheduler thread UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); try { ExceptionTracker tracker = new ExceptionTracker(); Thread.setDefaultUncaughtExceptionHandler(tracker); mMockManager.setNumDevices(1); // config should only be created three times setCreateConfigExpectations(args, 3); mCommandOptions.setLoopMode(true); mCommandOptions.setMinLoopTime(0); Object notifier = waitForExpectedInvokeCalls(2); mMockConfiguration.validateOptions(); replayMocks(); mScheduler.addCommand(args); mScheduler.start(); synchronized (notifier) { notifier.wait(1 * 1000); } mScheduler.shutdown(); mScheduler.join(); verifyMocks(); assertNull("exception occurred on background thread!", tracker.mThrowable); } finally { Thread.setDefaultUncaughtExceptionHandler(defaultHandler); } } class ExceptionTracker implements UncaughtExceptionHandler { private Throwable mThrowable = null; /** * {@inheritDoc} */ @Override public void uncaughtException(Thread t, Throwable e) { e.printStackTrace(); mThrowable = e; } } /** * Verify that scheduler goes into shutdown mode when a {@link FatalHostError} is thrown. */ public void testRun_fatalError() throws Throwable { mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(), (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject()); EasyMock.expectLastCall().andThrow(new FatalHostError("error")); String[] args = new String[] {}; mMockManager.setNumDevices(2); setCreateConfigExpectations(args, 1); mMockConfiguration.validateOptions(); replayMocks(); mScheduler.addCommand(args); mScheduler.start(); // no need to call shutdown explicitly - scheduler should shutdown by itself mScheduler.join(); verifyMocks(); } /** * Test{@link CommandScheduler#run()} when config is matched to a specific device serial number * <p/> * Adds two configs to run, and verify they both run on one device */ public void testRun_configSerial() throws Throwable { String[] args = new String[] {}; mMockManager.setNumDevices(2); setCreateConfigExpectations(args, 2); // allocate and free a device to get its serial ITestDevice dev = mMockManager.allocateDevice(); mDeviceOptions.addSerial(dev.getSerialNumber()); setExpectedInvokeCalls(1); mMockConfiguration.validateOptions(); mMockConfiguration.validateOptions(); replayMocks(); mScheduler.addCommand(args); mScheduler.addCommand(args); mMockManager.freeDevice(dev, FreeDeviceState.AVAILABLE); mScheduler.start(); mScheduler.shutdownOnEmpty(); mScheduler.join(); verifyMocks(); } /** * Test{@link CommandScheduler#run()} when config is matched to a exclude specific device serial * number. * <p/> * Adds two configs to run, and verify they both run on the other device */ public void testRun_configExcludeSerial() throws Throwable { String[] args = new String[] {}; mMockManager.setNumDevices(2); setCreateConfigExpectations(args, 2); // allocate and free a device to get its serial ITestDevice dev = mMockManager.allocateDevice(); mDeviceOptions.addExcludeSerial(dev.getSerialNumber()); ITestDevice expectedDevice = mMockManager.allocateDevice(); setExpectedInvokeCalls(1); mMockConfiguration.validateOptions(); mMockConfiguration.validateOptions(); replayMocks(); mScheduler.addCommand(args); mScheduler.addCommand(args); mMockManager.freeDevice(dev, FreeDeviceState.AVAILABLE); mMockManager.freeDevice(expectedDevice, FreeDeviceState.AVAILABLE); mScheduler.start(); mScheduler.shutdownOnEmpty(); mScheduler.join(); verifyMocks(); } /** * Test {@link CommandScheduler#run()} when one config has been rescheduled */ @SuppressWarnings("unchecked") public void testRun_rescheduled() throws Throwable { String[] args = new String[] {}; mMockManager.setNumDevices(2); setCreateConfigExpectations(args, 1); mMockConfiguration.validateOptions(); final IConfiguration rescheduledConfig = EasyMock.createMock(IConfiguration.class); EasyMock.expect(rescheduledConfig.getCommandOptions()).andStubReturn(mCommandOptions); EasyMock.expect(rescheduledConfig.getDeviceRequirements()).andStubReturn( mDeviceOptions); // an ITestInvocationn#invoke response for calling reschedule IAnswer rescheduleAndThrowAnswer = new IAnswer() { @Override public Object answer() throws Throwable { IRescheduler rescheduler = (IRescheduler) EasyMock.getCurrentArguments()[2]; rescheduler.scheduleConfig(rescheduledConfig); throw new DeviceNotAvailableException("not avail"); } }; mMockInvocation.invoke((ITestDevice)EasyMock.anyObject(), (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject()); EasyMock.expectLastCall().andAnswer(rescheduleAndThrowAnswer); // expect one more success call setExpectedInvokeCalls(1); replayMocks(rescheduledConfig); mScheduler.addCommand(args); mScheduler.start(); mScheduler.shutdownOnEmpty(); mScheduler.join(); EasyMock.verify(mMockConfigFactory, mMockConfiguration, mMockInvocation); } /** * Test {@link CommandScheduler#shutdown()} when no devices are available. */ public void testShutdown() throws Exception { mMockManager.setNumDevices(0); mScheduler.start(); while (!mScheduler.isAlive()) { Thread.sleep(10); } // hack - sleep a bit more to ensure allocateDevices is called Thread.sleep(50); mScheduler.shutdown(); mScheduler.join(); // test will hang if not successful } /** * Set EasyMock expectations for a create configuration call. */ private void setCreateConfigExpectations(String[] args, int times) throws ConfigurationException { EasyMock.expect( mMockConfigFactory.createConfigurationFromArgs(EasyMock.aryEq(args))) .andReturn(mMockConfiguration) .times(times); EasyMock.expect(mMockConfiguration.getCommandOptions()).andStubReturn(mCommandOptions); EasyMock.expect(mMockConfiguration.getDeviceRequirements()).andStubReturn( mDeviceOptions); } }