// Path: bootique-job/src/test/java/io/bootique/job/fixture/SerialJob1.java
// @SerialJob
// public class SerialJob1 extends Job1 {
//     private static final long DEFAULT_RUNNING_TIME = 10_000_000_000L; // not a real time, but approx. CPU cycles
// 
//     public SerialJob1() {
//         this(DEFAULT_RUNNING_TIME);
//     }
// 
//     public SerialJob1(long runIterations) {
//         super(JobMetadata.build(SerialJob1.class), runIterations);
//     }
// }
// 
// Path: bootique-job/src/main/java/io/bootique/job/runnable/JobOutcome.java
// public enum JobOutcome {
// 
// 	SUCCESS, FAILURE, PARTIAL_SUCCESS, UNKNOWN, SKIPPED
// }
// 
// Path: bootique-job/src/main/java/io/bootique/job/runtime/JobModule.java
// public class JobModule extends ConfigModule {
// 
//     private static final Logger LOGGER = LoggerFactory.getLogger(JobModule.class);
// 
//     static final String JOBS_CONFIG_PREFIX = "jobs";
//     static final String SCHEDULER_CONFIG_PREFIX = "scheduler";
// 
//     public static final String JOB_OPTION = "job";
// 
//     /**
//      * @deprecated since 3.0 as JobMDCManager is no longer implemented as a listener.
//      */
//     // TX ID listener is usually the outermost listener in any app. It is a good idea to order your other listeners
//     // relative to this one , using higher ordering values.
//     @Deprecated
//     public static final int BUSINESS_TX_LISTENER_ORDER = Integer.MIN_VALUE + 800;
// 
//     /**
//      * @deprecated since 3.0 as JobLogListener is no longer exists
//      */
//     // goes inside LOG_LISTENER_ORDER
//     @Deprecated
//     public static final int LOG_LISTENER_ORDER = BUSINESS_TX_LISTENER_ORDER + 200;
// 
//     /**
//      * Returns an instance of {@link JobModuleExtender} used by downstream modules to load custom extensions to the
//      * JobModule. Should be invoked from a downstream Module's "configure" method.
//      *
//      * @param binder DI binder passed to the Module that invokes this method.
//      * @return an instance of {@link JobModuleExtender} that can be used to load custom extensions to the JobModule.
//      */
//     public static JobModuleExtender extend(Binder binder) {
//         return new JobModuleExtender(binder);
//     }
// 
//     @Override
//     protected String defaultConfigPrefix() {
//         // main config sets up Scheduler, so renaming default config prefix
//         return SCHEDULER_CONFIG_PREFIX;
//     }
// 
//     @Override
//     public void configure(Binder binder) {
// 
//         JobModule.extend(binder).initAllExtensions();
// 
//         // binding via provider, to simplify overriding in the "bootique-job-instrumented" module
//         binder.bind(JobRegistry.class).toProvider(JobRegistryProvider.class).inSingletonScope();
// 
//         BQCoreModule.extend(binder).addCommand(ExecCommand.class)
//                 .addOption(OptionMetadata.builder(JOB_OPTION).description("Specifies the name of the job to execute or schedule. "
//                                 + "Used in conjunction with '--execute' or '--schedule' commands. "
//                                 + "Available job names can be viewed using '--list' command.")
//                         .valueRequired("job_name")
//                         .build())
//                 .addCommand(ListCommand.class)
//                 .addCommand(ScheduleCommand.class)
//                 .addValueObjectDescriptor(Cron.class, new ValueObjectDescriptor("percent expression, e.g. '0 0 * * * *'"));
//     }
// 
//     @Provides
//     @Singleton
//     Scheduler createScheduler(
//             LockHandler lockHandler,
//             JobRegistry jobRegistry,
//             ConfigurationFactory configFactory,
//             ShutdownManager shutdownManager) {
// 
//         return config(SchedulerFactory.class, configFactory).createScheduler(lockHandler, jobRegistry, shutdownManager);
//     }
// 
//     @Provides
//     @Singleton
//     LockHandler provideDefaultLockHandler(Map<String, LockHandler> lockHandlers) {
//         switch (lockHandlers.size()) {
//             case 0:
//                 return new LocalLockHandler();
//             case 1:
//                 LOGGER.info("Using '{}' lock handler", lockHandlers.keySet().iterator().next());
//                 return lockHandlers.values().iterator().next();
//             default:
//                 throw new RuntimeException(
//                         "There's more than one LockHandler defined. Can't determine the default: "
//                                 + lockHandlers.keySet());
//         }
//     }
// }


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
import io.bootique.BQRuntime;
import io.bootique.Bootique;
import io.bootique.job.fixture.SerialJob1;
import io.bootique.job.runnable.JobOutcome;
import io.bootique.job.runnable.JobResult;
import io.bootique.job.runtime.JobModule;
import io.bootique.junit5.BQApp;
import io.bootique.junit5.BQTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
        boolean allRun = latch.await(10, TimeUnit.SECONDS);

        // check if any of the job instances hanged up
        if (!allRun) {
            fail("Timeout while waiting for job execution. Still left: " + latch.getCount());
        }

        // verify that all jobs have finished execution without throwing an exception
        assertEquals(count, resultQueue.size());

        // one of the jobs is expected to finish successfully
        // (which one is unknown though, because scheduler also uses an executor internally)
        boolean foundOneSuccessful = false;
        Iterator<JobResult> iter = resultQueue.iterator();
        while (iter.hasNext()) {
            JobResult result = iter.next();
            if (result.getOutcome() == JobOutcome.SUCCESS) {
                iter.remove();
                foundOneSuccessful = true;
                break;
            }
        }

        assertTrue(foundOneSuccessful, "No jobs finished successfully, expected exactly one");

        for (int i = 1; i < count; i++) {
            // we expect all other simultaneous jobs to be skipped by scheduler;
            // otherwise we expect failure, because io.bootique.job.fixture.ExecutableJob
            // throws an exception if run more than once
            JobOutcome actualOutcome = resultQueue.poll().getOutcome();
// Path: bootique-job/src/test/java/io/bootique/job/fixture/SerialJob1.java
// @SerialJob
// public class SerialJob1 extends Job1 {
//     private static final long DEFAULT_RUNNING_TIME = 10_000_000_000L; // not a real time, but approx. CPU cycles
// 
//     public SerialJob1() {
//         this(DEFAULT_RUNNING_TIME);
//     }
// 
//     public SerialJob1(long runIterations) {
//         super(JobMetadata.build(SerialJob1.class), runIterations);
//     }
// }
// 
// Path: bootique-job/src/main/java/io/bootique/job/runnable/JobOutcome.java
// public enum JobOutcome {
// 
// 	SUCCESS, FAILURE, PARTIAL_SUCCESS, UNKNOWN, SKIPPED
// }
// 
// Path: bootique-job/src/main/java/io/bootique/job/runtime/JobModule.java
// public class JobModule extends ConfigModule {
// 
//     private static final Logger LOGGER = LoggerFactory.getLogger(JobModule.class);
// 
//     static final String JOBS_CONFIG_PREFIX = "jobs";
//     static final String SCHEDULER_CONFIG_PREFIX = "scheduler";
// 
//     public static final String JOB_OPTION = "job";
// 
//     /**
//      * @deprecated since 3.0 as JobMDCManager is no longer implemented as a listener.
//      */
//     // TX ID listener is usually the outermost listener in any app. It is a good idea to order your other listeners
//     // relative to this one , using higher ordering values.
//     @Deprecated
//     public static final int BUSINESS_TX_LISTENER_ORDER = Integer.MIN_VALUE + 800;
// 
//     /**
//      * @deprecated since 3.0 as JobLogListener is no longer exists
//      */
//     // goes inside LOG_LISTENER_ORDER
//     @Deprecated
//     public static final int LOG_LISTENER_ORDER = BUSINESS_TX_LISTENER_ORDER + 200;
// 
//     /**
//      * Returns an instance of {@link JobModuleExtender} used by downstream modules to load custom extensions to the
//      * JobModule. Should be invoked from a downstream Module's "configure" method.
//      *
//      * @param binder DI binder passed to the Module that invokes this method.
//      * @return an instance of {@link JobModuleExtender} that can be used to load custom extensions to the JobModule.
//      */
//     public static JobModuleExtender extend(Binder binder) {
//         return new JobModuleExtender(binder);
//     }
// 
//     @Override
//     protected String defaultConfigPrefix() {
//         // main config sets up Scheduler, so renaming default config prefix
//         return SCHEDULER_CONFIG_PREFIX;
//     }
// 
//     @Override
//     public void configure(Binder binder) {
// 
//         JobModule.extend(binder).initAllExtensions();
// 
//         // binding via provider, to simplify overriding in the "bootique-job-instrumented" module
//         binder.bind(JobRegistry.class).toProvider(JobRegistryProvider.class).inSingletonScope();
// 
//         BQCoreModule.extend(binder).addCommand(ExecCommand.class)
//                 .addOption(OptionMetadata.builder(JOB_OPTION).description("Specifies the name of the job to execute or schedule. "
//                                 + "Used in conjunction with '--execute' or '--schedule' commands. "
//                                 + "Available job names can be viewed using '--list' command.")
//                         .valueRequired("job_name")
//                         .build())
//                 .addCommand(ListCommand.class)
//                 .addCommand(ScheduleCommand.class)
//                 .addValueObjectDescriptor(Cron.class, new ValueObjectDescriptor("percent expression, e.g. '0 0 * * * *'"));
//     }
// 
//     @Provides
//     @Singleton
//     Scheduler createScheduler(
//             LockHandler lockHandler,
//             JobRegistry jobRegistry,
//             ConfigurationFactory configFactory,
//             ShutdownManager shutdownManager) {
// 
//         return config(SchedulerFactory.class, configFactory).createScheduler(lockHandler, jobRegistry, shutdownManager);
//     }
// 
//     @Provides
//     @Singleton
//     LockHandler provideDefaultLockHandler(Map<String, LockHandler> lockHandlers) {
//         switch (lockHandlers.size()) {
//             case 0:
//                 return new LocalLockHandler();
//             case 1:
//                 LOGGER.info("Using '{}' lock handler", lockHandlers.keySet().iterator().next());
//                 return lockHandlers.values().iterator().next();
//             default:
//                 throw new RuntimeException(
//                         "There's more than one LockHandler defined. Can't determine the default: "
//                                 + lockHandlers.keySet());
//         }
//     }
// }


// Path: bootique-job/src/test/java/io/bootique/job/scheduler/SchedulerSerialJobIT.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
import io.bootique.BQRuntime;
import io.bootique.Bootique;
import io.bootique.job.fixture.SerialJob1;
import io.bootique.job.runnable.JobOutcome;
import io.bootique.job.runnable.JobResult;
import io.bootique.job.runtime.JobModule;
import io.bootique.junit5.BQApp;
import io.bootique.junit5.BQTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

        boolean allRun = latch.await(10, TimeUnit.SECONDS);

        // check if any of the job instances hanged up
        if (!allRun) {
            fail("Timeout while waiting for job execution. Still left: " + latch.getCount());
        }

        // verify that all jobs have finished execution without throwing an exception
        assertEquals(count, resultQueue.size());

        // one of the jobs is expected to finish successfully
        // (which one is unknown though, because scheduler also uses an executor internally)
        boolean foundOneSuccessful = false;
        Iterator<JobResult> iter = resultQueue.iterator();
        while (iter.hasNext()) {
            JobResult result = iter.next();
            if (result.getOutcome() == JobOutcome.SUCCESS) {
                iter.remove();
                foundOneSuccessful = true;
                break;
            }
        }

        assertTrue(foundOneSuccessful, "No jobs finished successfully, expected exactly one");

        for (int i = 1; i < count; i++) {
            // we expect all other simultaneous jobs to be skipped by scheduler;
            // otherwise we expect failure, because io.bootique.job.fixture.ExecutableJob
            // throws an exception if run more than once
            JobOutcome actualOutcome = resultQueue.poll().getOutcome();
            assertEquals(JobOutcome.SKIPPED, actualOutcome, "Execution #" + (i + 1) + " was not skipped; actual outcome: " + actualOutcome);