About Maven

Maven is a build, build-project and package management software. Based on the "pom.xml" files (project descriptors) it auto-downloads everything what is needed on top of a plain maven installation and stores it inside a local maven repository which is located at "%HOMEPATH%\.m2" or "~/.m2".

When used for building, maven can build multi-module projects including aggregated information like this information site, apidocs and project reports along with the resulting application. Also Unit testing frameworks and CI frameworks can easily be integrated.

Maven manages the full build cycle including dependencies and deployment.

About the IDE, IntelliJ IDEA

The basic mavenized package & build-project management functionality is built-into Intellij IDEA even without an installed Maven, as the IDE can use the "pom.xml" files to manage the software project.

The IDE can now also be used to build the project but without a Maven installation it can't build any Maven goals. What can be achieved is building the project using the IDE's own logic which is sophisticated enough to run & debug Unit tests and also the full server.

An opensource version of the IDE that is capable of handling POJOs and Maven projects can be obtained from http://www.jetbrains.com/idea/, however it's a recommendation to use the paid version of the IDE to obtain more advanced support for Frameworks like JPA, Spring and others.

Quick Guide: How to Build

No IDE is required for building the project only. The build just depends on Maven. So the first step is to prepare & install Maven once.

Installing Maven

A maven installation itself is nothing complicated:

  1. Download Maven from http://maven.apache.org/ and unpack the downloaded archive at a location of your choice. E.g. "C:\Projects\DevEnv\Maven".
  2. Open the environment settings of the OS and add 2 new entries to your profile:
    • MAVEN_HOME=\path\to\maven
    • PATH=%PATH%;%MAVEN_HOME%\bin
  3. Open a console window and type "mvn -version". If everything worked well you should see output similar to:
    Apache Maven 2.2.1 (r801777; 2009-08-06 21:16:01+0200)
    Java version: 1.6.0_16
    Java home: C:\Projects\DevEnv\jdk64\jre
    Default locale: de_DE, platform encoding: Cp1252
    OS name: "windows 7" version: "6.1" arch: "amd64" Family: "windows"
    

Building the Access Layer

Once Maven is installed the access layer can be built using the provided "make.bat". If the build fails make sure the build-machine has the correct environment settings and has a public Internet connection.

The correct build sequence is:

cd \Path\To\AccessLayer
make.bat

<<< check build output >>>

cd target\bin-app.dir

<<< collect final application from here >>>

Troubleshooting - Things to check are:

  • Is the environment correctly setup.
  • Does "mvn -version" return a correct version number >= 2.2.1
  • Is an Internet connection available (e.g. check proxy settings if not)
  • Does the build machine provide enough resources:
    • Free RAM >= 1GB
    • Free storage >= 4GB

"How To Build.txt":

-------------------------------------------------------------------------------
>>>>>           Trend Micro - GRID Access Layer - How To Build            <<<<<
-------------------------------------------------------------------------------

About

	The GRID Access Layer (GACL) is a Maven 2.2 based project.

	Building follows in general the standard rules of any Maven 2.2 project as
	it is documented @ http://maven.apache.org/

	As a summary, the prerequisites on a build machine are:
	- An installed JDK >= 1.6 (best 64bit but not required)
	- An installed Maven 2.2 ("mvn -version" returns >= 2.2)


Building

	The easiest way to build the access layer is to use the "make.bat" file
	and build it under Microsoft Windows. However as the build system is
	not bound to a platform, it builds successfully also under Linux or
	Mac using "mvn" directly (see sections below).

	After installing the JDK and Maven run:

	make.bat
	
	Once the build finished, all results can be taken from the folder 
	"target/access-layer-[version]-dist" or one of the archives.

	Note: The folder "target/access-layer-[version]-dist/webapps/doc" contains
	a detailed project documentation (Project site) with more in depth
	information on what the ACL is all about. This documentation is built out
	of sources contained in the "site" directories and only available after
	the build succeeded.

	
	If the build fails, verify that:

	"java -version" returns something similar to:

	    java version "1.6.0_16"
        Java(TM) SE Runtime Environment (build 1.6.0_16-b01)
        Java HotSpot(TM) 64-Bit Server VM (build 14.2-b01, mixed mode)

    "mvn -version" returns something similar to:

        Apache Maven 2.2.1 (r801777; 2009-08-06 21:16:01+0200)
        Java version: 1.6.0_16
        Java home: C:\Projects\DevEnv\jdk64\jre
        Default locale: de_DE, platform encoding: Cp1252
        OS name: "windows 7" version: "6.1" arch: "amd64" Family: "windows"

    The build machine has enough spare resources (RAM > 1024, Disk > 4GB)


