/*
* $Id$
*
* Janus platform is an open-source multiagent platform.
* More details on http://www.janusproject.io
*
* Copyright (C) 2014-2015 Sebastian RODRIGUEZ, Nicolas GAUD, Stéphane GALLAND.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.janusproject.tests.testutils;
import static org.junit.Assert.assertNull;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.inject.Module;
import com.google.inject.util.Modules;
import org.eclipse.xtext.xbase.lib.Functions.Function1;
import org.junit.Rule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import io.janusproject.Boot;
import io.janusproject.kernel.Kernel;
import io.janusproject.modules.StandardJanusPlatformModule;
import io.janusproject.services.executor.EarlyExitException;
import io.sarl.core.Initialize;
import io.sarl.core.Lifecycle;
import io.sarl.core.Schedules;
import io.sarl.lang.annotation.PerceptGuardEvaluator;
import io.sarl.lang.core.Agent;
import io.sarl.lang.core.BuiltinCapacitiesProvider;
import io.sarl.tests.api.Nullable;
/**
* Abstract class for creating unit tests that needs to launch a Janus instance.
*
* @param <S> - the type of the service.
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
@SuppressWarnings("all")
public abstract class AbstractJanusRunTest extends AbstractJanusTest {
/** Standard timeout in seconds.
*
* @see #EXTRA_TIMEOUT
* @see #NO_TIMEOUT
*/
public static final int STANDARD_TIMEOUT = 40;
/** Extra timeout in seconds.
*
* @see #STANDARD_TIMEOUT
* @see #NO_TIMEOUT
*/
public static final int EXTRA_TIMEOUT = 240;
/** No timeout.
*
* @see #STANDARD_TIMEOUT
* @see #EXTRA_TIMEOUT
*/
public static final int NO_TIMEOUT = -1;
/**
* Reference to the instance of the Janus kernel.
*/
protected Kernel janusKernel;
@Nullable
private List<Object> results;
@Rule
public TestWatcher janusRunWatcher = new TestWatcher() {
@Override
protected void starting(Description description) {
JanusRun skipRun = description.getAnnotation(JanusRun.class);
if (skipRun != null) {
try {
runJanus(skipRun.agent(), skipRun.enableLogging());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
protected void finished(Description description) {
if (AbstractJanusRunTest.this.janusKernel != null) {
AbstractJanusRunTest.this.janusKernel = null;
}
}
};
/**
* Replies the number of results provided by the ran platform.
*
* @return the number of results.
*/
protected int getNumberOfResults() {
return this.results.size();
}
/**
* Test if the number of results provided by the Janus platform is equal to the given number.
*
* @param expected - the expected number of results.
*/
protected void assertNumberOfResults(int expected) {
assertEquals("Invalid number of results provided by the platform.", expected, this.results.size());
}
/**
* Replies result at the given index of the run of the agent.
*
* @param type - the type of the result.
* @param index - the index of the result.
* @return the value; or <code>null</code> if no result.
*/
protected <T> T getResult(Class<T> type, int index) {
if (this.results != null) {
try {
return type.cast(this.results.get(index));
} catch (Throwable exception) {
//
}
}
return null;
}
/**
* Replies result at the given index of the run of the agent.
* @return the results.
*/
protected List<Object> getResults() {
if (this.results != null) {
return Collections.unmodifiableList(this.results);
}
return Collections.emptyList();
}
/**
* Replies the initialization parameters for the agents.
* @return the parameters.
*/
protected Object[] getAgentInitializationParameters() {
return new Object[] {
this.results,
};
}
/**
* Replies the index of the first result of the given type.
*
* @param type - the type of the result.
* @return the index; or <code>-1</code> if not found.
*/
protected int indexOfResult(Class<?> type) {
return indexOfResult(type, 0);
}
/**
* Replies the index of the first result of the given type starting at the given index.
*
* @param type - the type of the result.
* @param fromIndex - the start index.
* @return the index; or <code>-1</code> if not found.
*/
protected int indexOfResult(Class<?> type, int fromIndex) {
if (this.results != null) {
try {
for (int i = fromIndex; i < this.results.size(); ++i) {
Object r = this.results.get(i);
if (type.isInstance(r)) {
return i;
}
}
} catch (Throwable exception) {
//
}
}
return -1;
}
/**
* Start the Janus platform offline.
*
* This function has no timeout for the end of the run.
*
* @param type - the type of the agent to launch at start-up.
* @param enableLogging - indicates if the logging is enable or not.
* @throws Exception - if the kernel cannot be launched.
*/
protected void runJanus(Class<? extends TestingAgent> type, boolean enableLogging) throws Exception {
runJanus(type, enableLogging, true, NO_TIMEOUT);
}
/**
* Start the Janus platform offline with logging enabled.
*
* This function enables logging and has no timeout for the end of the run.
*
* @param type - the type of the agent to launch at start-up.
* @return the kernel.
* @throws Exception - if the kernel cannot be launched.
*/
protected void runJanus(Class<? extends TestingAgent> type) throws Exception {
runJanus(type, true, true, NO_TIMEOUT);
}
/**
* Start the Janus platform.
*
* @param type - the type of the agent to launch at start-up.
* @param enableLogging - indicates if the logging is enable or not.
* @param offline - indicates if the Janus platform is offline
* @param timeout - the maximum waiting time in seconds, or <code>-1</code> to ignore the timeout.
* See {@link #STANDARD_TIMEOUT}, {@link #EXTRA_TIMEOUT} or {@link #NO_TIMEOUT}.
* @throws Exception - if the kernel cannot be launched.
*/
protected void runJanus(Class<? extends TestingAgent> type, boolean enableLogging, boolean offline, int timeout)
throws Exception {
setupTheJanusKernel(type, enableLogging, offline);
waitForTheKernel(timeout);
}
/**
* Set-up the Janus platform.
*
* @param type - the type of the agent to launch at start-up.
* @param enableLogging - indicates if the logging is enable or not.
* @param offline - indicates if the Janus platform is offline
* @return the kernel.
* @throws Exception - if the kernel cannot be launched.
*/
protected Kernel setupTheJanusKernel(Class<? extends TestingAgent> type, boolean enableLogging, boolean offline)
throws Exception {
assertNull("Janus already launched.", this.janusKernel);
Module module = new StandardJanusPlatformModule();
Boot.setConsoleLogger(new PrintStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
//
}
@Override
public void write(byte[] b) throws IOException {
//
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
//
}
}));
this.results = new ArrayList<>();
if (!enableLogging) {
module = Modules.override(new StandardJanusPlatformModule()).with(new NoLogTestingModule());
} else {
module = Modules.override(new StandardJanusPlatformModule()).with(new ErrorLogTestingModule(this.results));
}
Boot.setOffline(offline);
this.janusKernel = Boot.startJanusWithModule(module, type, getAgentInitializationParameters());
Logger current = this.janusKernel.getLogger();
while (current.getParent() != null && current.getParent() != current) {
current = current.getParent();
}
if (current != null) {
current.setLevel(enableLogging ? Level.SEVERE : Level.OFF);
}
return this.janusKernel;
}
/**
* Wait for the end of the Janus platform.
*
* @param timeout - the maximum waiting time in seconds, or <code>-1</code> to ignore the timeout.
* See {@link #STANDARD_TIMEOUT}, {@link #EXTRA_TIMEOUT} or {@link #NO_TIMEOUT}.
* @throws Exception - if the kernel cannot be launched.
*/
public void waitForTheKernel(int timeout) throws Exception {
long endTime;
if (timeout >= 0) {
endTime = System.currentTimeMillis() + timeout * 1000;
} else {
endTime = -1;
}
boolean isJanusRunning = this.janusKernel.isRunning();
while (isJanusRunning && (endTime == -1 || System.currentTimeMillis() <= endTime)) {
isJanusRunning = this.janusKernel.isRunning();
Thread.yield();
}
Boot.setConsoleLogger(null);
if (isJanusRunning) {
throw new TimeoutException();
}
}
/**
* Wait for the end of the Janus platform.
*
* @param timeout - the maximum waiting time in seconds, or <code>-1</code> to ignore the timeout.
* See {@link #STANDARD_TIMEOUT}, {@link #EXTRA_TIMEOUT} or {@link #NO_TIMEOUT}.
* @param predicate the predicate to use as stop condition.
* @throws Exception - if the kernel cannot be launched.
*/
public void waitForTheKernel(int timeout, Function1<List<Object>, Boolean> predicate) throws Exception {
long endTime;
if (timeout >= 0) {
endTime = System.currentTimeMillis() + timeout * 1000;
} else {
endTime = -1;
}
boolean isJanusRunning = this.janusKernel.isRunning();
while (isJanusRunning && (endTime == -1 || System.currentTimeMillis() <= endTime)) {
isJanusRunning = this.janusKernel.isRunning() || !(predicate.apply(this.results));
Thread.yield();
}
Boot.setConsoleLogger(null);
if (isJanusRunning) {
throw new TimeoutException();
}
}
/**
* Interface that permits to mark a method that is manually launching the Janus.
*
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
protected @interface JanusRun {
/**
* The type of the agent to launch.
*
* @return the type of the agent to launch.
*/
Class<? extends TestingAgent> agent();
/**
* Indicates if the logging is enabled.
*
* @return <code>true</code> if the logging is enabled; <code>false</code> otherwise.
*/
boolean enableLogging() default false;
}
/**
* Abstract implementation of an agent that is used for testing Janus
*
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*/
protected static abstract class TestingAgent extends Agent {
private List<Object> results;
private Object[] initializationParameters;
/**
* @param provider - the provider of builtin capacities.
* @param parentID - the identifier of the parent's agent.
* @param agentID - the identifier of the agent.
*/
public TestingAgent(BuiltinCapacitiesProvider provider, UUID parentID, UUID agentID) {
super(provider, parentID, agentID);
}
/**
* @param parentID - the identifier of the parent's agent.
* @param agentID - the identifier of the agent.
*/
public TestingAgent(UUID parentID, UUID agentID) {
super(parentID, agentID);
}
/**
* Add a result.
*
* @param result - the result.
*/
protected synchronized void addResult(Object result) {
this.results.add(result);
}
/**
* Replies the number of results provided by the ran platform.
*
* @return the number of results.
*/
protected int getNumberOfResults() {
return this.results.size();
}
/**
* Add a result.
*
* @param result - the result.
*/
protected synchronized void addResults(Collection<?> results) {
this.results.addAll(results);
}
/**
* Replies result at the given index of the run of the agent.
* @return the results.
*/
protected synchronized List<Object> getResults() {
if (this.results != null) {
return Collections.unmodifiableList(this.results);
}
return Collections.emptyList();
}
/** Replies the initialization parameters of the agents.
*
* @return the initialization parameters.
*/
protected Object[] getAgentInitializationParameters() {
return this.initializationParameters;
}
@PerceptGuardEvaluator
private void $guardEvaluator$Initialize(final Initialize occurrence, final Collection<Runnable> ___SARLlocal_runnableCollection) {
assert occurrence != null;
assert ___SARLlocal_runnableCollection != null;
___SARLlocal_runnableCollection.add(() -> $behaviorUnit$Initialize$0(occurrence));
}
/**
* Invoked at the start of the agent.
*
* @param occurrence - the initialization event.
*/
private void $behaviorUnit$Initialize$0(final Initialize occurrence) {
this.initializationParameters = occurrence.parameters;
this.results = (List<Object>) occurrence.parameters[0];
try {
if (runAgentTest()) {
getSkill(Schedules.class).in(1000, (it) -> forceKillMe());
}
} catch (Throwable exception) {
if (!(exception instanceof EarlyExitException)) {
addResult(exception);
}
throw exception;
}
}
protected void forceKillMe() {
getSkill(Lifecycle.class).killMe();
}
/**
* Invoked to run the unit test. This function is invoked at agent initialization
*
* @return <code>true</code> for killing the agent during its initialization.
*/
protected abstract boolean runAgentTest();
}
}