@Test
  public void testDelayedStart() throws Exception {
    expectJobAccepted();
    expectJobFetch();

    // Query to test if live tasks exist for the job.
    expectActiveTaskFetch(TASK);

    // Live tasks exist, so the cron manager must delay the cron launch.
    delayExecutor.execute(capture(delayLaunchCapture));

    // The cron manager will then try to initiate the kill.
    scheduler.killTasks((Query.Builder) anyObject(), eq(CronJobManager.CRON_USER));

    // Immediate query and delayed query.
    expectActiveTaskFetch(TASK).times(2);

    // Simulate the live task disappearing.
    expectActiveTaskFetch();

    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());

    control.replay();

    cron.receiveJob(sanitizedConfiguration);
    cron.startJobNow(job.getKey());
    assertEquals(ImmutableSet.of(job.getKey()), cron.getPendingRuns());
    delayLaunchCapture.getValue().run();
    assertEquals(ImmutableSet.<IJobKey>of(), cron.getPendingRuns());
  }
  @Test
  public void testUpdate() throws Exception {
    SanitizedConfiguration updated =
        new SanitizedConfiguration(
            IJobConfiguration.build(job.newBuilder().setCronSchedule("1 2 3 4 5")));

    expectJobAccepted();
    cronScheduler.deschedule(DEFAULT_JOB_KEY);
    expectJobAccepted(updated.getJobConfig());

    control.replay();

    cron.receiveJob(sanitizedConfiguration);
    cron.updateJob(updated);
  }
  @Test(expected = ScheduleException.class)
  public void testInvalidCronSchedule() throws Exception {
    expect(cronScheduler.isValidSchedule(job.getCronSchedule())).andReturn(false);

    control.replay();

    cron.receiveJob(sanitizedConfiguration);
  }
  @Test(expected = ScheduleException.class)
  public void testRunOverlapRejected() throws Exception {
    IJobConfiguration killExisting =
        IJobConfiguration.build(
            job.newBuilder().setCronCollisionPolicy(CronCollisionPolicy.RUN_OVERLAP));

    control.replay();

    cron.receiveJob(new SanitizedConfiguration(killExisting));
  }
  @Test
  public void testDelayedStartResets() throws Exception {
    expectJobAccepted();
    expectJobFetch();

    // Query to test if live tasks exist for the job.
    expectActiveTaskFetch(TASK);

    // Live tasks exist, so the cron manager must delay the cron launch.
    delayExecutor.execute(capture(delayLaunchCapture));

    // The cron manager will then try to initiate the kill.
    scheduler.killTasks((Query.Builder) anyObject(), eq(CronJobManager.CRON_USER));

    // Immediate query and delayed query.
    expectActiveTaskFetch(TASK).times(2);

    // Simulate the live task disappearing.
    expectActiveTaskFetch();

    // Round two.
    expectJobFetch();
    expectActiveTaskFetch(TASK);
    delayExecutor.execute(capture(delayLaunchCapture));
    scheduler.killTasks((Query.Builder) anyObject(), eq(CronJobManager.CRON_USER));
    expectActiveTaskFetch(TASK).times(2);
    expectActiveTaskFetch();

    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
    expectLastCall().times(2);

    control.replay();

    cron.receiveJob(sanitizedConfiguration);
    cron.startJobNow(job.getKey());
    delayLaunchCapture.getValue().run();

    // Start the job again.  Since the previous delayed start completed, this should repeat the
    // entire process.
    cron.startJobNow(job.getKey());
    delayLaunchCapture.getValue().run();
  }
  @Test(expected = ScheduleException.class)
  public void testScheduleFails() throws Exception {
    expectJobValidated(job);
    storageUtil.jobStore.saveAcceptedJob(MANAGER_KEY, sanitizedConfiguration.getJobConfig());
    expect(cronScheduler.schedule(eq(job.getCronSchedule()), EasyMock.<Runnable>anyObject()))
        .andThrow(new CronException("injected"));

    control.replay();

    cron.receiveJob(sanitizedConfiguration);
  }
  @Test
  public void testDeleteInconsistent() throws Exception {
    // Tests a case where a job exists in the storage, but is not registered with the cron system.

    expect(storageUtil.jobStore.fetchJob(MANAGER_KEY, job.getKey()))
        .andReturn(Optional.of(sanitizedConfiguration.getJobConfig()));

    control.replay();

    assertTrue(cron.deleteJob(job.getKey()));
  }
  @Test
  public void testStart() throws Exception {
    expectJobAccepted();
    expectJobFetch();
    expectActiveTaskFetch();

    // Job is executed immediately since there are no existing tasks to kill.
    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());
    expect(cronScheduler.getSchedule(DEFAULT_JOB_KEY))
        .andReturn(Optional.of(job.getCronSchedule()))
        .times(2);

    control.replay();

    assertEquals(ImmutableMap.<IJobKey, String>of(), cron.getScheduledJobs());
    cron.receiveJob(sanitizedConfiguration);
    assertEquals(ImmutableMap.of(job.getKey(), job.getCronSchedule()), cron.getScheduledJobs());
    cron.startJobNow(job.getKey());
    assertEquals(ImmutableMap.of(job.getKey(), job.getCronSchedule()), cron.getScheduledJobs());
  }
  @Test
  public void testCancelNewCollision() throws Exception {
    IJobConfiguration killExisting =
        IJobConfiguration.build(
            job.newBuilder().setCronCollisionPolicy(CronCollisionPolicy.CANCEL_NEW));
    Capture<Runnable> jobTriggerCapture = expectJobAccepted(killExisting);
    expectActiveTaskFetch(TASK);

    control.replay();

    cron.receiveJob(new SanitizedConfiguration(killExisting));
    jobTriggerCapture.getValue().run();
  }
  @Test
  public void testKillExistingCollisionFailedKill() throws Exception {
    IJobConfiguration killExisting =
        IJobConfiguration.build(
            job.newBuilder().setCronCollisionPolicy(CronCollisionPolicy.KILL_EXISTING));
    Capture<Runnable> jobTriggerCapture = expectJobAccepted(killExisting);
    expectActiveTaskFetch(TASK);
    scheduler.killTasks(Query.jobScoped(killExisting.getKey()).active(), CRON_USER);
    expectLastCall().andThrow(new ScheduleException("injected"));

    control.replay();

    cron.receiveJob(new SanitizedConfiguration(killExisting));
    jobTriggerCapture.getValue().run();
  }
  @Test
  public void testConsistentState() throws Exception {
    IJobConfiguration updated =
        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5"));

    expectJobAccepted();
    cronScheduler.deschedule(DEFAULT_JOB_KEY);
    expectJobAccepted(updated);

    control.replay();

    cron.receiveJob(sanitizedConfiguration);

    IJobConfiguration failedUpdate =
        IJobConfiguration.build(updated.newBuilder().setCronSchedule(null));
    try {
      cron.updateJob(new SanitizedConfiguration(failedUpdate));
      fail();
    } catch (ScheduleException e) {
      // Expected.
    }

    cron.updateJob(new SanitizedConfiguration(updated));
  }
  @Test
  public void testDelayedStartMultiple() throws Exception {
    expectJobAccepted();
    expectJobFetch();

    // Query to test if live tasks exist for the job.
    expectActiveTaskFetch(TASK).times(3);

    // Live tasks exist, so the cron manager must delay the cron launch.
    delayExecutor.execute(capture(delayLaunchCapture));

    // The cron manager will then try to initiate the kill.
    expectJobFetch();
    expectJobFetch();
    scheduler.killTasks((Query.Builder) anyObject(), eq(CronJobManager.CRON_USER));
    expectLastCall().times(3);

    // Immediate queries and delayed query.
    expectActiveTaskFetch(TASK).times(4);

    // Simulate the live task disappearing.
    expectActiveTaskFetch();

    stateManager.insertPendingTasks(sanitizedConfiguration.getTaskConfigs());

    control.replay();

    cron.receiveJob(sanitizedConfiguration);

    // Attempt to trick the cron manager into launching multiple times, or launching multiple
    // pollers.
    cron.startJobNow(job.getKey());
    cron.startJobNow(job.getKey());
    cron.startJobNow(job.getKey());
    delayLaunchCapture.getValue().run();
  }
  @Before
  public void setUp() throws Exception {
    scheduler = createMock(SchedulerCore.class);
    stateManager = createMock(StateManager.class);
    delayExecutor = createMock(Executor.class);
    delayLaunchCapture = createCapture();
    storageUtil = new StorageTestUtil(this);
    storageUtil.expectOperations();
    cronScheduler = createMock(CronScheduler.class);
    shutdownRegistry = createMock(ShutdownRegistry.class);

    cron =
        new CronJobManager(
            stateManager, storageUtil.storage, cronScheduler, shutdownRegistry, delayExecutor);
    cron.schedulerCore = scheduler;
    job = makeJob();
    sanitizedConfiguration = SanitizedConfiguration.fromUnsanitized(job);
  }
  @Test
  public void testInvalidStoredJob() throws Exception {
    // Invalid jobs are left alone, but doesn't halt operation.

    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
    cronScheduler.awaitRunning();
    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());

    IJobConfiguration jobA =
        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5 6 7"));
    IJobConfiguration jobB = IJobConfiguration.build(makeJob().newBuilder().setCronSchedule(null));

    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY)).andReturn(ImmutableList.of(jobA, jobB));
    expect(cronScheduler.isValidSchedule(jobA.getCronSchedule())).andReturn(false);

    control.replay();

    cron.schedulerActive(new SchedulerActive());
  }
  @Test
  public void testRunOverlapLoadedSuccessfully() throws Exception {
    // Existing RUN_OVERLAP jobs should still load and map.

    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
    cronScheduler.awaitRunning();
    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());

    IJobConfiguration jobA =
        IJobConfiguration.build(
            makeJob().newBuilder().setCronCollisionPolicy(CronCollisionPolicy.RUN_OVERLAP));

    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY)).andReturn(ImmutableList.of(jobA));
    expect(cronScheduler.isValidSchedule(jobA.getCronSchedule())).andReturn(true);
    expect(cronScheduler.schedule(eq(jobA.getCronSchedule()), EasyMock.<Runnable>anyObject()))
        .andReturn("keyA");

    control.replay();

    cron.schedulerActive(new SchedulerActive());
  }
  @Test(expected = IllegalStateException.class)
  public void testJobStoredTwice() throws Exception {
    // Simulate an inconsistent storage that contains two cron jobs under the same key.

    expect(cronScheduler.startAsync()).andReturn(cronScheduler);
    cronScheduler.awaitRunning();
    shutdownRegistry.addAction(EasyMock.<ExceptionalCommand<?>>anyObject());

    IJobConfiguration jobA =
        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("1 2 3 4 5"));
    IJobConfiguration jobB =
        IJobConfiguration.build(makeJob().newBuilder().setCronSchedule("* * * * *"));
    expect(storageUtil.jobStore.fetchJobs(MANAGER_KEY)).andReturn(ImmutableList.of(jobA, jobB));
    expectJobValidated(jobA);
    expect(cronScheduler.schedule(eq(jobA.getCronSchedule()), EasyMock.<Runnable>anyObject()))
        .andReturn("keyA");
    expectJobValidated(jobB);

    control.replay();

    cron.schedulerActive(new SchedulerActive());
  }