package com.evernote.android.job; import android.os.Build; import android.os.Bundle; import androidx.annotation.NonNull; import com.evernote.android.job.test.DummyJobs; import com.evernote.android.job.test.JobRobolectricTestRunner; import com.evernote.android.job.test.TestLogger; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.robolectric.annotation.Config; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Java6Assertions.assertThat; /** * @author rwondratschek */ @RunWith(JobRobolectricTestRunner.class) @FixMethodOrder(MethodSorters.JVM) public class JobExecutionTest extends BaseJobManagerTest { @Test public void testPeriodicJob() throws Exception { int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class) .setPeriodic(TimeUnit.MINUTES.toMillis(15)) .build() .schedule(); executeJob(jobId, Job.Result.SUCCESS); // make sure job request is still around assertThat(manager().getAllJobRequestsForTag(DummyJobs.SuccessJob.TAG)).hasSize(1); } @Test public void testSimpleJob() throws Throwable { final int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class) .setExecutionWindow(200_000L, 400_000L) .build() .schedule(); executeJob(jobId, Job.Result.SUCCESS); assertThat(manager().getAllJobRequestsForTag(DummyJobs.SuccessJob.TAG)).isEmpty(); assertThat(manager().getJobRequest(jobId)).isNull(); assertThat(manager().getJobRequest(jobId, true)).isNull(); } @Test public void testStartedState() throws Throwable { int jobId = DummyJobs.createBuilder(DummyJobs.TwoSecondPauseJob.class) .setExecutionWindow(300_000, 400_000) .build() .schedule(); executeJobAsync(jobId, Job.Result.SUCCESS); // wait until the job is started Thread.sleep(100); // request should be in started state, running but not removed from DB JobRequest startedRequest = manager().getJobRequest(jobId, true); assertThat(startedRequest).isNotNull(); assertThat(startedRequest.isStarted()).isTrue(); } @Test public void testPeriodicJobNotInStartedState() throws Throwable { int jobId = DummyJobs.createBuilder(DummyJobs.TwoSecondPauseJob.class) .setPeriodic(TimeUnit.MINUTES.toMillis(15)) .build() .schedule(); executeJobAsync(jobId, Job.Result.SUCCESS); // wait until the job is started Thread.sleep(100); // request should be in started state, running but not removed from DB JobRequest startedRequest = manager().getJobRequest(jobId, true); assertThat(startedRequest).isNotNull(); assertThat(startedRequest.isStarted()).isFalse(); } @Test public void verifyNoRaceConditionOneOff() throws Exception { final int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class) .setExecutionWindow(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(20)) .build() .schedule(); final JobProxy.Common common = new JobProxy.Common(context(), TestLogger.INSTANCE, jobId); final JobRequest request = common.getPendingRequest(true, true); assertThat(request).isNotNull(); final CountDownLatch latch = new CountDownLatch(1); new Thread() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException ignored) { } common.executeJobRequest(request, null); latch.countDown(); } }.start(); assertThat(common.getPendingRequest(true, false)).isNull(); assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue(); Thread.sleep(2_000); assertThat(common.getPendingRequest(true, false)).isNull(); } @Test public void verifyNoRaceConditionPeriodic() throws Exception { final int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class) .setPeriodic(TimeUnit.MINUTES.toMillis(15)) .build() .schedule(); final JobProxy.Common common = new JobProxy.Common(context(), TestLogger.INSTANCE, jobId); final JobRequest request = common.getPendingRequest(true, true); assertThat(request).isNotNull(); final CountDownLatch latch = new CountDownLatch(1); new Thread() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException ignored) { } common.executeJobRequest(request, null); latch.countDown(); } }.start(); assertThat(common.getPendingRequest(true, false)).isNull(); assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue(); Thread.sleep(2_000); assertThat(common.getPendingRequest(true, false)).isNotNull(); } @Test public void verifyPendingRequestNullWhenMarkedStated() { final int jobId = DummyJobs.createBuilder(DummyJobs.SuccessJob.class) .setPeriodic(TimeUnit.MINUTES.toMillis(15)) .build() .schedule(); final JobProxy.Common common = new JobProxy.Common(context(), TestLogger.INSTANCE, jobId); assertThat(common.getPendingRequest(true, false)).isNotNull(); assertThat(common.getPendingRequest(true, false)).isNotNull(); JobRequest request = common.getPendingRequest(true, false); assertThat(request).isNotNull(); common.markStarting(request); assertThat(common.getPendingRequest(true, false)).isNull(); } @Test public void verifyCanceledJobNotRescheduled() { final AtomicBoolean onRescheduleCalled = new AtomicBoolean(false); final Job job = new Job() { @NonNull @Override protected Result onRunJob(@NonNull Params params) { manager().cancelAll(); return Result.RESCHEDULE; } @Override protected void onReschedule(int newJobId) { onRescheduleCalled.set(true); } }; JobCreator jobCreator = new JobCreator() { @Override public Job create(@NonNull String tag) { return job; } }; manager().addJobCreator(jobCreator); final String tag = "something"; final int jobId = new JobRequest.Builder(tag) .setExecutionWindow(200_000L, 400_000L) .build() .schedule(); executeJob(jobId, Job.Result.RESCHEDULE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); assertThat(manager().getJobRequest(jobId)).isNull(); assertThat(manager().getJobRequest(jobId, true)).isNull(); assertThat(onRescheduleCalled.get()).isFalse(); } @Test public void verifySynchronizedAllowed() throws InterruptedException { final CountDownLatch start = new CountDownLatch(1); final Job job = new Job() { @NonNull @Override protected synchronized Result onRunJob(@NonNull Params params) { start.countDown(); try { Thread.sleep(8_000L); } catch (InterruptedException e) { e.printStackTrace(); } return Result.SUCCESS; } }; JobCreator jobCreator = new JobCreator() { @Override public Job create(@NonNull String tag) { return job; } }; manager().addJobCreator(jobCreator); final int jobId = new JobRequest.Builder("something") .setExecutionWindow(200_000L, 400_000L) .build() .schedule(); executeJobAsync(jobId, Job.Result.SUCCESS); assertThat(start.await(2, TimeUnit.SECONDS)).isTrue(); final CountDownLatch canceledWithin2Seconds = new CountDownLatch(1); new Thread() { @Override public void run() { job.cancel(); canceledWithin2Seconds.countDown(); } }.start(); assertThat(canceledWithin2Seconds.await(2, TimeUnit.MILLISECONDS)).isTrue(); } @Test public void verifyReschedulingTransientJobsWorks() { Bundle extras = new Bundle(); extras.putString("key", "hello"); int previousJobId = DummyJobs.createBuilder(DummyJobs.RescheduleJob.class) .setExecutionWindow(200_000L, 400_000L) .setTransientExtras(extras) .build() .schedule(); for (int i = 0; i < 5; i++) { executeJob(previousJobId, Job.Result.RESCHEDULE); assertThat(manager().getAllJobRequestsForTag(DummyJobs.RescheduleJob.TAG)).hasSize(1); JobRequest request = manager().getAllJobRequestsForTag(DummyJobs.RescheduleJob.TAG).iterator().next(); assertThat(request.getJobId()).isNotEqualTo(previousJobId); assertThat(request.getTransientExtras().getString("key", null)).isEqualTo("hello"); previousJobId = request.getJobId(); } } @Test public void verifyReschedulingInexactJobUsesTimeWindow() { int previousJobId = DummyJobs.createBuilder(DummyJobs.RescheduleJob.class) .setExecutionWindow(200_000L, 400_000L) .setBackoffCriteria(TimeUnit.MINUTES.toMillis(1), JobRequest.BackoffPolicy.LINEAR) .build() .schedule(); executeJob(previousJobId, Job.Result.RESCHEDULE); Set<JobRequest> allJobRequestsForTag = manager().getAllJobRequestsForTag(DummyJobs.RescheduleJob.TAG); assertThat(allJobRequestsForTag).hasSize(1); JobRequest firstRetry = allJobRequestsForTag.iterator().next(); assertThat(JobProxy.Common.getStartMs(firstRetry)).isNotEqualTo(JobProxy.Common.getEndMs(firstRetry)); executeJob(firstRetry.getJobId(), Job.Result.RESCHEDULE); allJobRequestsForTag = manager().getAllJobRequestsForTag(DummyJobs.RescheduleJob.TAG); assertThat(allJobRequestsForTag).hasSize(1); JobRequest secondRetry = allJobRequestsForTag.iterator().next(); assertThat(JobProxy.Common.getStartMs(secondRetry)).isNotEqualTo(JobProxy.Common.getEndMs(secondRetry)); assertThat(JobProxy.Common.getStartMs(secondRetry)).isNotEqualTo(JobProxy.Common.getEndMs(secondRetry)); } @Test public void verifyNotFoundJobCanceledOneOff() { final String tag = "something"; final int jobId = new JobRequest.Builder(tag) .setExecutionWindow(TimeUnit.HOURS.toMillis(4), TimeUnit.HOURS.toMillis(5)) .build() .schedule(); assertThat(manager().getAllJobRequestsForTag(tag)).hasSize(1); executeJob(jobId, Job.Result.FAILURE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); } @Test public void verifyNotFoundJobCanceledExact() { final String tag = "something"; final int jobId = new JobRequest.Builder(tag) .setExact(TimeUnit.HOURS.toMillis(4)) .build() .schedule(); assertThat(manager().getAllJobRequestsForTag(tag)).hasSize(1); executeJob(jobId, Job.Result.FAILURE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); } @Test public void verifyNotFoundJobCanceledDailyJob() { final String tag = "something"; int jobId = DailyJob.schedule(new JobRequest.Builder(tag), TimeUnit.HOURS.toMillis(5), TimeUnit.HOURS.toMillis(6)); assertThat(manager().getAllJobRequestsForTag(tag)).hasSize(1); executeJob(jobId, Job.Result.FAILURE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); } @Test public void verifyNotFoundJobCanceledPeriodic() { final String tag = "something"; final int jobId = new JobRequest.Builder(tag) .setPeriodic(TimeUnit.HOURS.toMillis(4)) .build() .schedule(); assertThat(manager().getAllJobRequestsForTag(tag)).hasSize(1); executeJob(jobId, Job.Result.FAILURE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); } @Test @Config(sdk = Build.VERSION_CODES.M) public void verifyNotFoundJobCanceledPeriodicFlexSupport() { final String tag = "something"; final int jobId = new JobRequest.Builder(tag) .setPeriodic(TimeUnit.HOURS.toMillis(4)) .build() .schedule(); assertThat(manager().getAllJobRequestsForTag(tag)).hasSize(1); executeJob(jobId, Job.Result.FAILURE); assertThat(manager().getAllJobRequestsForTag(tag)).isEmpty(); } }