package com.doist.jobschedulercompat.job; import com.doist.jobschedulercompat.JobInfo; import com.doist.jobschedulercompat.PersistableBundle; import com.doist.jobschedulercompat.util.BundleUtils; import com.doist.jobschedulercompat.util.JobCreator; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import android.app.Application; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import java.util.Collections; import java.util.Iterator; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import androidx.test.core.app.ApplicationProvider; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) public class JobStoreTest { private Application application; private JobStore jobStore; @Before public void setup() { application = ApplicationProvider.getApplicationContext(); jobStore = JobStore.get(application); } @After public void teardown() { synchronized (JobStore.LOCK) { jobStore.clear(); } } @Test public void testMaybeWriteStatusToDisk() { JobInfo job = JobCreator.create(application) .setRequiresCharging(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .setBackoffCriteria(10000L, JobInfo.BACKOFF_POLICY_EXPONENTIAL) .setOverrideDeadline(20000L) .setMinimumLatency(2000L) .setPersisted(true) .build(); JobStatus jobStatus = JobStatus.createFromJobInfo(job, "noop"); jobStore.add(jobStatus); waitForJobStoreWrite(); // Manually load tasks from xml file. JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getJobs().get(0); assertJobInfoEquals(job, loaded.getJob()); assertTrue("JobStore#containsJob invalid", jobStore.containsJob(jobStatus)); compareTimestampsSubjectToIoLatency( "Early run-times not the same after read", jobStatus.getEarliestRunTimeElapsed(), loaded.getEarliestRunTimeElapsed()); compareTimestampsSubjectToIoLatency( "Late run-times not the same after read", jobStatus.getLatestRunTimeElapsed(), loaded.getLatestRunTimeElapsed()); } @Test public void testWritingTwoFilesToDisk() { JobInfo job1 = JobCreator.create(application) .setRequiresDeviceIdle(true) .setPeriodic(10000L) .setRequiresCharging(true) .setPersisted(true) .build(); JobInfo job2 = JobCreator.create(application) .setMinimumLatency(5000L) .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR) .setOverrideDeadline(30000L) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setPersisted(true) .build(); JobStatus jobStatus1 = JobStatus.createFromJobInfo(job1, "noop"); JobStatus jobStatus2 = JobStatus.createFromJobInfo(job2, "noop"); jobStore.add(jobStatus1); jobStore.add(jobStatus2); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size()); Iterator<JobStatus> it = jobStatusSet.getJobs().iterator(); JobStatus loaded1 = it.next(); JobStatus loaded2 = it.next(); // Reverse them so we know which comparison to make. if (loaded1.getJobId() != job1.getId()) { JobStatus tmp = loaded1; loaded1 = loaded2; loaded2 = tmp; } assertJobInfoEquals(job1, loaded1.getJob()); assertJobInfoEquals(job2, loaded2.getJob()); assertTrue("JobStore#containsJob invalid.", jobStore.containsJob(jobStatus1)); assertTrue("JobStore#containsJob invalid.", jobStore.containsJob(jobStatus2)); // Check that the loaded task has the correct runtimes. compareTimestampsSubjectToIoLatency( "Early run-times not the same after read.", jobStatus1.getEarliestRunTimeElapsed(), loaded1.getEarliestRunTimeElapsed()); compareTimestampsSubjectToIoLatency( "Late run-times not the same after read.", jobStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed()); compareTimestampsSubjectToIoLatency( "Early run-times not the same after read.", jobStatus2.getEarliestRunTimeElapsed(), loaded2.getEarliestRunTimeElapsed()); compareTimestampsSubjectToIoLatency( "Late run-times not the same after read.", jobStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed()); } @Test public void testWritingTaskWithExtras() { JobInfo.Builder builder = JobCreator.create(application) .setRequiresDeviceIdle(true) .setPeriodic(10000L) .setRequiresCharging(true) .setPersisted(true); PersistableBundle extras = new PersistableBundle(); extras.putDouble("hello", 3.2); extras.putString("hi", "there"); extras.putInt("into", 3); builder.setExtras(extras); JobInfo job = builder.build(); JobStatus jobStatus = JobStatus.createFromJobInfo(job, "noop"); jobStore.add(jobStatus); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getJobs().iterator().next(); assertJobInfoEquals(job, loaded.getJob()); } public void testWritingTaskWithFlex() { JobInfo.Builder builder = JobCreator.create(application) .setRequiresDeviceIdle(true) .setPeriodic(TimeUnit.HOURS.toMillis(5), TimeUnit.HOURS.toMillis(1)) .setRequiresCharging(true) .setPersisted(true); JobStatus taskStatus = JobStatus.createFromJobInfo(builder.build(), "noop"); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getJobs().iterator().next(); assertEquals("Period not equal", loaded.getJob().getIntervalMillis(), taskStatus.getJob().getIntervalMillis()); assertEquals("Flex not equal", loaded.getJob().getFlexMillis(), taskStatus.getJob().getFlexMillis()); } @Test public void testMassivePeriodClampedOnRead() { long period = TimeUnit.HOURS.toMillis(2); JobInfo job = JobCreator.create(application).setPeriodic(period).setPersisted(true).build(); long invalidLateRuntimeElapsedMillis = SystemClock.elapsedRealtime() + (period) + period; // > period. long invalidEarlyRuntimeElapsedMillis = invalidLateRuntimeElapsedMillis - period; // Early = (late - period). JobStatus jobStatus = new JobStatus(job, "noop", invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis); jobStore.add(jobStatus); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getJobs().iterator().next(); // Assert early runtime was clamped to be under now + period. We can do <= here b/c we'll // call SystemClock.elapsedRealtime after doing the disk i/o. long newNowElapsed = SystemClock.elapsedRealtime(); assertTrue("Early runtime wasn't correctly clamped.", loaded.getEarliestRunTimeElapsed() <= newNowElapsed + period); // Assert late runtime was clamped to be now + period + flex. assertTrue("Early runtime wasn't correctly clamped.", loaded.getEarliestRunTimeElapsed() <= newNowElapsed + period); } @Test public void testSchedulerPersisted() { JobInfo job = JobCreator.create(application) .setOverrideDeadline(5000) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING) .setRequiresBatteryNotLow(true) .setPersisted(true) .build(); JobStatus jobStatus = JobStatus.createFromJobInfo(job, "noop"); jobStore.add(jobStatus); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); Iterator<JobStatus> it = jobStatusSet.getJobs().iterator(); assertTrue(it.hasNext()); assertEquals("Scheduler not correctly persisted.", "noop", it.next().getSchedulerTag()); } @SuppressWarnings("unchecked") @Test public void testCompat() { Uri uri = Uri.parse("doist.com"); String authority = "com.doist"; JobInfo.Builder builder = JobCreator.create(application) .addTriggerContentUri(new JobInfo.TriggerContentUri(uri, 0)) .setTriggerContentUpdateDelay(TimeUnit.SECONDS.toMillis(5)) .setTriggerContentMaxDelay(TimeUnit.SECONDS.toMillis(30)); Bundle transientExtras = new Bundle(); transientExtras.putBoolean("test", true); builder.setTransientExtras(transientExtras); JobInfo job = builder.build(); JobStatus jobStatus = JobStatus.createFromJobInfo(job, "noop"); jobStatus.changedUris = Collections.singleton(uri); jobStatus.changedAuthorities = Collections.singleton(authority); jobStore.add(jobStatus); waitForJobStoreWrite(); JobStore.JobSet jobStatusSet = new JobStore.JobSet(); jobStore.readJobMapFromDisk(jobStatusSet); assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getJobs().iterator().next(); assertEquals(jobStatus.changedUris, loaded.changedUris); assertEquals(jobStatus.changedAuthorities, loaded.changedAuthorities); assertJobInfoEquals(job, loaded.getJob()); } private void waitForJobStoreWrite() { try { final Semaphore semaphore = new Semaphore(1); semaphore.acquire(); jobStore.queue.offer(new Runnable() { @Override public void run() { semaphore.release(); } }, 1, TimeUnit.SECONDS); semaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * Helper function to assert that two {@link JobInfo} are equal. */ private void assertJobInfoEquals(JobInfo first, JobInfo second) { assertEquals("Different task ids", first.getId(), second.getId()); assertEquals("Different components", first.getService(), second.getService()); assertEquals("Different periodic status", first.isPeriodic(), second.isPeriodic()); assertEquals("Different period", first.getIntervalMillis(), second.getIntervalMillis()); assertEquals("Different initial backoff", first.getInitialBackoffMillis(), second.getInitialBackoffMillis()); assertEquals("Different backoff policy", first.getBackoffPolicy(), second.getBackoffPolicy()); assertEquals("Invalid charging constraint", first.isRequireCharging(), second.isRequireCharging()); assertEquals("Invalid battery not low constraint", first.isRequireBatteryNotLow(), second.isRequireBatteryNotLow()); assertEquals("Invalid idle constraint", first.isRequireDeviceIdle(), second.isRequireDeviceIdle()); assertEquals("Invalid connectivity constraint", first.getNetworkType(), second.getNetworkType()); assertEquals("Invalid deadline constraint", first.hasLateConstraint(), second.hasLateConstraint()); assertEquals("Invalid delay constraint", first.hasEarlyConstraint(), second.hasEarlyConstraint()); assertEquals("Extras don't match", first.getExtras().toMap(10), second.getExtras().toMap(10)); assertEquals("Transient extras don't match", BundleUtils.toMap(first.getTransientExtras(), 10), BundleUtils.toMap(second.getTransientExtras(), 10)); } /** * Comparing timestamps before and after IO read/writes involves some latency. */ private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) { assertTrue(error, Math.abs(ts1 - ts2) < TimeUnit.SECONDS.toMillis(1000)); } }