Building with "mvn"

	Multi-module Maven projects are setup hierarchically forming a tree
	of modules with dependencies. The root POM is the one located inside
	the root folder of the Access Layer project.

	Previous versions of the ACL required 2 separate build steps to
	generate all dependencies in the right order. Since version 1.1
	this is no longer required.

	1. Build a fresh application assembly (and install the artifacts):
    > mvn clean install

    2. Build only the changed artifacts:
    > mvn install

    3. Quick Build without reports and documentation site (3 x times faster)
    > mvn install -Dskip.site=true

    Note: The default JRE resource settings may not be sufficient when
    building with Maven. It may be required to set the following environment
    variable in the scope before executing a build:

    MAVEN_OPTS=-Xms256m -Xmx512m -XX:MaxPermSize=256m

-------------------------------------------------------------------------------
(C) Trend Micro Inc. 2010

Project Structure explained:

The project is setup as a Maven 2.2 multi-module project following the default directory layout for maven projects with the exception that the "site" folder is not contained below the "src" folder in order to simplify creating a main site.

Below is an overview of the most important files and folder including the 2 main pom.xml files:

.idea/                  - Intellij IDEA Project Files

config/                 - Configuration files and parent poms
    assemblies/         - Assembly descriptors defining the final ACL package layout.
    defaults/           - Contains statically dumped configurations (from Sonar) for
                          code quality analysis tools.

doc/                    - Additional static documentation, data sheet & concept.
server-application/     - The actual modules of the access layer.
    site/               - The main information site (documentation)

site-skin/              - Contains the skin used for building the documentation pages.
target/                 - The build output (may be missing initially and should
                          NEVER be submitted to any VCS systems)
testsuite/              - A collection of modules that are used to develop CI and
                          synthetic (e.g. load) tests for the ACL.
pom.xml                 - The ROOT pom for the Access Layer Project.
AccessLayer.iml         - The Intellij IDEA module file for the root module.

makeSite.bat            - Makes the information site.
make.bat                - Makes everything in the correct build order.

Running & Debugging the ACL from within the IDE

When using IntelliJ IDEA >= 8, there are existing run & debug configurations that can be used in order to Run and Debug the ACL completely from within the IDE.

See the following screenshot for a sample Setup:

Run Configuration

These settings will work in any Java IDE, it's not limited to IntelliJ IDEA. The important values to set are:

  • Working directory is set to: "AccessLayer/server/ws-server/implementation".
    If the working directory is set to something else, the Spring Context won't find the Spring Beans defined inside the ACL.
  • Classpath is set to the "ws-server" module's classpath defined inside the "pom.xml".
    Ensure that the classpath is set to what is defined inside the Maven POM for the module. (IntelliJ IDEA does this automatically)
  • Main class is set to: "net.sf.tinyjee.Server".
    The ACL runs within the application server TinyJEE. Using the server's main class starts the application server with an embedded Spring Context that is configured to scan the classpath for annotated Spring Beans.

