Skip to content

Commit d7990cc

Browse files
committed
Use same screen recorder across different tests executed on the same device
which should help with screen recordings sometimes containing multiple tests.
1 parent e5ddc2c commit d7990cc

12 files changed

+443
-126
lines changed

fork-runner/src/main/java/com/shazam/fork/runner/DeviceTestRunner.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.android.ddmlib.*;
1616
import com.shazam.fork.model.Device;
1717
import com.shazam.fork.model.*;
18+
import com.shazam.fork.runner.listeners.ScreenRecorder;
1819
import com.shazam.fork.system.adb.Installer;
1920

2021
import org.slf4j.Logger;
@@ -35,6 +36,7 @@ public class DeviceTestRunner implements Runnable {
3536
private final Queue<TestCaseEvent> queueOfTestsInPool;
3637
private final CountDownLatch deviceCountDownLatch;
3738
private final ProgressReporter progressReporter;
39+
private final ScreenRecorder screenRecorder;
3840
private final TestRunFactory testRunFactory;
3941

4042
public DeviceTestRunner(Installer installer,
@@ -43,13 +45,15 @@ public DeviceTestRunner(Installer installer,
4345
Queue<TestCaseEvent> queueOfTestsInPool,
4446
CountDownLatch deviceCountDownLatch,
4547
ProgressReporter progressReporter,
48+
ScreenRecorder screenRecorder,
4649
TestRunFactory testRunFactory) {
4750
this.installer = installer;
4851
this.pool = pool;
4952
this.device = device;
5053
this.queueOfTestsInPool = queueOfTestsInPool;
5154
this.deviceCountDownLatch = deviceCountDownLatch;
5255
this.progressReporter = progressReporter;
56+
this.screenRecorder = screenRecorder;
5357
this.testRunFactory = testRunFactory;
5458
}
5559

@@ -71,6 +75,7 @@ public void run() {
7175
device,
7276
pool,
7377
progressReporter,
78+
screenRecorder,
7479
queueOfTestsInPool);
7580
testRun.execute();
7681
}

fork-runner/src/main/java/com/shazam/fork/runner/DeviceTestRunnerFactory.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
package com.shazam.fork.runner;
1212

13-
import com.shazam.fork.model.*;
13+
import com.shazam.fork.model.Device;
14+
import com.shazam.fork.model.Pool;
15+
import com.shazam.fork.model.TestCaseEvent;
16+
import com.shazam.fork.runner.listeners.ScreenRecorderImpl;
1417
import com.shazam.fork.system.adb.Installer;
1518

1619
import java.util.Queue;
@@ -31,14 +34,15 @@ public Runnable createDeviceTestRunner(Pool pool,
3134
CountDownLatch deviceInPoolCountDownLatch,
3235
Device device,
3336
ProgressReporter progressReporter
34-
) {
37+
) {
3538
return new DeviceTestRunner(
3639
installer,
3740
pool,
3841
device,
3942
testClassQueue,
4043
deviceInPoolCountDownLatch,
4144
progressReporter,
45+
new ScreenRecorderImpl(device.getDeviceInterface()),
4246
testRunFactory);
4347
}
4448
}

fork-runner/src/main/java/com/shazam/fork/runner/TestRunFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
import com.shazam.fork.model.Device;
1616
import com.shazam.fork.model.Pool;
1717
import com.shazam.fork.model.TestCaseEvent;
18+
import com.shazam.fork.runner.listeners.ScreenRecorder;
1819
import com.shazam.fork.runner.listeners.TestRunListenersFactory;
1920

21+
import javax.annotation.Nonnull;
2022
import java.util.List;
2123
import java.util.Queue;
2224

23-
import javax.annotation.Nonnull;
24-
2525
import static com.shazam.fork.runner.TestRunParameters.Builder.testRunParameters;
2626
import static com.shazam.fork.system.PermissionGrantingManager.permissionGrantingManager;
2727

