1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tradefed.command;
18 
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 import static org.mockito.Mockito.mock;
22 import static org.mockito.Mockito.when;
23 
24 import com.android.ddmlib.IDevice;
25 import com.android.tradefed.config.ConfigurationDescriptor;
26 import com.android.tradefed.config.ConfigurationException;
27 import com.android.tradefed.config.DeviceConfigurationHolder;
28 import com.android.tradefed.config.GlobalConfiguration;
29 import com.android.tradefed.config.IConfiguration;
30 import com.android.tradefed.config.IConfigurationFactory;
31 import com.android.tradefed.config.IDeviceConfiguration;
32 import com.android.tradefed.device.DeviceNotAvailableException;
33 import com.android.tradefed.device.DeviceSelectionOptions;
34 import com.android.tradefed.device.IDeviceManager;
35 import com.android.tradefed.device.ITestDevice;
36 import com.android.tradefed.device.MockDeviceManager;
37 import com.android.tradefed.device.StubDevice;
38 import com.android.tradefed.device.TestDeviceOptions;
39 import com.android.tradefed.device.TestDeviceState;
40 import com.android.tradefed.invoker.IInvocationContext;
41 import com.android.tradefed.invoker.IRescheduler;
42 import com.android.tradefed.invoker.ITestInvocation;
43 import com.android.tradefed.log.ILeveledLogOutput;
44 import com.android.tradefed.log.LogUtil.CLog;
45 import com.android.tradefed.result.ILogSaver;
46 import com.android.tradefed.result.ITestInvocationListener;
47 import com.android.tradefed.result.error.ErrorIdentifier;
48 import com.android.tradefed.util.RunInterruptedException;
49 import com.android.tradefed.util.RunUtil;
50 import com.android.tradefed.util.keystore.IKeyStoreClient;
51 
52 import com.google.common.truth.Truth;
53 
54 import org.junit.After;
55 import org.junit.Before;
56 import org.junit.BeforeClass;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.junit.runners.JUnit4;
60 import org.mockito.AdditionalMatchers;
61 import org.mockito.Mock;
62 import org.mockito.Mockito;
63 import org.mockito.MockitoAnnotations;
64 
65 import java.util.ArrayList;
66 import java.util.List;
67 
68 /** Longer running test for {@link CommandScheduler} */
69 @RunWith(JUnit4.class)
70 public class CommandSchedulerFuncTest {
71 
72     private static final long WAIT_TIMEOUT_MS = 30 * 1000;
73     /** the {@link CommandScheduler} under test, with all dependencies mocked out */
74     private CommandScheduler mCommandScheduler;
75 
76     private MeasuredInvocation mMockTestInvoker;
77     private MockDeviceManager mMockDeviceManager;
78     private List<IDeviceConfiguration> mMockDeviceConfig;
79     @Mock IConfiguration mSlowConfig;
80     @Mock IConfiguration mFastConfig;
81     @Mock IConfigurationFactory mMockConfigFactory;
82     @Mock ILeveledLogOutput mMockLogLevel;
83     @Mock ILogSaver mMockLogSaver;
84     private CommandOptions mCommandOptions;
85     private DeviceSelectionOptions mDeviceOptions;
86     private boolean mInterruptible = false;
87     private IDeviceConfiguration mMockConfig;
88 
89     @BeforeClass
setUpClass()90     public static void setUpClass() throws ConfigurationException {
91         try {
92             GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
93         } catch (IllegalStateException e) {
94             // ignore
95         }
96     }
97 
98     @Before
setUp()99     public void setUp() throws Exception {
100         MockitoAnnotations.initMocks(this);
101 
102         mDeviceOptions = new DeviceSelectionOptions();
103         mMockDeviceConfig = new ArrayList<IDeviceConfiguration>();
104         mMockConfig = new DeviceConfigurationHolder("device");
105         mMockConfig.addSpecificConfig(mDeviceOptions);
106         mMockConfig.addSpecificConfig(new TestDeviceOptions());
107         mMockDeviceConfig.add(mMockConfig);
108 
109         mInterruptible = false;
110 
111         mMockDeviceManager = new MockDeviceManager(1);
112         mMockTestInvoker = new MeasuredInvocation();
113 
114         mCommandOptions = new CommandOptions();
115         mCommandOptions.setLoopMode(true);
116         mCommandOptions.setMinLoopTime(0);
117         when(mSlowConfig.getCommandOptions()).thenReturn(mCommandOptions);
118         when(mSlowConfig.getTestInvocationListeners())
119                 .thenReturn(new ArrayList<ITestInvocationListener>());
120         when(mFastConfig.getCommandOptions()).thenReturn(mCommandOptions);
121         when(mFastConfig.getTestInvocationListeners())
122                 .thenReturn(new ArrayList<ITestInvocationListener>());
123         when(mSlowConfig.getDeviceRequirements()).thenReturn(new DeviceSelectionOptions());
124         when(mFastConfig.getDeviceRequirements()).thenReturn(new DeviceSelectionOptions());
125         when(mSlowConfig.getDeviceConfig()).thenReturn(mMockDeviceConfig);
126         when(mSlowConfig.getDeviceConfigByName(Mockito.eq("device"))).thenReturn(mMockConfig);
127         when(mSlowConfig.getCommandLine()).thenReturn("");
128         when(mFastConfig.getDeviceConfigByName(Mockito.eq("device"))).thenReturn(mMockConfig);
129         when(mFastConfig.getDeviceConfig()).thenReturn(mMockDeviceConfig);
130         when(mFastConfig.getCommandLine()).thenReturn("");
131         when(mSlowConfig.getConfigurationDescription()).thenReturn(new ConfigurationDescriptor());
132         when(mFastConfig.getConfigurationDescription()).thenReturn(new ConfigurationDescriptor());
133         when(mFastConfig.getTests()).thenReturn(new ArrayList<>());
134         when(mSlowConfig.getTests()).thenReturn(new ArrayList<>());
135         when(mFastConfig.getLogSaver()).thenReturn(mMockLogSaver);
136         when(mSlowConfig.getLogSaver()).thenReturn(mMockLogSaver);
137         when(mFastConfig.getLogOutput()).thenReturn(mMockLogLevel);
138         when(mSlowConfig.getLogOutput()).thenReturn(mMockLogLevel);
139 
140         mCommandScheduler =
141                 new CommandScheduler() {
142                     @Override
143                     ITestInvocation createRunInstance() {
144                         return mMockTestInvoker;
145                     }
146 
147                     @Override
148                     protected IDeviceManager getDeviceManager() {
149                         return mMockDeviceManager;
150                     }
151 
152                     @Override
153                     protected IConfigurationFactory getConfigFactory() {
154                         if (mInterruptible) {
155                             // simulate the invocation becoming interruptible
156                             RunUtil.getDefault().allowInterrupt(true);
157                         }
158                         return mMockConfigFactory;
159                     }
160 
161                     @Override
162                     protected void initLogging() {
163                         // ignore
164                     }
165 
166                     @Override
167                     protected void cleanUp() {
168                         // ignore
169                     }
170                 };
171     }
172 
173     @After
tearDown()174     public void tearDown() throws Exception {
175         if (mCommandScheduler != null) {
176             mCommandScheduler.shutdownOnEmpty();
177         }
178     }
179 
180     /**
181      * Test config priority scheduling. Verifies that configs are prioritized according to their
182      * total run time.
183      *
184      * <p>This test continually executes two configs in loop mode. One config executes quickly (ie
185      * "fast config"). The other config (ie "slow config") takes ~ 2 * fast config time to execute.
186      *
187      * <p>The run is stopped after the slow config is executed 20 times. At the end of the test, it
188      * is expected that "fast config" has executed roughly twice as much as the "slow config".
189      */
190     @Test
testRun_scheduling()191     public void testRun_scheduling() throws Exception {
192         String[] fastConfigArgs = new String[] {"fastConfig"};
193         String[] slowConfigArgs = new String[] {"slowConfig"};
194         List<String> nullArg = null;
195         when(mMockConfigFactory.createConfigurationFromArgs(
196                         AdditionalMatchers.aryEq(fastConfigArgs),
197                         Mockito.eq(nullArg),
198                         (IKeyStoreClient) Mockito.any()))
199                 .thenReturn(mFastConfig);
200         when(mMockConfigFactory.createConfigurationFromArgs(
201                         AdditionalMatchers.aryEq(slowConfigArgs),
202                         Mockito.eq(nullArg),
203                         (IKeyStoreClient) Mockito.any()))
204                 .thenReturn(mSlowConfig);
205 
206         mCommandScheduler.start();
207         mCommandScheduler.addCommand(fastConfigArgs);
208         mCommandScheduler.addCommand(slowConfigArgs);
209 
210         synchronized (mMockTestInvoker) {
211             mMockTestInvoker.wait(WAIT_TIMEOUT_MS);
212         }
213         mCommandScheduler.shutdown();
214         mCommandScheduler.join(WAIT_TIMEOUT_MS);
215 
216         CLog.i(
217                 "fast times %d slow times %d",
218                 mMockTestInvoker.mFastCount, mMockTestInvoker.mSlowCount);
219         Truth.assertThat(mMockTestInvoker.mFastCount).isGreaterThan(mMockTestInvoker.mSlowCount);
220         assertFalse(mMockTestInvoker.runInterrupted);
221     }
222 
223     private class MeasuredInvocation implements ITestInvocation {
224         private final Object mSlowCountLock = new Object();
225         int mSlowCount = 0;
226         private final Object mFastCountLock = new Object();
227         int mFastCount = 0;
228         int mSlowCountLimit = 40;
229         public boolean runInterrupted = false;
230         public boolean printedStop = false;
231         public long mSleepTimMs = 200L;
232 
233         @Override
invoke( IInvocationContext metadata, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener... listeners)234         public void invoke(
235                 IInvocationContext metadata,
236                 IConfiguration config,
237                 IRescheduler rescheduler,
238                 ITestInvocationListener... listeners)
239                 throws DeviceNotAvailableException {
240             try {
241                 if (mInterruptible) {
242                     // simulate the invocation becoming interruptible
243                     RunUtil.getDefault().allowInterrupt(true);
244                 }
245                 if (config.equals(mSlowConfig)) {
246                     // sleep for 2 * fast config time
247                     RunUtil.getDefault().sleep(mSleepTimMs);
248                     synchronized (mSlowCountLock) {
249                         mSlowCount++;
250                     }
251                     if (mSlowCount >= mSlowCountLimit) {
252                         synchronized (this) {
253                             notify();
254                         }
255                     }
256                 } else if (config.equals(mFastConfig)) {
257                     RunUtil.getDefault().sleep(100);
258                     synchronized (mFastCountLock) {
259                         mFastCount++;
260                     }
261                 } else {
262                     throw new IllegalArgumentException("unknown config");
263                 }
264             } catch (RunInterruptedException e) {
265                 CLog.e(e);
266                 // Yield right away if an exception occur due to an interrupt.
267                 runInterrupted = true;
268                 synchronized (this) {
269                     notify();
270                 }
271             }
272         }
273 
274         @Override
notifyInvocationForceStopped(String message, ErrorIdentifier errorId)275         public void notifyInvocationForceStopped(String message, ErrorIdentifier errorId) {
276             runInterrupted = true;
277             printedStop = true;
278             CLog.d("#notifyInvocationForceStopped");
279         }
280 
281         @Override
notifyInvocationStopped(String message)282         public void notifyInvocationStopped(String message) {
283             CLog.d("#notifyInvocationStopped");
284         }
285     }
286 
287     /** Test that the Invocation is not interruptible even when Battery is low. */
288     @Test
testBatteryLowLevel()289     public void testBatteryLowLevel() throws Throwable {
290         ITestDevice mockDevice = mock(ITestDevice.class);
291         when(mockDevice.getSerialNumber()).thenReturn("serial");
292         IDevice mockIDevice = new StubDevice("serial");
293         when(mockDevice.getIDevice()).thenReturn(mockIDevice);
294         when(mockDevice.getDeviceState()).thenReturn(TestDeviceState.ONLINE);
295 
296         TestDeviceOptions testDeviceOptions = new TestDeviceOptions();
297         testDeviceOptions.setCutoffBattery(20);
298         mMockConfig.addSpecificConfig(testDeviceOptions);
299         assertTrue(testDeviceOptions.getCutoffBattery() == 20);
300         when(mSlowConfig.getDeviceOptions()).thenReturn(testDeviceOptions);
301 
302         mMockDeviceManager.clearAllDevices();
303         mMockDeviceManager.addDevice(mockDevice);
304 
305         String[] slowConfigArgs = new String[] {"slowConfig"};
306         List<String> nullArg = null;
307         when(mMockConfigFactory.createConfigurationFromArgs(
308                         AdditionalMatchers.aryEq(slowConfigArgs),
309                         Mockito.eq(nullArg),
310                         (IKeyStoreClient) Mockito.any()))
311                 .thenReturn(mSlowConfig);
312 
313         mCommandScheduler.start();
314         mCommandScheduler.addCommand(slowConfigArgs);
315 
316         synchronized (mMockTestInvoker) {
317             mMockTestInvoker.wait(WAIT_TIMEOUT_MS);
318         }
319 
320         mCommandScheduler.shutdown();
321         mCommandScheduler.join(WAIT_TIMEOUT_MS);
322         assertFalse(mMockTestInvoker.runInterrupted);
323         // Notify was not sent to the invocation because it was not forced shutdown.
324         assertFalse(mMockTestInvoker.printedStop);
325     }
326 
327     /** Test that the Invocation is interruptible when Battery is low. */
328     @Test
testBatteryLowLevel_interruptible()329     public void testBatteryLowLevel_interruptible() throws Throwable {
330         ITestDevice mockDevice = mock(ITestDevice.class);
331         when(mockDevice.getSerialNumber()).thenReturn("serial");
332         IDevice mockIDevice = new StubDevice("serial");
333         when(mockDevice.getBattery()).thenReturn(10);
334         when(mockDevice.getIDevice()).thenReturn(mockIDevice);
335         when(mockDevice.getDeviceState()).thenReturn(TestDeviceState.ONLINE);
336 
337         TestDeviceOptions testDeviceOptions = new TestDeviceOptions();
338         testDeviceOptions.setCutoffBattery(20);
339         mMockConfig.addSpecificConfig(testDeviceOptions);
340         when(mSlowConfig.getDeviceOptions()).thenReturn(testDeviceOptions);
341 
342         mMockDeviceManager.clearAllDevices();
343         mMockDeviceManager.addDevice(mockDevice);
344 
345         String[] slowConfigArgs = new String[] {"slowConfig"};
346         List<String> nullArg = null;
347         when(mMockConfigFactory.createConfigurationFromArgs(
348                         AdditionalMatchers.aryEq(slowConfigArgs),
349                         Mockito.eq(nullArg),
350                         (IKeyStoreClient) Mockito.any()))
351                 .thenReturn(mSlowConfig);
352 
353         mCommandScheduler.start();
354         mInterruptible = true;
355         mCommandScheduler.addCommand(slowConfigArgs);
356 
357         synchronized (mMockTestInvoker) {
358             mMockTestInvoker.wait(WAIT_TIMEOUT_MS);
359         }
360 
361         mCommandScheduler.shutdown();
362         mCommandScheduler.join(WAIT_TIMEOUT_MS);
363         assertTrue(mMockTestInvoker.runInterrupted);
364     }
365 
366     /**
367      * Test that the Invocation is interrupted by the shutdownHard and finishes with an
368      * interruption. {@link CommandScheduler#shutdownHard()}
369      */
370     @Test
testShutdown_interruptible()371     public void testShutdown_interruptible() throws Throwable {
372         String[] slowConfigArgs = new String[] {"slowConfig"};
373         mMockTestInvoker.mSleepTimMs = 10000L; // Sleep much longer than expected interrupt
374         List<String> nullArg = null;
375         when(mMockConfigFactory.createConfigurationFromArgs(
376                         AdditionalMatchers.aryEq(slowConfigArgs),
377                         Mockito.eq(nullArg),
378                         (IKeyStoreClient) Mockito.any()))
379                 .thenReturn(mSlowConfig);
380 
381         mCommandScheduler.start();
382         mInterruptible = true;
383         mCommandScheduler.addCommand(slowConfigArgs);
384 
385         Thread test =
386                 new Thread(
387                         new Runnable() {
388                             @Override
389                             public void run() {
390                                 RunUtil.getDefault().sleep(500);
391                                 mCommandScheduler.shutdownHard();
392                             }
393                         });
394         test.setName("CommandSchedulerFuncTest#testShutdown_interruptible");
395         test.start();
396         synchronized (mMockTestInvoker) {
397             mMockTestInvoker.wait(WAIT_TIMEOUT_MS);
398         }
399         test.join();
400         mCommandScheduler.join(WAIT_TIMEOUT_MS);
401         // Was interrupted during execution.
402         assertTrue(mMockTestInvoker.runInterrupted);
403         // Notify was sent to the invocation
404         assertTrue(mMockTestInvoker.printedStop);
405     }
406 
407     /**
408      * Test that the Invocation is not interrupted by shutdownHard. Invocation terminate then
409      * scheduler finishes. {@link CommandScheduler#shutdownHard()}
410      */
411     @Test
testShutdown_notInterruptible()412     public void testShutdown_notInterruptible() throws Throwable {
413         final LongInvocation li = new LongInvocation(5);
414         mCommandOptions.setLoopMode(false);
415         mCommandScheduler =
416                 new CommandScheduler() {
417                     @Override
418                     ITestInvocation createRunInstance() {
419                         return li;
420                     }
421 
422                     @Override
423                     protected IDeviceManager getDeviceManager() {
424                         return mMockDeviceManager;
425                     }
426 
427                     @Override
428                     protected IConfigurationFactory getConfigFactory() {
429                         if (mInterruptible) {
430                             // simulate the invocation becoming interruptible
431                             RunUtil.getDefault().allowInterrupt(true);
432                         }
433                         return mMockConfigFactory;
434                     }
435 
436                     @Override
437                     protected void initLogging() {
438                         // ignore
439                     }
440 
441                     @Override
442                     protected void cleanUp() {
443                         // ignore
444                     }
445 
446                     @Override
447                     public long getShutdownTimeout() {
448                         return 30000;
449                     }
450                 };
451         String[] slowConfigArgs = new String[] {"slowConfig"};
452         List<String> nullArg = null;
453         when(mMockConfigFactory.createConfigurationFromArgs(
454                         AdditionalMatchers.aryEq(slowConfigArgs),
455                         Mockito.eq(nullArg),
456                         (IKeyStoreClient) Mockito.any()))
457                 .thenReturn(mSlowConfig);
458 
459         mCommandScheduler.start();
460         mInterruptible = false;
461         mCommandScheduler.addCommand(slowConfigArgs);
462 
463         Thread shutdownThread =
464                 new Thread(
465                         new Runnable() {
466                             @Override
467                             public void run() {
468                                 RunUtil.getDefault().sleep(1000);
469                                 mCommandScheduler.shutdownHard();
470                             }
471                         });
472         shutdownThread.setName("CommandSchedulerFuncTest#testShutdown_notInterruptible");
473         shutdownThread.start();
474         synchronized (li) {
475             // Invocation will finish first because shorter than shutdownHard final timeout
476             li.wait(WAIT_TIMEOUT_MS);
477         }
478         shutdownThread.join();
479         mCommandScheduler.join(WAIT_TIMEOUT_MS);
480         // Stop but was not interrupted
481         assertFalse(mMockTestInvoker.runInterrupted);
482         // Notify was not sent to the invocation because it was not interrupted.
483         assertFalse(mMockTestInvoker.printedStop);
484     }
485 
486     private class LongInvocation implements ITestInvocation {
487         public boolean runInterrupted = false;
488         private int mIteration = 15;
489 
LongInvocation(int iteration)490         public LongInvocation(int iteration) {
491             mIteration = iteration;
492         }
493 
494         @Override
invoke( IInvocationContext metadata, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener... listeners)495         public void invoke(
496                 IInvocationContext metadata,
497                 IConfiguration config,
498                 IRescheduler rescheduler,
499                 ITestInvocationListener... listeners)
500                 throws DeviceNotAvailableException {
501             try {
502                 if (mInterruptible) {
503                     // simulate the invocation becoming interruptible
504                     RunUtil.getDefault().allowInterrupt(true);
505                 }
506                 for (int i = 0; i < mIteration; i++) {
507                     RunUtil.getDefault().sleep(2000);
508                 }
509                 synchronized (this) {
510                     notify();
511                 }
512             } catch (RunInterruptedException e) {
513                 CLog.e(e);
514                 // Yield right away if an exception occur due to an interrupt.
515                 runInterrupted = true;
516                 synchronized (this) {
517                     notify();
518                 }
519             }
520         }
521 
522         @Override
notifyInvocationForceStopped(String message, ErrorIdentifier errorId)523         public void notifyInvocationForceStopped(String message, ErrorIdentifier errorId) {
524             runInterrupted = true;
525             CLog.d("#notifyInvocationForceStopped");
526         }
527 
528         @Override
notifyInvocationStopped(String message)529         public void notifyInvocationStopped(String message) {
530             CLog.d("#notifyInvocationStopped");
531         }
532     }
533 
534     /**
535      * Test that the Invocation is interrupted by {@link CommandScheduler#shutdownHard()} but only
536      * after the shutdown timeout is expired because the invocation was uninterruptible so we only
537      * allow for so much time before shutting down.
538      */
539     @Test
testShutdown_notInterruptible_timeout()540     public void testShutdown_notInterruptible_timeout() throws Throwable {
541         final LongInvocation li = new LongInvocation(15);
542         mCommandOptions.setLoopMode(false);
543         mCommandScheduler =
544                 new CommandScheduler() {
545                     @Override
546                     ITestInvocation createRunInstance() {
547                         return li;
548                     }
549 
550                     @Override
551                     protected IDeviceManager getDeviceManager() {
552                         return mMockDeviceManager;
553                     }
554 
555                     @Override
556                     protected IConfigurationFactory getConfigFactory() {
557                         if (mInterruptible) {
558                             // simulate the invocation becoming interruptible
559                             RunUtil.getDefault().allowInterrupt(true);
560                         }
561                         return mMockConfigFactory;
562                     }
563 
564                     @Override
565                     protected void initLogging() {
566                         // ignore
567                     }
568 
569                     @Override
570                     protected void cleanUp() {
571                         // ignore
572                     }
573 
574                     @Override
575                     public long getShutdownTimeout() {
576                         return 5000;
577                     }
578                 };
579         String[] slowConfigArgs = new String[] {"slowConfig"};
580         List<String> nullArg = null;
581         when(mMockConfigFactory.createConfigurationFromArgs(
582                         AdditionalMatchers.aryEq(slowConfigArgs),
583                         Mockito.eq(nullArg),
584                         (IKeyStoreClient) Mockito.any()))
585                 .thenReturn(mSlowConfig);
586 
587         mCommandScheduler.start();
588         mInterruptible = false;
589         mCommandScheduler.addCommand(slowConfigArgs);
590 
591         Thread shutdownThread =
592                 new Thread(
593                         new Runnable() {
594                             @Override
595                             public void run() {
596                                 RunUtil.getDefault().sleep(1000);
597                                 mCommandScheduler.shutdownHard();
598                             }
599                         });
600         shutdownThread.setName("CommandSchedulerFuncTest#testShutdown_notInterruptible_timeout");
601         shutdownThread.start();
602         synchronized (li) {
603             // Setting a timeout longer than the shutdown timeout.
604             li.wait(WAIT_TIMEOUT_MS);
605         }
606         shutdownThread.join();
607         mCommandScheduler.join(WAIT_TIMEOUT_MS);
608         // Stop and was interrupted by timeout of shutdownHard()
609         assertTrue(li.runInterrupted);
610     }
611 
612     /** Test that if the invocation run time goes over the timeout, it will be forced stopped. */
613     @Test
testShutdown_invocation_timeout()614     public void testShutdown_invocation_timeout() throws Throwable {
615         final LongInvocation li = new LongInvocation(2);
616         mCommandOptions.setLoopMode(false);
617         mCommandOptions.setInvocationTimeout(1000L);
618         mCommandScheduler =
619                 new CommandScheduler() {
620                     @Override
621                     ITestInvocation createRunInstance() {
622                         return li;
623                     }
624 
625                     @Override
626                     protected IDeviceManager getDeviceManager() {
627                         return mMockDeviceManager;
628                     }
629 
630                     @Override
631                     protected IConfigurationFactory getConfigFactory() {
632                         return mMockConfigFactory;
633                     }
634 
635                     @Override
636                     protected void initLogging() {
637                         // ignore
638                     }
639 
640                     @Override
641                     protected void cleanUp() {
642                         // ignore
643                     }
644                 };
645         String[] slowConfigArgs = new String[] {"slowConfig"};
646         List<String> nullArg = null;
647         when(mMockConfigFactory.createConfigurationFromArgs(
648                         AdditionalMatchers.aryEq(slowConfigArgs),
649                         Mockito.eq(nullArg),
650                         (IKeyStoreClient) Mockito.any()))
651                 .thenReturn(mSlowConfig);
652 
653         mCommandScheduler.start();
654         mInterruptible = true;
655         mCommandScheduler.addCommand(slowConfigArgs);
656         mCommandScheduler.join(mCommandOptions.getInvocationTimeout() * 3);
657         // Stop and was interrupted by timeout
658         assertTrue(li.runInterrupted);
659     }
660 }
661