All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.snice.testing.runtime.Snice Maven / Gradle / Ivy

package io.snice.testing.runtime;

import io.snice.testing.core.MessageBuilder;
import io.snice.testing.core.action.ActionBuilder;
import io.snice.testing.core.protocol.Protocol;
import io.snice.testing.core.scenario.Scenario;
import io.snice.testing.core.scenario.Simulation;
import io.snice.testing.core.scenario.SimulationException;
import io.snice.testing.runtime.impl.SniceLocalDevRuntimeProvider;
import io.snice.testing.runtime.spi.SniceRuntimeProvider;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;

/**
 * The entry point into the runtime environment of Snice Testing. The main purpose is simply to
 * parse the command line arguments, potentially process environment variables and to load the
 * proper services, which will configure themselves and provide the actual runtime.
 */
public class Snice {

    private static final String DEFAULT_RUNTIME_PROVIDER = SniceLocalDevRuntimeProvider.class.getName();

    private static final Logger logger = LoggerFactory.getLogger(Snice.class);

    private final SniceRuntime runtime;

    private Snice(final SniceRuntime runtime) {
        this.runtime = runtime;
    }

    public SniceRuntime runtime() {
        return runtime;
    }

    public CompletionStage sync() {
        return runtime.sync();
    }

    public static final String ARG_RUNTIME = "runtime";
    public static final String ARG_SIMULATION = "simulation";

    /**
     * When we start Snice, this is the amount of time we will wait for a simulation to
     * be schedule and start running.
     */
    public static final String ARG_WAIT_FOR_SIMULATION = "wait";

    private static ArgumentParser createArgParser() {
        final var parser = ArgumentParsers.newFor("snice").build();
        parser.addArgument("--" + ARG_RUNTIME)
                .help("The fully-qualified class name of the runtime provider")
                .setDefault(DEFAULT_RUNTIME_PROVIDER);

        parser.addArgument("--" + ARG_WAIT_FOR_SIMULATION)
                .help("The amount of time (in seconds) we will wait for a scenario to be scheduled, after which the container will shut down again")
                .type(Integer.TYPE)
                .setDefault(1);

        parser.addArgument("--" + ARG_SIMULATION)
                .help("The fully-qualified class name of the Simulation to run");

        return parser;
    }

    private static CliArgs parseArgs(final String... args) {
        try {
            final var unknownArgs = new ArrayList();
            final var attrs = new HashMap();
            createArgParser().parseKnownArgs(args, unknownArgs, attrs);
            return new CliArgs(args, new Namespace(attrs), unknownArgs);
        } catch (final ArgumentParserException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Convenience method for kicking off a single action. Mainly used for simple test cases
     * and demos.
     */
    public static CompletionStage run(final ActionBuilder builder) {
        return start(new String[]{}).runtime().run(builder);
    }

    /**
     * Convenience method for kicking off a test with a single message to send. Mainly used for simple test cases
     * and demos.
     */
    public static CompletionStage run(final MessageBuilder... message) {
        return start(new String[]{}).runtime().run(message);
    }


    public static CompletionStage run(final Scenario scenario, final Protocol.Builder... protocols) {
        return start(new String[]{}).runtime().run(scenario, protocols);
    }

    public static CompletionStage run(final Scenario scenario, final List protocols) {
        return start(new String[]{}).runtime().run(scenario, protocols);
    }

    public static  CompletionStage run(final T plan) {
        return start(new String[]{}).runtime().run(plan);
    }

    /**
     * Parse the command line arguments and bootstrap the runtime system. Optionally, load and kick-off
     * any {@link Simulation}s that may have been given on the command line.
     *
     * @param args the command line arguments
     * @return an instance of {@link Snice} with a loaded and started {@link Runtime}. It is guaranteed that
     * the {@link Runtime} has successfully been started before returning. If not, we will bail out.
     * @throws SimulationException.LoadSimulationException if a {@link Simulation} has been specified on the command
     *                                                     line and we are unable to load it (for whatever reason),
     *                                                     we'll bail out and the system will grind to a halt.
     */
    public static Snice start(final String... args) throws SimulationException.LoadSimulationException {
        final var cli = parseArgs(args);
        final var simulationMaybe = loadSimulation(cli);

        final var runtimeProviderClass = cli.namespace().get(ARG_RUNTIME);
        final var runtime = ServiceLoader.load(SniceRuntimeProvider.class)
                .stream()
                .map(ServiceLoader.Provider::get)
                .filter(p -> runtimeProviderClass.equals(p.getClass().getName()))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Unable to load an appropriate SniceRuntime given " +
                        "the supplied SniceRuntimeProvider of \"" + runtimeProviderClass + "\". " +
                        "Check your CLI argument --runtime to ensure you've specified the fully-qualified " +
                        "java class name of the provider"))
                .create(cli);

        final var startFuture = runtime.start();
        try {
            startFuture.get();
        } catch (final InterruptedException e) {
            e.printStackTrace();
        } catch (final ExecutionException e) {
            e.printStackTrace();
        }

        final var snice = new Snice(runtime);
        simulationMaybe.ifPresent(sim -> snice.runtime().run(sim));
        return snice;
    }

    private static  Optional loadSimulation(final CliArgs cli) throws SimulationException.LoadSimulationException {
        return Optional.ofNullable(cli.namespace().getString(ARG_SIMULATION))
                .or(Snice::findSimulationAutomatically)
                .map(Snice::loadSimulationClass);
    }

    private static  T loadSimulationClass(final String className) {
        try {
            final var clazz = (Class) Class.forName(className);
            final var constructor = clazz.getConstructor(null);
            return constructor.newInstance(null);
        } catch (final ClassCastException e) {
            throw new SimulationException.LoadSimulationException("The class \"" + className + "\" is not of type Simulation", e);
        } catch (final ClassNotFoundException e) {
            throw new SimulationException.LoadSimulationException("Unknown Simulation \"" + className + "\"", e);
        } catch (final NoSuchMethodException e) {
            throw new SimulationException.LoadSimulationException("Missing default public no-arg constructor for Simulation " + className, e);
        } catch (final InvocationTargetException | InstantiationException | IllegalAccessException e) {
            throw new SimulationException.LoadSimulationException("Unable to load Simulation " + className, e);
        }
    }

    /**
     * Overly simplified but good enough for now. It tries to find the main class, assuming it is also
     * a {@link Simulation}, which it currently doesn't check but that'll blow up in the {@link #loadSimulation(CliArgs)}
     * so the user would know.
     *
     * @return
     */
    private static Optional findSimulationAutomatically() {
        final var stack = Thread.currentThread().getStackTrace();
        final var mainMaybe = stack[stack.length - 1];
        final var className = mainMaybe.getClassName();
        try {
            loadSimulationClass(className);
        } catch (final SimulationException.LoadSimulationException e) {
            // doesn't implement Simulation. Ignore and move on.
            return Optional.empty();
        }
        return Optional.of(className);
    }

    public static void main(final String... args) {
        start(args).sync();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy