blob: e64481ea6e241430e3cb4a0a21228b47db9cdc59 [file] [log] [blame]
Matthew Williams01ac45b2014-07-22 20:44:12 -07001package com.android.server.job;
Matthew Williams3d86fd22014-05-16 18:02:17 -07002
3
4import android.content.ComponentName;
5import android.content.Context;
Christopher Tate7060b042014-06-09 19:50:00 -07006import android.app.job.JobInfo;
7import android.app.job.JobInfo.Builder;
Matthew Williams3d86fd22014-05-16 18:02:17 -07008import android.os.PersistableBundle;
Matthew Williamsfa8e5082015-10-15 15:59:12 -07009import android.os.SystemClock;
Matthew Williams3d86fd22014-05-16 18:02:17 -070010import android.test.AndroidTestCase;
11import android.test.RenamingDelegatingContext;
12import android.util.Log;
Matthew Williams01ac45b2014-07-22 20:44:12 -070013import android.util.ArraySet;
Matthew Williams3d86fd22014-05-16 18:02:17 -070014
Christopher Tate2f36fd62016-02-18 18:36:08 -080015import com.android.server.job.JobStore.JobSet;
Christopher Tate7060b042014-06-09 19:50:00 -070016import com.android.server.job.controllers.JobStatus;
Matthew Williams3d86fd22014-05-16 18:02:17 -070017
Matthew Williams01ac45b2014-07-22 20:44:12 -070018import java.util.Iterator;
Matthew Williams3d86fd22014-05-16 18:02:17 -070019
Matthew Williams3d86fd22014-05-16 18:02:17 -070020/**
21 * Test reading and writing correctly from file.
22 */
Matthew Williams01ac45b2014-07-22 20:44:12 -070023public class JobStoreTest extends AndroidTestCase {
Matthew Williams3d86fd22014-05-16 18:02:17 -070024 private static final String TAG = "TaskStoreTest";
25 private static final String TEST_PREFIX = "_test_";
Matthew Williams01ac45b2014-07-22 20:44:12 -070026
Matthew Williams3d86fd22014-05-16 18:02:17 -070027 private static final int SOME_UID = 34234;
28 private ComponentName mComponent;
Matthew Williams01ac45b2014-07-22 20:44:12 -070029 private static final long IO_WAIT = 1000L;
Matthew Williams3d86fd22014-05-16 18:02:17 -070030
Christopher Tate7060b042014-06-09 19:50:00 -070031 JobStore mTaskStoreUnderTest;
Matthew Williams3d86fd22014-05-16 18:02:17 -070032 Context mTestContext;
Matthew Williams3d86fd22014-05-16 18:02:17 -070033
34 @Override
35 public void setUp() throws Exception {
36 mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
37 Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
Matthew Williams01ac45b2014-07-22 20:44:12 -070038 mTaskStoreUnderTest =
39 JobStore.initAndGetForTesting(mTestContext, mTestContext.getFilesDir());
Matthew Williams3d86fd22014-05-16 18:02:17 -070040 mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
41 }
42
43 @Override
44 public void tearDown() throws Exception {
45 mTaskStoreUnderTest.clear();
46 }
47
48 public void testMaybeWriteStatusToDisk() throws Exception {
49 int taskId = 5;
50 long runByMillis = 20000L; // 20s
51 long runFromMillis = 2000L; // 2s
52 long initialBackoff = 10000L; // 10s
53
Christopher Tate7060b042014-06-09 19:50:00 -070054 final JobInfo task = new Builder(taskId, mComponent)
Matthew Williams3d86fd22014-05-16 18:02:17 -070055 .setRequiresCharging(true)
Matthew Williamsd1c06752014-08-22 14:15:28 -070056 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
57 .setBackoffCriteria(initialBackoff, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
Matthew Williams3d86fd22014-05-16 18:02:17 -070058 .setOverrideDeadline(runByMillis)
59 .setMinimumLatency(runFromMillis)
Matthew Williamsd1c06752014-08-22 14:15:28 -070060 .setPersisted(true)
Matthew Williams3d86fd22014-05-16 18:02:17 -070061 .build();
Dianne Hackbornb0001f62016-02-16 10:30:33 -080062 final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1);
Matthew Williams3d86fd22014-05-16 18:02:17 -070063 mTaskStoreUnderTest.add(ts);
64 Thread.sleep(IO_WAIT);
65 // Manually load tasks from xml file.
Christopher Tate2f36fd62016-02-18 18:36:08 -080066 final JobSet jobStatusSet = new JobSet();
Matthew Williams01ac45b2014-07-22 20:44:12 -070067 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
68
69 assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -080070 final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0);
Matthew Williams01ac45b2014-07-22 20:44:12 -070071 assertTasksEqual(task, loadedTaskStatus.getJob());
Matthew Williams48a30db2014-09-23 13:39:36 -070072 assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts));
Matthew Williams01ac45b2014-07-22 20:44:12 -070073 assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid());
74 compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
75 ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
76 compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
77 ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
Matthew Williams3d86fd22014-05-16 18:02:17 -070078
79 }
80
81 public void testWritingTwoFilesToDisk() throws Exception {
Christopher Tate7060b042014-06-09 19:50:00 -070082 final JobInfo task1 = new Builder(8, mComponent)
Matthew Williams3d86fd22014-05-16 18:02:17 -070083 .setRequiresDeviceIdle(true)
84 .setPeriodic(10000L)
85 .setRequiresCharging(true)
Matthew Williamsd1c06752014-08-22 14:15:28 -070086 .setPersisted(true)
Matthew Williams3d86fd22014-05-16 18:02:17 -070087 .build();
Christopher Tate7060b042014-06-09 19:50:00 -070088 final JobInfo task2 = new Builder(12, mComponent)
Matthew Williams3d86fd22014-05-16 18:02:17 -070089 .setMinimumLatency(5000L)
Matthew Williamsd1c06752014-08-22 14:15:28 -070090 .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
Matthew Williams3d86fd22014-05-16 18:02:17 -070091 .setOverrideDeadline(30000L)
Matthew Williamsd1c06752014-08-22 14:15:28 -070092 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
93 .setPersisted(true)
Matthew Williams3d86fd22014-05-16 18:02:17 -070094 .build();
Dianne Hackbornb0001f62016-02-16 10:30:33 -080095 final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, SOME_UID, null, -1);
96 final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, SOME_UID, null, -1);
Matthew Williams3d86fd22014-05-16 18:02:17 -070097 mTaskStoreUnderTest.add(taskStatus1);
98 mTaskStoreUnderTest.add(taskStatus2);
99 Thread.sleep(IO_WAIT);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700100
Christopher Tate2f36fd62016-02-18 18:36:08 -0800101 final JobSet jobStatusSet = new JobSet();
Matthew Williams01ac45b2014-07-22 20:44:12 -0700102 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
103 assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800104 Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator();
Matthew Williams01ac45b2014-07-22 20:44:12 -0700105 JobStatus loaded1 = it.next();
106 JobStatus loaded2 = it.next();
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700107
108 // Reverse them so we know which comparison to make.
109 if (loaded1.getJobId() != 8) {
110 JobStatus tmp = loaded1;
111 loaded1 = loaded2;
112 loaded2 = tmp;
113 }
114
Matthew Williams01ac45b2014-07-22 20:44:12 -0700115 assertTasksEqual(task1, loaded1.getJob());
116 assertTasksEqual(task2, loaded2.getJob());
Matthew Williams48a30db2014-09-23 13:39:36 -0700117 assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1));
118 assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2));
Matthew Williams01ac45b2014-07-22 20:44:12 -0700119 // Check that the loaded task has the correct runtimes.
120 compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
121 taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
122 compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
123 taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
124 compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
125 taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
126 compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
127 taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
Matthew Williams3d86fd22014-05-16 18:02:17 -0700128
129 }
130
131 public void testWritingTaskWithExtras() throws Exception {
Christopher Tate7060b042014-06-09 19:50:00 -0700132 JobInfo.Builder b = new Builder(8, mComponent)
Matthew Williams3d86fd22014-05-16 18:02:17 -0700133 .setRequiresDeviceIdle(true)
134 .setPeriodic(10000L)
Matthew Williams900c67f2014-07-09 12:46:53 -0700135 .setRequiresCharging(true)
Matthew Williamsd1c06752014-08-22 14:15:28 -0700136 .setPersisted(true);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700137
138 PersistableBundle extras = new PersistableBundle();
139 extras.putDouble("hello", 3.2);
140 extras.putString("hi", "there");
141 extras.putInt("into", 3);
142 b.setExtras(extras);
Christopher Tate7060b042014-06-09 19:50:00 -0700143 final JobInfo task = b.build();
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800144 JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700145
146 mTaskStoreUnderTest.add(taskStatus);
147 Thread.sleep(IO_WAIT);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700148
Christopher Tate2f36fd62016-02-18 18:36:08 -0800149 final JobSet jobStatusSet = new JobSet();
Matthew Williams01ac45b2014-07-22 20:44:12 -0700150 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
151 assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800152 JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
Matthew Williams01ac45b2014-07-22 20:44:12 -0700153 assertTasksEqual(task, loaded.getJob());
Matthew Williams3d86fd22014-05-16 18:02:17 -0700154 }
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000155 public void testWritingTaskWithSourcePackage() throws Exception {
156 JobInfo.Builder b = new Builder(8, mComponent)
157 .setRequiresDeviceIdle(true)
158 .setPeriodic(10000L)
159 .setRequiresCharging(true)
160 .setPersisted(true);
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800161 JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID,
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800162 "com.google.android.gms", 0);
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000163
164 mTaskStoreUnderTest.add(taskStatus);
165 Thread.sleep(IO_WAIT);
166
Christopher Tate2f36fd62016-02-18 18:36:08 -0800167 final JobSet jobStatusSet = new JobSet();
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000168 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
169 assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800170 JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000171 assertEquals("Source package not equal.", loaded.getSourcePackageName(),
172 taskStatus.getSourcePackageName());
173 assertEquals("Source user not equal.", loaded.getSourceUserId(),
174 taskStatus.getSourceUserId());
175 }
176
177 public void testWritingTaskWithFlex() throws Exception {
178 JobInfo.Builder b = new Builder(8, mComponent)
179 .setRequiresDeviceIdle(true)
180 .setPeriodic(5*60*60*1000, 1*60*60*1000)
181 .setRequiresCharging(true)
182 .setPersisted(true);
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800183 JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000184
185 mTaskStoreUnderTest.add(taskStatus);
186 Thread.sleep(IO_WAIT);
187
Christopher Tate2f36fd62016-02-18 18:36:08 -0800188 final JobSet jobStatusSet = new JobSet();
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000189 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
190 assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800191 JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000192 assertEquals("Period not equal.", loaded.getJob().getIntervalMillis(),
193 taskStatus.getJob().getIntervalMillis());
194 assertEquals("Flex not equal.", loaded.getJob().getFlexMillis(),
195 taskStatus.getJob().getFlexMillis());
196 }
Matthew Williams3d86fd22014-05-16 18:02:17 -0700197
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700198 public void testMassivePeriodClampedOnRead() throws Exception {
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000199 final long ONE_HOUR = 60*60*1000L; // flex
200 final long TWO_HOURS = 2 * ONE_HOUR; // period
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700201 JobInfo.Builder b = new Builder(8, mComponent)
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000202 .setPeriodic(TWO_HOURS, ONE_HOUR)
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700203 .setPersisted(true);
204 final long invalidLateRuntimeElapsedMillis =
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000205 SystemClock.elapsedRealtime() + (TWO_HOURS * ONE_HOUR) + TWO_HOURS; // > period+flex
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700206 final long invalidEarlyRuntimeElapsedMillis =
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000207 invalidLateRuntimeElapsedMillis - TWO_HOURS; // Early is (late - period).
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800208 final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800209 0 /* sourceUserId */,
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700210 invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis);
211
212 mTaskStoreUnderTest.add(js);
213 Thread.sleep(IO_WAIT);
214
Christopher Tate2f36fd62016-02-18 18:36:08 -0800215 final JobSet jobStatusSet = new JobSet();
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700216 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
217 assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800218 JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700219
220 // Assert early runtime was clamped to be under now + period. We can do <= here b/c we'll
221 // call SystemClock.elapsedRealtime after doing the disk i/o.
222 final long newNowElapsed = SystemClock.elapsedRealtime();
223 assertTrue("Early runtime wasn't correctly clamped.",
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000224 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS);
225 // Assert late runtime was clamped to be now + period + flex.
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700226 assertTrue("Early runtime wasn't correctly clamped.",
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000227 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS + ONE_HOUR);
Shreyas Basarge5db09082016-01-07 13:38:29 +0000228 }
229
230 public void testPriorityPersisted() throws Exception {
231 JobInfo.Builder b = new Builder(92, mComponent)
232 .setOverrideDeadline(5000)
233 .setPriority(42)
234 .setPersisted(true);
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800235 final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
Shreyas Basarge5db09082016-01-07 13:38:29 +0000236 mTaskStoreUnderTest.add(js);
237 Thread.sleep(IO_WAIT);
Christopher Tate2f36fd62016-02-18 18:36:08 -0800238 final JobSet jobStatusSet = new JobSet();
Shreyas Basarge5db09082016-01-07 13:38:29 +0000239 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
Christopher Tate2f36fd62016-02-18 18:36:08 -0800240 JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
Shreyas Basarge5db09082016-01-07 13:38:29 +0000241 assertEquals("Priority not correctly persisted.", 42, loaded.getPriority());
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700242 }
243
Matthew Williams3d86fd22014-05-16 18:02:17 -0700244 /**
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000245 * Test that non persisted job is not written to disk.
246 */
247 public void testNonPersistedTaskIsNotPersisted() throws Exception {
248 JobInfo.Builder b = new Builder(42, mComponent)
249 .setOverrideDeadline(10000)
250 .setPersisted(false);
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800251 JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000252 mTaskStoreUnderTest.add(jsNonPersisted);
253 b = new Builder(43, mComponent)
254 .setOverrideDeadline(10000)
255 .setPersisted(true);
Dianne Hackbornb0001f62016-02-16 10:30:33 -0800256 JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1);
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000257 mTaskStoreUnderTest.add(jsPersisted);
258 Thread.sleep(IO_WAIT);
Christopher Tate2f36fd62016-02-18 18:36:08 -0800259 final JobSet jobStatusSet = new JobSet();
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000260 mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
261 assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
Christopher Tate2f36fd62016-02-18 18:36:08 -0800262 JobStatus jobStatus = jobStatusSet.getAllJobs().iterator().next();
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000263 assertEquals("Wrong job persisted.", 43, jobStatus.getJobId());
264 }
265
266 /**
Matthew Williams3d86fd22014-05-16 18:02:17 -0700267 * Helper function to throw an error if the provided task and TaskStatus objects are not equal.
268 */
Christopher Tate7060b042014-06-09 19:50:00 -0700269 private void assertTasksEqual(JobInfo first, JobInfo second) {
Matthew Williams3d86fd22014-05-16 18:02:17 -0700270 assertEquals("Different task ids.", first.getId(), second.getId());
271 assertEquals("Different components.", first.getService(), second.getService());
272 assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic());
273 assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis());
274 assertEquals("Different inital backoff.", first.getInitialBackoffMillis(),
275 second.getInitialBackoffMillis());
276 assertEquals("Different backoff policy.", first.getBackoffPolicy(),
277 second.getBackoffPolicy());
278
279 assertEquals("Invalid charging constraint.", first.isRequireCharging(),
280 second.isRequireCharging());
281 assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
282 second.isRequireDeviceIdle());
283 assertEquals("Invalid unmetered constraint.",
Matthew Williamsd1c06752014-08-22 14:15:28 -0700284 first.getNetworkType() == JobInfo.NETWORK_TYPE_UNMETERED,
285 second.getNetworkType() == JobInfo.NETWORK_TYPE_UNMETERED);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700286 assertEquals("Invalid connectivity constraint.",
Matthew Williamsd1c06752014-08-22 14:15:28 -0700287 first.getNetworkType() == JobInfo.NETWORK_TYPE_ANY,
288 second.getNetworkType() == JobInfo.NETWORK_TYPE_ANY);
Matthew Williams3d86fd22014-05-16 18:02:17 -0700289 assertEquals("Invalid deadline constraint.",
290 first.hasLateConstraint(),
291 second.hasLateConstraint());
292 assertEquals("Invalid delay constraint.",
293 first.hasEarlyConstraint(),
294 second.hasEarlyConstraint());
295 assertEquals("Extras don't match",
296 first.getExtras().toString(), second.getExtras().toString());
297 }
298
299 /**
300 * When comparing timestamps before and after DB read/writes (to make sure we're saving/loading
301 * the correct values), there is some latency involved that terrorises a naive assertEquals().
302 * We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision
303 * more reasonable.
304 */
305 private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) {
306 final long DELTA_MILLIS = 700L; // We allow up to 700ms of latency for IO read/writes.
307 assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS + IO_WAIT);
308 }
309
310 private static class StubClass {}
311
Matthew Williams01ac45b2014-07-22 20:44:12 -0700312}