Notes

  • Important: When the ACL is started it will create a folder called "~tinyjee" (if not existing) inside the configured working path, containing all logs, configs, databases and other related content.
    The ACL uses a database to store contents using the CoreDB schema. By default an embedded HSQL database is automatically used to satisfy this requirement. (see Admin Guide for more details on how to configure another database).
  • As usual when started within the debugger, breakpoints can be set at any location and code can be changed & hot-swapped while the system is up and running (if supported by the used JVM & IDE).
  • Nearly all 3rd-party libraries offer source packages for download inside the maven repository (including the application server).
    It is in general wise to download the sources as it will allow the IDE to offer source navigation, code documentation and debugging inside foreign code. In order to get the sources for all referenced Maven dependencies execute the command mvn dependency:sources or click the button "Maven Projects/Download artifacts" (in IntelliJ IDEA).

    When using IntelliJ IDEA, code navigation is enabled by holding the CTRL key and moving the mouse over the symbols to navigate to. Pressing CTRL-Q will display code help for the symbol under the caret.

  • The ACL supports a couple of commandline parameters (VM parameters) that can be set in order to define what database is used, what file storage is attached or what log level is applied. See Admin Guide for additional information what parameters exist.

Troubleshooting

  • Startup inside the IDE fails with a ClassNotFoundException:
    Make sure you compile the project once before starting it for the first time. It may be required to execute mvn install in order to make sure all resources are compiled, 3rd party libraries are all downloaded and everything is deployed into the right place.
  • When starting the ACL the application server complains that an invalid servlet API is loaded:
    Some IDEs automatically add J2EE / JEE libraries when they discover that the project uses the JEE API. If this happens it's very likely that a library version conflict occurs stopping the ACL from working correctly. Check the classpath for JARs like "javaee.jar" that were added by the IDE and are not defined inside pom.xml. Remove all occurrences of such libraries.
  • The ACL starts but no servlets were registered (= no or not enough entry points are written to stdout):
    Verify the content in stdout, look for Setting system property 'autoScanPackages' to 'com.trendmicro.grid.acl', if the entry is missing or set to something wrong it's very likely that the working path is incorrect. If the working path is correct and the issue still happens, consider settings the system property manually (VM parameters).
  • After starting the ACL the changes that were made are not visible or breakpoints are not stopping at the expected location:
    This is very likely an update problem. The code that is running inside the JVM is not the same as the code that is shown inside the IDE. Resolve this problem by executing mvn clean install and if this doesn't work try "Rebuild Project" from within the IDE.

Automated Testing using JUnit

Maven and Unit Tests

Maven 2 has built-in support for various test frameworks including JUnit4. Built-in support means that a special source path is defined that is used to host the test classes and a test runner exists that can auto-detect test classes and execute them during a build.

Attention: Failing a unit test breaks the build by default. Please run unit tests locally using mvn test before submitting changes to perforce.

Developing Ordinary Unit Tests

Unit tests for this project are following the standard of JUnit 4.8 (The JUnit version is defined inside "config/defaults/pom.xml") and must be placed inside the source path for test classes as defined by Maven 2.

In order to create and run Unit tests follow the standard tutorials & documentation for JUnit 4 and Maven:

Creating New Unit Test Classes

When using a Maven 2 aware IDE, tests classes can usually be auto-created using the IDEs JUnit support. Within IntelliJ IDEA, test can be generated by placing the caret on the name of the class to test and pressing ALT-RETURN to show the intention actions and select Create Test afterwards.

Please take care to create JUnit4 tests (JUnit3 is not compatible) inside the "test" path. In addition test classes should be named with the keyword "Test" at the end to make them easier to discover.

Note: The name of the test class has no effect on whether it's executed or not, the build system will execute all public, non-abstract classes that exist inside the "test" path.

A standard test-class for the class "com.trendmicro.grid.MySampleAction" looks like:

package com.trendmicro.grid;

import org.junit.*;
import static org.junit.Assert.*;

/**
 * TODO: Create Description.
 *
 * @author Author, 2010-08-12
 * @version 1.0
 */
public class MySampleActionTest {

        MySampleAction action;

        @Before
        public void setUp() {
                action = new MySampleAction();
        }

        @Test
        public void testDoSomethingDoesSomething() {
                fail("Test is missing");
        }
}

Debugging Unit Tests

