diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index f3c96af5fc..00b25c2273 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -245,15 +245,9 @@ - org.quartz-scheduler - quartz - ${quartz.version} - - - c3p0 - c3p0 - - + com.cronutils + cron-utils + 8.1.1 diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 83eb03fe61..49ac0a3d59 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -80,7 +80,7 @@ import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.repository.xml.XmlRepositoryRoleDAO; -import sonia.scm.schedule.QuartzScheduler; +import sonia.scm.schedule.CronScheduler; import sonia.scm.schedule.Scheduler; import sonia.scm.security.AccessTokenCookieIssuer; import sonia.scm.security.AuthorizationChangedEventProducer; @@ -218,7 +218,7 @@ public class ScmServletModule extends ServletModule bind(PluginManager.class, DefaultPluginManager.class); // bind scheduler - bind(Scheduler.class).to(QuartzScheduler.class); + bind(Scheduler.class).to(CronScheduler.class); // bind health check stuff bind(HealthCheckContextListener.class); diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronExpression.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronExpression.java new file mode 100644 index 0000000000..fbbff51f57 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronExpression.java @@ -0,0 +1,59 @@ +package sonia.scm.schedule; + +import com.cronutils.model.Cron; +import com.cronutils.model.CronType; +import com.cronutils.model.definition.CronDefinition; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.model.time.ExecutionTime; +import com.cronutils.parser.CronParser; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Optional; + +final class CronExpression { + + private final Clock clock; + private final String expression; + private final ExecutionTime executionTime; + + CronExpression(String expression) { + this(Clock.systemUTC(), expression); + } + + CronExpression(Clock clock, String expression) { + this.clock = clock; + this.expression = expression; + executionTime = createExecutionTime(expression); + } + + boolean shouldRun(ZonedDateTime time) { + ZonedDateTime now = ZonedDateTime.now(clock); + return time.isBefore(now); + } + + Optional calculateNextRun() { + ZonedDateTime now = ZonedDateTime.now(clock); + Optional nextExecution = executionTime.nextExecution(now); + if (nextExecution.isPresent()) { + ZonedDateTime next = nextExecution.get(); + if (Duration.between(now, next).toMillis() < 1000) { + return executionTime.nextExecution(now.plusSeconds(1L)); + } + } + return nextExecution; + } + + private ExecutionTime createExecutionTime(String expression) { + CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ); + CronParser parser = new CronParser(cronDefinition); + Cron cron = parser.parse(expression); + return ExecutionTime.forCron(cron); + } + + @Override + public String toString() { + return expression; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java new file mode 100644 index 0000000000..7899697746 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java @@ -0,0 +1,57 @@ +package sonia.scm.schedule; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Singleton +public class CronScheduler implements Scheduler { + + private static final Logger LOG = LoggerFactory.getLogger(CronScheduler.class); + + private final ScheduledExecutorService executorService; + private final CronTaskFactory taskFactory; + + @Inject + public CronScheduler(CronTaskFactory taskFactory) { + this.taskFactory = taskFactory; + this.executorService = createExecutor(); + } + + private ScheduledExecutorService createExecutor() { + return Executors.newScheduledThreadPool(2, new CronThreadFactory()); + } + + @Override + public CronTask schedule(String expression, Runnable runnable) { + return schedule(taskFactory.create(expression, runnable)); + } + + @Override + public CronTask schedule(String expression, Class runnable) { + return schedule(taskFactory.create(expression, runnable)); + } + + private CronTask schedule(CronTask task) { + if (task.hasNextRun()) { + LOG.debug("schedule task {}", task); + Future future = executorService.scheduleAtFixedRate(task, 0L, 1L, TimeUnit.SECONDS); + task.setFuture(future); + } else { + LOG.debug("skip scheduling, because task {} has no next run", task); + } + return task; + } + + @Override + public void close() { + LOG.debug("shutdown underlying executor service"); + executorService.shutdown(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronTask.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronTask.java new file mode 100644 index 0000000000..0e8c636184 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronTask.java @@ -0,0 +1,74 @@ +package sonia.scm.schedule; + +import com.cronutils.utils.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.concurrent.Future; + +class CronTask implements Task, Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(CronTask.class); + + private final String name; + private final CronExpression expression; + private final Runnable runnable; + + private ZonedDateTime nextRun; + private Future future; + + CronTask(String name, CronExpression expression, Runnable runnable) { + this.name = name; + this.expression = expression; + this.runnable = runnable; + this.nextRun = expression.calculateNextRun().orElse(null); + } + + void setFuture(Future future) { + this.future = future; + } + + @Override + public synchronized void run() { + if (expression.shouldRun(nextRun)) { + LOG.debug("execute task {}, because of matching expression {}", name, expression); + runnable.run(); + Optional next = expression.calculateNextRun(); + if (next.isPresent()) { + nextRun = next.get(); + } else { + LOG.debug("cancel task {}, because expression {} has no next execution", name, expression); + cancel(); + } + } else { + LOG.trace("skip execution of task {}, because expression {} does not match", name, expression); + } + } + + boolean hasNextRun() { + return nextRun != null; + } + + @VisibleForTesting + String getName() { + return name; + } + + @VisibleForTesting + CronExpression getExpression() { + return expression; + } + + @Override + public synchronized void cancel() { + LOG.debug("cancel task {} with expression {}", name, expression); + future.cancel(false); + } + + @Override + public String toString() { + return name + "(" + expression + ")"; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronTaskFactory.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronTaskFactory.java new file mode 100644 index 0000000000..6ad7096363 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronTaskFactory.java @@ -0,0 +1,32 @@ +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.util.Providers; + +import javax.inject.Inject; +import javax.inject.Provider; + +class CronTaskFactory { + + private final Injector injector; + private final PrivilegedRunnableFactory runnableFactory; + + @Inject + public CronTaskFactory(Injector injector, PrivilegedRunnableFactory runnableFactory) { + this.injector = injector; + this.runnableFactory = runnableFactory; + } + + CronTask create(String expression, Runnable runnable) { + return create(expression, runnable.getClass().getName(), Providers.of(runnable)); + } + + CronTask create(String expression, Class runnable) { + return create(expression, runnable.getName(), injector.getProvider(runnable)); + } + + private CronTask create(String expression, String name, Provider runnableProvider) { + Runnable runnable = runnableFactory.create(runnableProvider); + return new CronTask(name, new CronExpression(expression), runnable); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronThreadFactory.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronThreadFactory.java new file mode 100644 index 0000000000..f4f77d1c3e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronThreadFactory.java @@ -0,0 +1,47 @@ +package sonia.scm.schedule; + +import org.apache.shiro.util.ThreadContext; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This thread factory creates threads without a shiro context. + * This is to avoid classloader leaks, because the {@link ThreadContext} of shiro uses {@link InheritableThreadLocal}, + * which could bind a class with a reference to a {@link sonia.scm.plugin.PluginClassLoader}. + */ +class CronThreadFactory implements ThreadFactory, AutoCloseable { + + private static final String NAME_TEMPLATE = "CronScheduler-%d-%d"; + + private static final AtomicLong FACTORY_COUNTER = new AtomicLong(); + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final long factoryId = FACTORY_COUNTER.incrementAndGet(); + private final AtomicLong threadCounter = new AtomicLong(); + + @Override + public Thread newThread(final Runnable r) { + try { + return executorService.submit(() -> { + ThreadContext.remove(); + return new Thread(r, createName()); + }).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("failed to schedule runnable"); + } + } + + private String createName() { + long threadId = threadCounter.incrementAndGet(); + return String.format(NAME_TEMPLATE, factoryId, threadId); + } + + @Override + public void close() { + executorService.shutdown(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java b/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java deleted file mode 100644 index dd8312a6d8..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java +++ /dev/null @@ -1,85 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import com.google.common.base.Preconditions; -import com.google.inject.Injector; -import com.google.inject.Provider; -import org.quartz.Job; -import org.quartz.JobDataMap; -import org.quartz.JobDetail; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.web.security.AdministrationContext; - -/** - * InjectionEnabledJob allows the execution of quartz jobs and enable injection on them. - * - * @author Sebastian Sdorra - * @since 1.47 - */ -public class InjectionEnabledJob implements Job { - - private static final Logger logger = LoggerFactory.getLogger(InjectionEnabledJob.class); - - @Override - @SuppressWarnings("unchecked") - public void execute(JobExecutionContext jec) throws JobExecutionException { - Preconditions.checkNotNull(jec, "execution context is null"); - - JobDetail detail = jec.getJobDetail(); - Preconditions.checkNotNull(detail, "job detail not provided"); - - JobDataMap dataMap = detail.getJobDataMap(); - Preconditions.checkNotNull(dataMap, "job detail does not contain data map"); - - Injector injector = (Injector) dataMap.get(Injector.class.getName()); - Preconditions.checkNotNull(injector, "data map does not contain injector"); - - final Provider runnableProvider = (Provider) dataMap.get(Runnable.class.getName()); - if (runnableProvider == null) { - throw new JobExecutionException("could not find runnable provider"); - } - - AdministrationContext ctx = injector.getInstance(AdministrationContext.class); - ctx.runAsAdmin(() -> { - logger.trace("create runnable from provider"); - Runnable runnable = runnableProvider.get(); - logger.debug("execute injection enabled job {}", runnable.getClass()); - runnable.run(); - }); - } - - -} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/PrivilegedRunnableFactory.java b/scm-webapp/src/main/java/sonia/scm/schedule/PrivilegedRunnableFactory.java new file mode 100644 index 0000000000..da630a78f2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/PrivilegedRunnableFactory.java @@ -0,0 +1,29 @@ +package sonia.scm.schedule; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.security.AdministrationContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +class PrivilegedRunnableFactory { + + private static final Logger LOG = LoggerFactory.getLogger(PrivilegedRunnableFactory.class); + + private final AdministrationContext context; + + @Inject + PrivilegedRunnableFactory(AdministrationContext context) { + this.context = context; + } + + public Runnable create(Provider runnableProvider) { + return () -> context.runAsAdmin(() -> { + LOG.trace("create runnable from provider"); + Runnable runnable = runnableProvider.get(); + LOG.debug("execute scheduled job {}", runnable.getClass()); + runnable.run(); + }); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java deleted file mode 100644 index 5b46d438d8..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java +++ /dev/null @@ -1,175 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import com.google.common.base.Throwables; -import com.google.inject.Injector; -import com.google.inject.Provider; -import com.google.inject.Singleton; -import java.io.IOException; -import javax.inject.Inject; -import org.quartz.CronScheduleBuilder; -import org.quartz.JobBuilder; -import org.quartz.JobDataMap; -import org.quartz.JobDetail; -import org.quartz.SchedulerException; -import org.quartz.Trigger; -import org.quartz.TriggerBuilder; -import org.quartz.impl.StdSchedulerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.Initable; -import sonia.scm.SCMContextProvider; - -/** - * {@link Scheduler} which uses the quartz scheduler. - * - * @author Sebastian Sdorra - * @since 1.47 - * - * @see Quartz Job Scheduler - */ -@Singleton -public class QuartzScheduler implements Scheduler, Initable { - - private static final Logger logger = LoggerFactory.getLogger(QuartzScheduler.class); - - private final Injector injector; - private final org.quartz.Scheduler scheduler; - - /** - * Creates a new quartz scheduler. - * - * @param injector injector - */ - @Inject - public QuartzScheduler(Injector injector) - { - this.injector = injector; - - // get default scheduler - try { - scheduler = StdSchedulerFactory.getDefaultScheduler(); - } catch (SchedulerException ex) { - throw Throwables.propagate(ex); - } - } - - /** - * Creates a new quartz scheduler. This constructor is only for testing. - * - * @param injector injector - * @param scheduler quartz scheduler - */ - QuartzScheduler(Injector injector, org.quartz.Scheduler scheduler) - { - this.injector = injector; - this.scheduler = scheduler; - } - - @Override - public void init(SCMContextProvider context) - { - try - { - if (!scheduler.isStarted()) - { - scheduler.start(); - } - } - catch (SchedulerException ex) - { - logger.error("can not start scheduler", ex); - } - } - - @Override - public void close() throws IOException - { - try - { - if (scheduler.isStarted()){ - scheduler.shutdown(); - } - } - catch (SchedulerException ex) - { - logger.error("can not stop scheduler", ex); - } - } - - @Override - public Task schedule(String expression, final Runnable runnable) - { - return schedule(expression, new Provider(){ - @Override - public Runnable get() - { - return runnable; - } - }); - } - - @Override - public Task schedule(String expression, Class runnable) - { - return schedule(expression, injector.getProvider(runnable)); - } - - private Task schedule(String expression, Provider provider){ - // create data map with injection provider for InjectionEnabledJob - JobDataMap map = new JobDataMap(); - map.put(Runnable.class.getName(), provider); - map.put(Injector.class.getName(), injector); - - // create job detail for InjectionEnabledJob with the provider for the annotated class - JobDetail detail = JobBuilder.newJob(InjectionEnabledJob.class) - .usingJobData(map) - .build(); - - // create a trigger with the cron expression from the annotation - Trigger trigger = TriggerBuilder.newTrigger() - .forJob(detail) - .withSchedule(CronScheduleBuilder.cronSchedule(expression)) - .build(); - - try { - scheduler.scheduleJob(detail, trigger); - } catch (SchedulerException ex) { - throw Throwables.propagate(ex); - } - - return new QuartzTask(scheduler, trigger.getJobKey()); - } - - -} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java deleted file mode 100644 index a45790a2e9..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java +++ /dev/null @@ -1,68 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import com.google.common.base.Throwables; -import org.quartz.JobKey; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; - -/** - * Task implementation for the {@link QuartzScheduler}. - * - * @author Sebastian Sdorra - */ -public class QuartzTask implements Task { - - private final org.quartz.Scheduler scheduler; - private final JobKey jobKey; - - QuartzTask(Scheduler scheduler, JobKey jobKey) - { - this.scheduler = scheduler; - this.jobKey = jobKey; - } - - @Override - public void cancel() - { - try - { - scheduler.deleteJob(jobKey); - } - catch (SchedulerException ex) - { - throw Throwables.propagate(ex); - } - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronExpressionTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronExpressionTest.java new file mode 100644 index 0000000000..6558bce781 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronExpressionTest.java @@ -0,0 +1,75 @@ +package sonia.scm.schedule; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CronExpressionTest { + + @Mock + private Clock clock; + + @BeforeEach + void setUpClockMock() { + when(clock.getZone()).thenReturn(ZoneId.systemDefault()); + when(clock.instant()).thenReturn(Instant.parse("2007-12-03T10:15:00.00Z")); + } + + @Test + void shouldCalculateTheNextRunIn30Seconds() { + assertNextRun("30 * * * * ?", 30); + } + + @Test + void shouldCalculateTheNextRunIn10Seconds() { + assertNextRun("0/10 * * * * ?", 10); + } + + @Test + void shouldReturnEmptyOptional() { + CronExpression expression = new CronExpression(clock, "30 12 12 12 * ? 1985"); + + Optional optionalNextRun = expression.calculateNextRun(); + assertThat(optionalNextRun).isNotPresent(); + } + + @Test + void shouldReturnTrue() { + ZonedDateTime time = ZonedDateTime.now(clock).minusSeconds(1L); + + CronExpression expression = new CronExpression(clock, "30 * * * * ?"); + assertThat(expression.shouldRun(time)).isTrue(); + } + + @Test + void shouldReturnFalse() { + ZonedDateTime time = ZonedDateTime.now(clock).plusSeconds(1L); + + CronExpression expression = new CronExpression(clock, "30 * * * * ?"); + assertThat(expression.shouldRun(time)).isFalse(); + } + + private void assertNextRun(String expressionAsString, long expectedSecondsToNextRun) { + CronExpression expression = new CronExpression(clock, expressionAsString); + + Optional optionalNextRun = expression.calculateNextRun(); + assertThat(optionalNextRun).isPresent(); + + ZonedDateTime nextRun = optionalNextRun.get(); + long nextRunInSeconds = Duration.between(ZonedDateTime.now(clock), nextRun).getSeconds(); + assertThat(nextRunInSeconds).isEqualTo(expectedSecondsToNextRun); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java new file mode 100644 index 0000000000..622b27228d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java @@ -0,0 +1,66 @@ +package sonia.scm.schedule; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CronSchedulerTest { + + @Mock + private CronTaskFactory taskFactory; + + @Mock + private CronTask task; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUpTaskFactory() { + lenient().when(taskFactory.create(anyString(), any(Runnable.class))).thenReturn(task); + lenient().when(taskFactory.create(anyString(), any(Class.class))).thenReturn(task); + } + + @Test + void shouldScheduleWithClass() { + when(task.hasNextRun()).thenReturn(true); + try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + scheduler.schedule("vep", TestingRunnable.class); + verify(task).setFuture(any(Future.class)); + } + } + + @Test + void shouldScheduleWithRunnable() { + when(task.hasNextRun()).thenReturn(true); + try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + scheduler.schedule("vep", new TestingRunnable()); + verify(task).setFuture(any(Future.class)); + } + } + + @Test + void shouldSkipSchedulingWithoutNextRun(){ + try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + scheduler.schedule("vep", new TestingRunnable()); + verify(task, never()).setFuture(any(Future.class)); + } + } + + private static class TestingRunnable implements Runnable { + + @Override + public void run() { + + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskFactoryTest.java new file mode 100644 index 0000000000..fe5c299b74 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskFactoryTest.java @@ -0,0 +1,67 @@ +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.util.Providers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.inject.Provider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CronTaskFactoryTest { + + @Mock + private Injector injector; + + @Mock + private PrivilegedRunnableFactory runnableFactory; + + @InjectMocks + private CronTaskFactory taskFactory; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUpMocks() { + when(runnableFactory.create(any(Provider.class))).thenAnswer(ic -> { + Provider r = ic.getArgument(0); + return r.get(); + }); + } + + @Test + void shouldCreateATaskWithNameFromRunnable() { + CronTask task = taskFactory.create("30 * * * * ?", new One()); + assertThat(task.getName()).isEqualTo(One.class.getName()); + } + + @Test + void shouldCreateATaskWithNameFromClass() { + when(injector.getProvider(One.class)).thenReturn(Providers.of(new One())); + + CronTask task = taskFactory.create("30 * * * * ?", One.class); + assertThat(task.getName()).isEqualTo(One.class.getName()); + } + + @Test + void shouldCreateATaskWithCronExpression() { + CronTask task = taskFactory.create("30 * * * * ?", new One()); + assertThat(task.getExpression().toString()).isEqualTo("30 * * * * ?"); + } + + public static class One implements Runnable { + + @Override + public void run() { + + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskTest.java new file mode 100644 index 0000000000..7cfbba29e3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronTaskTest.java @@ -0,0 +1,95 @@ +package sonia.scm.schedule; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.concurrent.Future; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CronTaskTest { + + @Mock + private CronExpression expression; + + @Mock + private Runnable runnable; + + @Mock + private Future future; + + @Test + void shouldReturnTrue() { + when(expression.calculateNextRun()).thenReturn(Optional.of(ZonedDateTime.now())); + + CronTask task = task(); + + assertThat(task.hasNextRun()).isTrue(); + } + + @Test + void shouldReturnFalse() { + when(expression.calculateNextRun()).thenReturn(Optional.empty()); + + CronTask task = task(); + + assertThat(task.hasNextRun()).isFalse(); + } + + private CronTask task() { + return new CronTask("one", expression, runnable); + } + + @Test + void shouldCancelWithoutNextRun() { + ZonedDateTime time = ZonedDateTime.now(); + when(expression.calculateNextRun()).thenAnswer(new FirstTimeAnswer(Optional.of(time), Optional.empty())); + when(expression.shouldRun(time)).thenReturn(true); + + CronTask task = task(); + task.setFuture(future); + task.run(); + + verify(runnable).run(); + verify(future).cancel(false); + } + + @Test + void shouldNotRun() { + task().run(); + + verify(runnable, never()).run(); + } + + private static class FirstTimeAnswer implements Answer { + + private boolean first = true; + private final Object answer; + private final Object secondAnswer; + + FirstTimeAnswer(Object answer, Object secondAnswer) { + this.answer = answer; + this.secondAnswer = secondAnswer; + } + + @Override + public Object answer(InvocationOnMock invocation) { + if (first) { + first = false; + return answer; + } + return secondAnswer; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronThreadFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronThreadFactoryTest.java new file mode 100644 index 0000000000..313f007e19 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronThreadFactoryTest.java @@ -0,0 +1,87 @@ +package sonia.scm.schedule; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class CronThreadFactoryTest { + + private Runnable doNothind = () -> {}; + + @Test + void shouldCreateThreadWithName() { + try (CronThreadFactory threadFactory = new CronThreadFactory()) { + Thread thread = threadFactory.newThread(doNothind); + assertThat(thread.getName()).startsWith("CronScheduler-"); + } + } + + @Test + void shouldCreateThreadsWithDifferentNames() { + try (CronThreadFactory threadFactory = new CronThreadFactory()) { + Thread one = threadFactory.newThread(doNothind); + Thread two = threadFactory.newThread(doNothind); + assertThat(one.getName()).isNotEqualTo(two.getName()); + } + } + + @Test + void shouldCreateThreadsWithDifferentNamesFromDifferentFactories() { + String one; + try (CronThreadFactory threadFactory = new CronThreadFactory()) { + one = threadFactory.newThread(doNothind).getName(); + } + + String two; + try (CronThreadFactory threadFactory = new CronThreadFactory()) { + two = threadFactory.newThread(doNothind).getName(); + } + + assertThat(one).isNotEqualTo(two); + } + + @Nested + class ShiroTests { + + @Mock + private Subject subject; + + @BeforeEach + void setUpContext() { + ThreadContext.bind(subject); + } + + @Test + void shouldNotInheritShiroContext() throws InterruptedException { + ShiroResourceCapturingRunnable runnable = new ShiroResourceCapturingRunnable(); + try (CronThreadFactory threadFactory = new CronThreadFactory()) { + Thread thread = threadFactory.newThread(runnable); + thread.start(); + thread.join(); + } + assertThat(runnable.resources).isSameAs(Collections.emptyMap()); + } + } + + + private static class ShiroResourceCapturingRunnable implements Runnable { + + private Map resources; + + @Override + public void run() { + resources = ThreadContext.getResources(); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java deleted file mode 100644 index 88516d761c..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java +++ /dev/null @@ -1,168 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import com.google.inject.Injector; -import com.google.inject.Provider; -import org.junit.Test; -import static org.mockito.Mockito.*; -import org.junit.Rule; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import org.quartz.JobDataMap; -import org.quartz.JobDetail; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import sonia.scm.web.security.AdministrationContext; -import sonia.scm.web.security.PrivilegedAction; - -/** - * Unit tests for {@link InjectionEnabledJob}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class InjectionEnabledJobTest { - - @Mock - private Injector injector; - - @Mock - private JobDataMap dataMap; - - @Mock - private JobDetail detail; - - @Mock - private JobExecutionContext jec; - - @Mock - private Provider runnable; - - @Mock - private AdministrationContext context; - - @Rule - public ExpectedException expected = ExpectedException.none(); - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without context. - * - * @throws JobExecutionException - */ - @Test - public void testExecuteWithoutContext() throws JobExecutionException - { - expected.expect(NullPointerException.class); - expected.expectMessage("execution context"); - new InjectionEnabledJob().execute(null); - } - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without job detail. - * - * @throws JobExecutionException - */ - @Test - public void testExecuteWithoutJobDetail() throws JobExecutionException - { - expected.expect(NullPointerException.class); - expected.expectMessage("detail"); - new InjectionEnabledJob().execute(jec); - } - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without data map. - * - * @throws JobExecutionException - */ - @Test - public void testExecuteWithoutDataMap() throws JobExecutionException - { - when(jec.getJobDetail()).thenReturn(detail); - expected.expect(NullPointerException.class); - expected.expectMessage("data map"); - new InjectionEnabledJob().execute(jec); - } - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without injector. - * - * @throws JobExecutionException - */ - @Test - public void testExecuteWithoutInjector() throws JobExecutionException - { - when(jec.getJobDetail()).thenReturn(detail); - when(detail.getJobDataMap()).thenReturn(dataMap); - expected.expect(NullPointerException.class); - expected.expectMessage("injector"); - new InjectionEnabledJob().execute(jec); - } - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without runnable. - * - * @throws JobExecutionException - */ - @Test - public void testExecuteWithoutRunnable() throws JobExecutionException - { - when(jec.getJobDetail()).thenReturn(detail); - when(detail.getJobDataMap()).thenReturn(dataMap); - when(dataMap.get(Injector.class.getName())).thenReturn(injector); - expected.expect(JobExecutionException.class); - expected.expectMessage("runnable"); - new InjectionEnabledJob().execute(jec); - } - - /** - * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)}. - * - * @throws JobExecutionException - */ - @Test - public void testExecute() throws JobExecutionException - { - when(jec.getJobDetail()).thenReturn(detail); - when(detail.getJobDataMap()).thenReturn(dataMap); - when(dataMap.get(Injector.class.getName())).thenReturn(injector); - when(dataMap.get(Runnable.class.getName())).thenReturn(runnable); - when(injector.getInstance(AdministrationContext.class)).thenReturn(context); - new InjectionEnabledJob().execute(jec); - verify(context).runAsAdmin(Mockito.any(PrivilegedAction.class)); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/PrivilegedRunnableFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/PrivilegedRunnableFactoryTest.java new file mode 100644 index 0000000000..66c87c5a37 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/PrivilegedRunnableFactoryTest.java @@ -0,0 +1,53 @@ +package sonia.scm.schedule; + +import com.google.inject.util.Providers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.web.security.AdministrationContext; +import sonia.scm.web.security.PrivilegedAction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +@ExtendWith(MockitoExtension.class) +class PrivilegedRunnableFactoryTest { + + @Mock + private AdministrationContext administrationContext; + + @InjectMocks + private PrivilegedRunnableFactory runnableFactory; + + @Test + void shouldRunAsPrivilegedAction() { + doAnswer((ic) -> { + PrivilegedAction action = ic.getArgument(0); + action.run(); + return null; + }).when(administrationContext).runAsAdmin(any(PrivilegedAction.class)); + + RemindingRunnable runnable = new RemindingRunnable(); + + Runnable action = runnableFactory.create(Providers.of(runnable)); + assertThat(action).isNotExactlyInstanceOf(RemindingRunnable.class); + + assertThat(runnable.run).isFalse(); + action.run(); + assertThat(runnable.run).isTrue(); + } + + private static class RemindingRunnable implements Runnable { + + private boolean run = false; + + @Override + public void run() { + run = true; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java deleted file mode 100644 index cd272ee7ba..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import com.google.inject.Injector; -import com.google.inject.Provider; -import java.io.IOException; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.quartz.CronTrigger; -import org.quartz.JobDetail; -import org.quartz.SchedulerException; -import org.quartz.Trigger; - -/** - * Unit tests for {@link QuartzScheduler}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class QuartzSchedulerTest { - - @Mock - private Injector injector; - - @Mock - private org.quartz.Scheduler quartzScheduler; - - private QuartzScheduler scheduler; - - @Before - public void setUp() - { - scheduler = new QuartzScheduler(injector, quartzScheduler); - } - - /** - * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Runnable)}. - * - * @throws SchedulerException - */ - @Test - @SuppressWarnings("unchecked") - public void testSchedule() throws SchedulerException - { - DummyRunnable dr = new DummyRunnable(); - Task task = scheduler.schedule("42 2 * * * ?", dr); - assertNotNull(task); - - ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); - ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); - verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); - - Trigger trigger = triggerCaptor.getValue(); - assertThat(trigger, is(instanceOf(CronTrigger.class))); - CronTrigger cron = (CronTrigger) trigger; - assertEquals("42 2 * * * ?", cron.getCronExpression()); - - JobDetail detail = detailCaptor.getValue(); - assertEquals(InjectionEnabledJob.class, detail.getJobClass()); - Provider runnable = (Provider) detail.getJobDataMap().get(Runnable.class.getName()); - assertNotNull(runnable); - assertSame(dr, runnable.get()); - assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); - } - - /** - * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Class)}. - * - * @throws SchedulerException - */ - @Test - public void testScheduleWithClass() throws SchedulerException - { - scheduler.schedule("42 * * * * ?", DummyRunnable.class); - - verify(injector).getProvider(DummyRunnable.class); - - ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); - ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); - verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); - - Trigger trigger = triggerCaptor.getValue(); - assertThat(trigger, is(instanceOf(CronTrigger.class))); - CronTrigger cron = (CronTrigger) trigger; - assertEquals("42 * * * * ?", cron.getCronExpression()); - - JobDetail detail = detailCaptor.getValue(); - assertEquals(InjectionEnabledJob.class, detail.getJobClass()); - assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); - } - - /** - * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)}. - * - * @throws SchedulerException - */ - @Test - public void testInit() throws SchedulerException - { - when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); - scheduler.init(null); - verify(quartzScheduler).start(); - } - - /** - * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler is already started. - * - * @throws SchedulerException - */ - @Test - public void testInitAlreadyRunning() throws SchedulerException - { - when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); - scheduler.init(null); - verify(quartzScheduler, never()).start(); - } - - /** - * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler throws an exception. - * - * @throws SchedulerException - */ - @Test - @SuppressWarnings("unchecked") - public void testInitException() throws SchedulerException - { - when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); - scheduler.init(null); - verify(quartzScheduler, never()).start(); - } - - /** - * Tests {@link QuartzScheduler#close()}. - * - * @throws IOException - * @throws SchedulerException - */ - @Test - public void testClose() throws IOException, SchedulerException - { - when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); - scheduler.close(); - verify(quartzScheduler).shutdown(); - } - - /** - * Tests {@link QuartzScheduler#close()} when the underlying scheduler is not running. - * - * @throws IOException - * @throws SchedulerException - */ - @Test - public void testCloseNotRunning() throws IOException, SchedulerException - { - when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); - scheduler.close(); - verify(quartzScheduler, never()).shutdown(); - } - - /** - * Tests {@link QuartzScheduler#close()} when the underlying scheduler throws an exception. - * - * @throws IOException - * @throws SchedulerException - */ - @Test - @SuppressWarnings("unchecked") - public void testCloseException() throws IOException, SchedulerException - { - when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); - scheduler.close(); - verify(quartzScheduler, never()).shutdown(); - } - - - public static class DummyRunnable implements Runnable { - - @Override - public void run() - { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java deleted file mode 100644 index baf4c659cc..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/*** - * Copyright (c) 2015, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * https://bitbucket.org/sdorra/scm-manager - * - */ - -package sonia.scm.schedule; - -import org.junit.Test; - -import static org.mockito.Mockito.*; - -import org.junit.Before; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.quartz.JobKey; -import org.quartz.SchedulerException; - -/** - * Unit tests for {@link QuartzTask}. - * - * @author Sebastian Sdorra - */ -@RunWith(MockitoJUnitRunner.class) -public class QuartzTaskTest { - - @Mock - private org.quartz.Scheduler scheduler; - - private final JobKey jobKey = new JobKey("sample"); - - private QuartzTask task; - - @Before - public void setUp(){ - task = new QuartzTask(scheduler, jobKey); - } - - /** - * Tests {@link QuartzTask#cancel()}. - * - * @throws SchedulerException - */ - @Test - public void testCancel() throws SchedulerException - { - task.cancel(); - verify(scheduler).deleteJob(jobKey); - } - - /** - * Tests {@link QuartzTask#cancel()} when the scheduler throws an exception. - * @throws org.quartz.SchedulerException - */ - @SuppressWarnings("unchecked") - @Test(expected = RuntimeException.class) - public void testCancelWithException() throws SchedulerException - { - when(scheduler.deleteJob(jobKey)).thenThrow(SchedulerException.class); - task.cancel(); - } - -}