@@ -39,6 +39,7 @@ public TestRun createTestRun(@Nonnull TestCaseEvent testCase,
3939
Device device,
4040
Pool pool,
4141
ProgressReporter progressReporter,
42+
ScreenRecorder screenRecorder,
4243
Queue<TestCaseEvent> queueOfTestsInPool) {
4344
TestRunParameters testRunParameters = testRunParameters()
4445
.withDeviceInterface(device.getDeviceInterface())
@@ -57,6 +58,7 @@ public TestRun createTestRun(@Nonnull TestCaseEvent testCase,
5758
device,
5859
pool,
5960
progressReporter,
61+
screenRecorder,
6062
queueOfTestsInPool);
6163

6264
return new TestRun(
Lines changed: 12 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,29 @@
11
/*
2-
* Copyright 2019 Apple Inc.
2+
* Copyright 2022 Apple Inc.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License.
56
*
67
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
78
*
8-
* 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.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
913
*/
1014

1115
package com.shazam.fork.runner.listeners;
1216

13-
import com.android.ddmlib.*;
1417
import com.android.ddmlib.testrunner.TestIdentifier;
1518

16-
import org.slf4j.Logger;
17-
import org.slf4j.LoggerFactory;
18-
1919
import java.io.File;
20-
import java.io.IOException;
21-
22-
import static com.shazam.fork.utils.Utils.millisSinceNanoTime;
23-
import static com.shazam.fork.system.io.RemoteFileManager.remoteVideoForTest;
24-
import static com.shazam.fork.system.io.RemoteFileManager.removeRemotePath;
25-
import static java.lang.System.nanoTime;
26-
import static java.util.concurrent.TimeUnit.SECONDS;
27-
28-
class ScreenRecorder implements Runnable {
29-
private static final Logger logger = LoggerFactory.getLogger(ScreenRecorder.class);
30-
private static final int DURATION = 60;
31-
private static final int BIT_RATE_MBPS = 1;
32-
private static final ScreenRecorderOptions RECORDER_OPTIONS = new ScreenRecorderOptions.Builder()
33-
.setTimeLimit(DURATION, SECONDS)
34-
.setBitRate(BIT_RATE_MBPS)
35-
.build();
36-
37-
private final String remoteFilePath;
38-
private final File localVideoFile;
39-
private final IDevice deviceInterface;
40-
private final ScreenRecorderStopper screenRecorderStopper;
41-
42-
public ScreenRecorder(TestIdentifier test, ScreenRecorderStopper screenRecorderStopper, File localVideoFile,
43-
IDevice deviceInterface) {
44-
remoteFilePath = remoteVideoForTest(test);
45-
this.screenRecorderStopper = screenRecorderStopper;
46-
this.localVideoFile = localVideoFile;
47-
this.deviceInterface = deviceInterface;
48-
}
4920

50-
@Override
51-
public void run() {
52-
try {
53-
startRecordingTestVideo();
54-
if (screenRecorderStopper.hasFailed()) {
55-
pullTestVideo();
56-
}
57-
removeTestVideo();
58-
} catch (Exception e) {
59-
logger.error("Something went wrong while screen recording", e);
60-
}
61-
}
21+
public interface ScreenRecorder {
22+
void startScreenRecording(TestIdentifier test);
6223

63-
private void startRecordingTestVideo() throws TimeoutException, AdbCommandRejectedException, IOException,
64-
ShellCommandUnresponsiveException {
65-
NullOutputReceiver outputReceiver = new NullOutputReceiver();
66-
logger.trace("Started recording video at: {}", remoteFilePath);
67-
long startNanos = nanoTime();
68-
deviceInterface.startScreenRecorder(remoteFilePath, RECORDER_OPTIONS, outputReceiver);
69-
logger.trace("Recording finished in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
70-
}
24+
void stopScreenRecording(TestIdentifier test);
7125

72-
private void pullTestVideo() throws IOException, AdbCommandRejectedException, TimeoutException, SyncException {
73-
logger.trace("Started pulling file {} to {}", remoteFilePath, localVideoFile);
74-
long startNanos = nanoTime();
75-
deviceInterface.pullFile(remoteFilePath, localVideoFile.toString());
76-
logger.trace("Pulling finished in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
77-
}
26+
void saveScreenRecording(TestIdentifier test, File output);
7827

79-
private void removeTestVideo() {
80-
logger.trace("Started removing file {}", remoteFilePath);
81-
long startNanos = nanoTime();
82-
removeRemotePath(deviceInterface, remoteFilePath);
83-
logger.trace("Removed file in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
84-
}
28+
void removeScreenRecording(TestIdentifier test);
8529
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2022 Apple Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License.
6+
*
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.shazam.fork.runner.listeners;
16+
17+
import com.android.ddmlib.AdbCommandRejectedException;
18+
import com.android.ddmlib.IDevice;
19+
import com.android.ddmlib.NullOutputReceiver;
20+
import com.android.ddmlib.ScreenRecorderOptions;
21+
import com.android.ddmlib.ShellCommandUnresponsiveException;
22+
import com.android.ddmlib.SyncException;
23+
import com.android.ddmlib.TimeoutException;
24+
import com.android.ddmlib.testrunner.TestIdentifier;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import java.io.File;
29+
import java.io.IOException;
30+
import java.util.concurrent.ExecutorService;
31+
import java.util.concurrent.Future;
32+
33+
import static com.shazam.fork.Utils.namedExecutor;
34+
import static com.shazam.fork.system.io.RemoteFileManager.remoteVideoForTest;
35+
import static com.shazam.fork.system.io.RemoteFileManager.removeRemotePath;
36+
import static com.shazam.fork.utils.Utils.millisSinceNanoTime;
37+
import static java.lang.System.nanoTime;
38+
import static java.util.concurrent.TimeUnit.SECONDS;
39+
40+
public class ScreenRecorderImpl implements ScreenRecorder {
41+
private static final Logger logger = LoggerFactory.getLogger(ScreenRecorderImpl.class);
42+
private static final int DURATION = 60;
43+
private static final int BIT_RATE_MBPS = 1;
44+
private static final ScreenRecorderOptions RECORDER_OPTIONS = new ScreenRecorderOptions.Builder()
45+
.setTimeLimit(DURATION, SECONDS)
46+
.setBitRate(BIT_RATE_MBPS)
47+
.build();
48+
49+
private final IDevice deviceInterface;
50+
private final ScreenRecorderStopper screenRecorderStopper;
51+
private final ExecutorService recorderExecutor;
52+
private final ExecutorService fileExecutor;
53+
54+
private State state = State.Stopped;
55+
private Future<?> recorderTask;
56+
57+
public ScreenRecorderImpl(IDevice deviceInterface) {
58+
this.deviceInterface = deviceInterface;
59+
this.screenRecorderStopper = new ScreenRecorderStopper(deviceInterface);
60+
this.recorderExecutor = namedExecutor(/* numberOfThreads = */ 1, "RecorderExecutor-%d");
61+
this.fileExecutor = namedExecutor(/* numberOfThreads = */ 1, "RecorderFileExecutor-%d");
62+
}
63+
64+
@Override
65+
public void startScreenRecording(TestIdentifier test) {
66+
if (state != State.Stopped) {
67+
logger.warn("ScreenRecorder is {} -> stopping screen recording", state);
68+
stopScreenRecordingInternal();
69+
}
70+
71+
state = State.Recording;
72+
recorderTask = recorderExecutor.submit(() -> {
73+
try {
74+
String remoteFilePath = remoteVideoForTest(test);
75+
logger.debug("Started recording video {}", remoteFilePath);
76+
startRecordingTestVideo(remoteFilePath);
77+
} catch (TimeoutException e) {
78+
logger.debug("Screen recording was either interrupted or timed out", e);
79+
} catch (Exception e) {
80+
logger.error("Something went wrong while screen recording", e);
81+
}
82+
});
83+
}
84+
85+
@Override
86+
public void stopScreenRecording(TestIdentifier test) {
87+
if (state != State.Recording) {
88+
logger.warn("ScreenRecorder is {} when we tried to stop recording", state);
89+
}
90+
stopScreenRecordingInternal();
91+
}
92+
93+
private void stopScreenRecordingInternal() {
94+
logger.debug("Stopped screen recording");
95+
if (recorderTask != null) {
96+
recorderTask.cancel(true);
97+
}
98+
screenRecorderStopper.stopScreenRecord();
99+
state = State.Stopped;
100+
}
101+
102+
@Override
103+
public void saveScreenRecording(TestIdentifier test, File output) {
104+
fileExecutor.submit(() -> {
105+
try {
106+
String remoteFilePath = remoteVideoForTest(test);
107+
logger.debug("Save screen recording {} to {}", remoteFilePath, output);
108+
pullTestVideo(remoteFilePath, output);
109+
} catch (Exception e) {
110+
logger.error("Failed to pull a video file", e);
111+
}
112+
});
113+
}
114+
115+
@Override
116+
public void removeScreenRecording(TestIdentifier test) {
117+
fileExecutor.submit(() -> {
118+
try {
119+
String remoteFilePath = remoteVideoForTest(test);
120+
logger.debug("Remove screen recording {}", remoteFilePath);
121+
removeTestVideo(remoteFilePath);
122+
} catch (Exception e) {
123+
logger.error("Failed to remove a video file", e);
124+
}
125+
});
126+
}
127+
128+
private void startRecordingTestVideo(String remoteFilePath) throws TimeoutException,
129+
AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
130+
NullOutputReceiver outputReceiver = new NullOutputReceiver();
131+
logger.trace("Started recording video at: {}", remoteFilePath);
132+
long startNanos = nanoTime();
133+
deviceInterface.startScreenRecorder(remoteFilePath, RECORDER_OPTIONS, outputReceiver);
134+
logger.trace("Recording finished in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
135+
}
136+
137+
private void pullTestVideo(String remoteFilePath, File output) throws IOException,
138+
AdbCommandRejectedException, TimeoutException, SyncException {
139+
logger.trace("Started pulling file {} to {}", remoteFilePath, output);
140+
long startNanos = nanoTime();
141+
deviceInterface.pullFile(remoteFilePath, output.toString());
142+
logger.trace("Pulling finished in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
143+
}
144+
145+
private void removeTestVideo(String remoteFilePath) {
146+
logger.trace("Started removing file {}", remoteFilePath);
147+
long startNanos = nanoTime();
148+
removeRemotePath(deviceInterface, remoteFilePath);
149+
logger.trace("Removed file in {}ms {}", millisSinceNanoTime(startNanos), remoteFilePath);
150+
}
151+
152+
private enum State {
153+
Recording,
154+
Stopped
155+
}
156+
}

fork-runner/src/main/java/com/shazam/fork/runner/listeners/ScreenRecorderStopper.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import com.android.ddmlib.IDevice;
1414
import com.android.ddmlib.NullOutputReceiver;
1515
import com.shazam.fork.system.adb.CollectingShellOutputReceiver;
16-
1716
import org.slf4j.Logger;
1817
import org.slf4j.LoggerFactory;
1918

@@ -28,7 +27,6 @@ class ScreenRecorderStopper {
2827
private static final int PAUSE_BETWEEN_RECORDER_PROCESS_KILL = 300;
2928
private final NullOutputReceiver nullOutputReceiver = new NullOutputReceiver();
3029
private final IDevice deviceInterface;
31-
private boolean hasFailed;
3230

3331
ScreenRecorderStopper(IDevice deviceInterface) {
3432
this.deviceInterface = deviceInterface;
@@ -37,8 +35,7 @@ class ScreenRecorderStopper {
3735
/**
3836
* Stops all running screenrecord processes.
3937
*/
40-
public void stopScreenRecord(boolean hasFailed) {
41-
this.hasFailed = hasFailed;
38+
void stopScreenRecord() {
4239
boolean hasKilledScreenRecord = true;
4340
int tries = 0;
4441
while (hasKilledScreenRecord && tries++ < SCREENRECORD_KILL_ATTEMPTS) {
@@ -47,10 +44,6 @@ public void stopScreenRecord(boolean hasFailed) {
4744
}
4845
}
4946

50-
public boolean hasFailed() {
51-
return hasFailed;
52-
}
53-
5447
private boolean attemptToGracefullyKillScreenRecord() {
5548
CollectingShellOutputReceiver receiver = new CollectingShellOutputReceiver();
5649
try {

0 commit comments

Comments
 (0)