Unit tests can easily be run or debugged from within the IDE using a standard JUnit run & debug configuration. (From within IntelliJ IDEA it's enough to right click a test class and create a new "Test-Run Configuration" in order to run the test)

Developing Integration & Regression Tests

The used application server TinyJEE offers a special test runner that can be used in conjunction with JUnit4 allowing to execute tests in a fully functional server environment.

Using this feature allows developing tests that can simulate all use cases that external modules may be raising against the access layer (including SOAP & REST calls) and effectively it allows to:

  • Create integration tests:
    Tests that use custom code to simulate the ACL usage or better test classes that use the original modules to initiate a call to the ACL.
  • Create regression tests:
    Tests that validate that a detected bug is first reproduced, then fixed and finally ensured that it remains fixed. (See package "com.trendmicro.grid.acl.l0.regressiontest" inside the module "ws-server" for examples)

Creating regression or integration tests

In order to simplify the process and ensure the system is running with correct settings, this type of tests should be exclusively created inside the module "ws-server" and the should be placed in the packages: "com.trendmicro.grid.acl.l0.integrationtest" or "com.trendmicro.grid.acl.l0.regressiontest".

In addition the tests must either directly or indirectly derive from the abstract base class "com.trendmicro.grid.acl.l0.AbstractServiceTestBase":

package com.trendmicro.grid.acl.l0;

import ch.qos.logback.classic.Level;
import com.trendmicro.grid.acl.commons.Hex;
import com.trendmicro.grid.acl.commons.Paths;
import com.trendmicro.grid.acl.commons.Utils;
import com.trendmicro.grid.acl.commons.XmlSerializer;
import com.trendmicro.grid.acl.ds.RepositoriesContext;
import com.trendmicro.grid.acl.ds.RepositorySelector;
import com.trendmicro.grid.acl.l0.client.PublicAuthenticationServiceClient;
import com.trendmicro.grid.acl.l0.datatypes.ProcessPackageDataSet;
import com.trendmicro.grid.acl.metadata.ValidationContext;
import net.sf.tinyjee.spring.test.TinyJEESpringJUnit4Runner;
import net.sf.tinyjee.streams.ByteBufferOutputStream;
import net.sf.tinyjee.streams.PositionInputStream;
import net.sf.tinyjee.test.Property;
import net.sf.tinyjee.test.TestConfiguration;
import net.sf.tinyjee.test.TinyJEEJUnit4Runner;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static org.junit.Assert.assertTrue;

/**
 * Is a base class for all test classes that need to run inside a full
 * application server environment.
 *
 * @author juergen_kellerer, 2010-05-14
 * @version 1.0
 */
@ContextConfiguration
@RunWith(TinyJEESpringJUnit4Runner.class)
@TestConfiguration(
		value = {
				@Property(name = "bitronix.tm.disableJmx", value = "true"),

				@Property(name = "gacl.3rdcache.is.synchronous", value = "true"),
				/*@Property(name = "gacl.3rdcache.enable-persistence", value = "true"),*/

				// required for Case335Test
				@Property(name = "gacl.accept.results.on.final.jobs", value = "true")
		},
		// Frameworks like hibernate, etc. are set to WARN level. Switch to DEBUG only for temporary purposes.
		logLevelFrameworks = "WARN", logLevel = "WARN")
public abstract class AbstractServiceTestBase {

	@Resource
	PublicAuthenticationServiceClient authenticationServiceClient;

	PublicAuthenticationService authenticationService;

	static Random random = new Random();

	static PrintWriter timedRunsLog;

	static {
		try {
			timedRunsLog = new PrintWriter(new FileOutputStream("~timed-test-runs.log", true), true);
			timedRunsLog.println("UnitTests Started: " + new Date());
		} catch (FileNotFoundException e) {
			throw new RuntimeException();
		}
	}

	private static File dummyFileStore;

	@BeforeClass
	public static void setLogLevelToDebug() {
		// Note: Using DEBUG log to ensure log calls are performed and will not fail when enabled in a production environment.
		final ch.qos.logback.classic.Logger logger =
				(ch.qos.logback.classic.Logger) LoggerFactory.getILoggerFactory().getLogger("com.trendmicro");
		logger.setLevel(Level.DEBUG);
	}

	@Before
	public void login() throws Exception {
		authenticationService = authenticationServiceClient.getLocalPort();
		authenticationService.authenticate("okuser", "okpassword");
	}

	@After
	public void logout() throws Exception {
		authenticationService.logout();
	}

	/**
	 * Returns a random number generator.
	 *
	 * @return a random number generator.
	 */
	public static Random getRandom() {
		return random;
	}

	/**
	 * Returns the path to the dummy file store.
	 *
	 * @return the path to the dummy file store.
	 */
	public static File getDummyFileStore() {
		if (dummyFileStore == null) {
			dummyFileStore = new File(Paths.getResourcePath(), "/dummy-file-repository");
			if (!dummyFileStore.isDirectory())
				dummyFileStore.mkdirs();
		}
		return dummyFileStore;
	}

	/**
	 * Returns a file from the dummy file repository.
	 *
	 * @param sha1   the sha1 hash of the file to get.
	 * @param create whether the file should be created or just returned.
	 * @return a file from the dummy file repository.
	 */
	public static File getFileFromDummyFileStore(byte[] sha1, boolean create) {
		final File dummyFile = new File(getDummyFileStore(), Hex.encode(sha1) + ".data");
		if (create) {
			if (dummyFile.exists() && !dummyFile.delete())
				throw new IllegalStateException("Cannot delete file " + dummyFile + ", it's in the way.");
			try {
				if (!dummyFile.createNewFile())
					throw new IllegalStateException("Cannot create file " + dummyFile + ".");
			} catch (IOException e) {
				throw new RuntimeException(dummyFile.toString(), e);
			}
		}
		return dummyFile;
	}

	/**
	 * Prints a measurement tag to stdout which may be used with the Jenkins plugin 'Measurement Plots'.
	 *
	 * @param uniqueName the unique name of the measurement to print.
	 * @param value      the value to print.
	 */
	public static void printMeasurement(String uniqueName, double value) {
		System.out.printf("<measurement><name>%s</name><value>%f</value></measurement>%n", uniqueName, value);
	}

	/**
	 * Returns a byte array with random data of the specified length.
	 *
	 * @param length the length of the random byte array to create.
	 * @return a byte array with random data of the specified length.
	 */
	public static byte[] createTestData(int length) {
		byte[] b = new byte[length];
		random.nextBytes(b);
		return b;
	}

	/**
	 * Returns the HTTP port number of the running application server instance.
	 *
	 * @return the HTTP port number of the running application server instance.
	 */
	public static int getLocalServerPort() {
		return Integer.getInteger("tinyjee.connection.http.port");
	}

	/**
	 * Returns the HTTPS port number of the running application server instance.
	 *
	 * @return the HTTPS port number of the running application server instance.
	 */
	public static int getLocalSecureServerPort() {
		return Integer.getInteger("tinyjee.connection.https.port");
	}

	/**
	 * Returns a new HTTP url pointing to the specified path on the running
	 * application server instance.
	 *
	 * @param path the path to point the to.
	 * @return a new HTTP url pointing to the specified path.
	 */
	public static URL getLocalHttpURL(String path) {
		try {
			return new URL("http", "127.0.0.1", getLocalServerPort(), path);
		} catch (MalformedURLException e) {
			throw new RuntimeException(e);
		}
	}

	@Resource
	protected RepositoriesContext context;

	/**
	 * Selects the activated repsitory implementations based on a list of
	 * implementation name artifacts.
	 * <p/>
	 * Note: The list is processed from left to right. Selection made earlier
	 * can get overwritten by selection made later.
	 *
	 * @param keys a list of implementation name artifacts to try to select.
	 */
	protected void selectRepositories(String... keys) {
		for (String key : keys) {
			for (RepositorySelector<?> selector : context.getAllSelectors())
				for (String availableKey : selector.getAvailableKeys())
					if (availableKey.contains(key)) {
						selector.setSelectedKey(availableKey);
						break;
					}
		}
	}

	/**
	 * Clears the test database.
	 *
	 * @param em the entity manager to use for executing the statements.
	 */
	protected void clearTestDataBase(EntityManager em) {
		em.createQuery("DELETE FROM PACKAGE_FILES_HISTORY").executeUpdate();
		em.createQuery("DELETE FROM PACKAGE_FILES").executeUpdate();
		em.createQuery("DELETE FROM PACKAGE_HISTORY").executeUpdate();
		em.createQuery("DELETE FROM PACKAGES").executeUpdate();
		em.createQuery("DELETE FROM FILE_CONTENT_HISTORY").executeUpdate();
		em.createQuery("DELETE FROM FILE_CONTENT_STATISTICS").executeUpdate();
		em.createQuery("DELETE FROM FILE_CONTENT_SOURCES").executeUpdate();
		em.createQuery("DELETE FROM FILE_CONTENTS").executeUpdate();
		em.flush();
	}

	/**
	 * Clears the the entity manager (removing any cached entries).
	 *
	 * @param em the entity manager to clear.
	 */
	protected void clearEntityManager(EntityManager em) {
		em.flush();
		em.clear();
		//causes lock timeout
		//em.getEntityManagerFactory().getCache().evictAll();
	}

	/**
	 * Uses HTTP-PUT to send the specified binary data against the specified url.
	 *
	 * @param uploadURL the url to send the data to.
	 * @param data      the data to send.
	 * @throws Exception in case of the operation failed for any reason.
	 */
	protected void putData(URL uploadURL, byte[] data) throws Exception {
		final HttpURLConnection urlConnection = (HttpURLConnection) uploadURL.openConnection();
		urlConnection.setChunkedStreamingMode(512);
		TinyJEEJUnit4Runner.Utils.putContent(urlConnection, new ByteArrayInputStream(data));
	}

	/**
	 * Uses HTTP-GET to get the binary data of the specified url.
	 *
	 * @param downloadURL the url to download.
	 * @return the downloaded data as byte array.
	 * @throws Exception in case of the operation failed for any reason.
	 */
	protected byte[] getData(URL downloadURL) throws Exception {
		return TinyJEEJUnit4Runner.Utils.getContent(downloadURL);
	}

	/**
	 * Clones the given dataset using XML serialization and de-serialization.
	 *
	 * @param other the dataset to clone.
	 * @return the cloned dataset.
	 * @throws Exception in case of the XML serialization failed.
	 */
	protected static ProcessPackageDataSet clone(ProcessPackageDataSet other) throws Exception {
		XmlSerializer<ProcessPackageDataSet> serializer = ProcessPackageDataSet.getXmlSerializer();
		ByteBufferOutputStream buffer = new ByteBufferOutputStream();
		serializer.save(other, buffer);
		return serializer.load(buffer.asInputStream());
	}

	/**
	 * Decodes all process package dataSets from the given zip.
	 *
	 * @param in       The zip to read from.
	 * @param suffixes a set of suffixes identifying the files to read.
	 * @return the decoded process package dataSets.
	 * @throws Exception In case of decoding failed.
	 */
	protected List<ProcessPackageDataSet> readDataSetsFromZip(InputStream in, String... suffixes) throws Exception {
		if (suffixes.length == 0)
			suffixes = new String[]{".xml"};

		List<ProcessPackageDataSet> storedDataSets = new ArrayList<ProcessPackageDataSet>();
		XmlSerializer<ProcessPackageDataSet> serializer = ProcessPackageDataSet.getXmlSerializer();

		ZipEntry e;
		ZipInputStream zIn = new ZipInputStream(in);
		try {
			while ((e = zIn.getNextEntry()) != null) {
				for (String pattern : suffixes) {
					if (e.getName().endsWith(pattern)) {
						storedDataSets.add(serializer.load(new PositionInputStream(zIn, false)));
						break;
					}
				}
			}
		} finally {
			zIn.close();
		}

		return storedDataSets;
	}

	/**
	 * Prints the time the runnable took to complete.
	 *
	 * @param t     the runnable to run.
	 * @param runs  the number of runs to time.
	 * @param title the title describing what the runnable tests.
	 */
	protected void timedTestRun(Runnable t, int runs, String title) {
		long time = System.nanoTime();

		for (int i = 0; i < runs; i++)
			t.run();

		double durationSeconds = Utils.calculateNanoDurationInSeconds(time);
		String line = String.format("%02d runs ; duration %s: '%s'%n", runs, Utils.formatNanoDuration(time), title);

		synchronized (System.out) {
			System.out.print(line);
			timedRunsLog.print(line);
			printMeasurement(title, durationSeconds);
		}
	}

	/**
	 * Runs the specified callable with as many threads concurrently as specified.
	 *
	 * @param threadCount The number of threads to use for running the callable.
	 * @param testCode    The test code to run.
	 *                    Note: The method must return true if the test was successful.
	 *                    Assertion methods are NOT supported.
	 * @throws Throwable In case of the callable was throwing an exception or when the threads got interrupted.
	 */
	protected void runTestWithMultipleThreads(int threadCount, final Callable<Boolean> testCode) throws Throwable {
		runTestWithMultipleThreads(threadCount, testCode, 0);
	}

	/**
	 * Runs the specified callable with as many threads concurrently as specified.
	 *
	 * @param threadCount The number of threads to use for running the callable.
	 * @param testCode    The test code to run.
	 *                    Note: The method must return true if the test was successful.
	 *                    Assertion methods are NOT supported.
	 * @param threadDelay Is the delay to add between the startup of the threads.
	 * @throws Throwable In case of the callable was throwing an exception or when the threads got interrupted.
	 */
	protected void runTestWithMultipleThreads(int threadCount, final Callable<Boolean> testCode,
	                                          long threadDelay) throws Throwable {
		final CountDownLatch startSignal = new CountDownLatch(1), readySignal = new CountDownLatch(threadCount);
		final BlockingQueue<Boolean> results = new ArrayBlockingQueue<Boolean>(threadCount);
		final AtomicReference<Throwable> throwable = new AtomicReference<Throwable>();

		for (int i = 0; i < threadCount; i++) {
			final long delayMS = i * threadDelay;
			new Thread() {
				@Override
				public void run() {
					boolean success = false;
					try {
						// Waiting for a syncronized start signal.
						readySignal.countDown();
						startSignal.await();

						// If we have a fixed delay between threads, apply it now.
						if (delayMS > 0)
							sleep(delayMS);

						// Execute the test code.
						success = testCode.call();
					} catch (Throwable t) {
						t.printStackTrace();
						throwable.set(t);
					} finally {
						try {
							results.put(success);
						} catch (InterruptedException e) {
							interrupt();
						}
					}
				}
			}.start();
		}

		// Waiting for the threads to get initialized.
		readySignal.await();
		Thread.yield();

		// Starting the threads now
		startSignal.countDown();
		for (int i = 0; i < threadCount; i++)
			assertTrue(testCode.toString(), results.take());

		final Throwable t = throwable.get();
		if (t != null)
			throw t;
	}

	/**
	 * Disables the Metadata validation within the Test thread.
	 */
	@Before
	public void disableMetadataValidation() {
		ValidationContext.getInstance().disableValidation();
	}
}

Technical Background

The code that is actually responsible for starting the application server before running the unit tests can be isolated to:

@RunWith(TinyJEESpringJUnit4Runner.class)
@ContextConfiguration({"property:log.level=WARN", "property:log.level.frameworks=ERROR"})

All test classes that are directly or indirectly annotated with these 2 lines will automatically run inside an application server environment. Please note that the server is started only once for the first occurrence of a test class that requires it.

ContextConfiguration allows to set server commandline parameters (like VM Parameters) and has currently one important limitation. It is not possible to use multiple different configurations under one "test" path. An attempt to do so will fail the test execution as the server is started only once and cannot be re-configured when it's already running.

Another note is that the server environment heavily depends on the features that were enabled with the configured classpath (pom dependencies). Therefore using the server environment in other modules than "ws-server" may lead to the effect of having only a partial server environment available.

When the application server is started it follows the same rules as the "Run & Debug" configurations do. Please make sure that you configure the working directory to point to the module's base directory when executing tests from within the IDE. Check the "Troubleshooting" section above for more details.