/*******************************************************************************
* Copyright (c) 2009-2015 CWI
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* * Davy Landman - Davy.Landman@cwi.nl - CWI
*******************************************************************************/
package org.rascalmpl.test.infrastructure;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.rascalmpl.interpreter.Evaluator;
import org.rascalmpl.interpreter.ITestResultListener;
import org.rascalmpl.interpreter.NullRascalMonitor;
import org.rascalmpl.interpreter.TestEvaluator;
import org.rascalmpl.interpreter.env.GlobalEnvironment;
import org.rascalmpl.interpreter.env.ModuleEnvironment;
import org.rascalmpl.interpreter.load.StandardLibraryContributor;
import org.rascalmpl.interpreter.result.AbstractFunction;
import org.rascalmpl.value.ISourceLocation;
import org.rascalmpl.value.IValueFactory;
import org.rascalmpl.values.ValueFactoryFactory;
/**
* Rascal modules can be tested separatly from each other. This runner includes all modules and nested modules and runs them spread over a workpool
* The size of the workppol is limited by to following:
* - 1 worker per 300MB available memory (-Xmx)
* - only as much workers as there are cpu's (-1 to keep room for the GC)
* - a maximum of 4 since after that the cache misses in the interpreter hurt more than we get benefit
* @author Davy Landman
*
*/
public class RascalJUnitParallelRecursiveTestRunner extends Runner {
private final int numberOfWorkers = Math.max(0, Math.min(4,Math.min(Runtime.getRuntime().availableProcessors() - 1, (int)(Runtime.getRuntime().maxMemory()/ 1024*1024*300))));
private final Semaphore importsCompleted = new Semaphore(0);
private final Semaphore waitForRunning = new Semaphore(0);
private final Semaphore workersCompleted = new Semaphore(0);
private final String[] prefixes;
private final Queue<String> modules = new ConcurrentLinkedQueue<>();
private final Queue<Description> descriptions = new ConcurrentLinkedQueue<>();
private final Queue<Consumer<RunNotifier>> results = new ConcurrentLinkedQueue<>();
private final IValueFactory VF = ValueFactoryFactory.getValueFactory();
private Description rootDesc;
private String rootName;
public RascalJUnitParallelRecursiveTestRunner(Class<?> clazz) {
System.out.println("Running parallel test with " + numberOfWorkers + " runners");
System.out.flush();
rootName = clazz.getName();
this.prefixes = clazz.getAnnotation(RecursiveRascalParallelTest.class).value();
}
@Override
public Description getDescription() {
if (rootDesc == null) {
long start = System.nanoTime();
fillModuleWorkList();
long stop = System.nanoTime();
reportTime("Iterating modules", start, stop);
startModuleTesters();
start = System.nanoTime();
rootDesc = Description.createSuiteDescription(rootName);
processIncomingModuleDescriptions(rootDesc);
stop = System.nanoTime();
reportTime("Importing modules, looking for tests", start, stop);
assert descriptions.isEmpty();
}
return rootDesc;
}
@Override
public void run(RunNotifier notifier) {
assert rootDesc != null;
notifier.fireTestRunStarted(rootDesc);
long start = System.nanoTime();
runTests(notifier);
long stop = System.nanoTime();
reportTime("Testing modules", start, stop);
notifier.fireTestRunFinished(new Result());
}
private void reportTime(String job, long start, long stop) {
System.out.println(job + ": " + ((stop-start)/1000_000) + "ms");
System.out.flush();
}
private void processIncomingModuleDescriptions(Description rootDesc) {
int completed = 0;
while (completed < numberOfWorkers) {
try {
if (importsCompleted.tryAcquire(10, TimeUnit.MILLISECONDS)) {
completed++;
}
}
catch (InterruptedException e) {
}
// consume stuff in the current queue
Description newDescription;
while ((newDescription = descriptions.poll()) != null) {
rootDesc.addChild(newDescription);
}
}
}
private void startModuleTesters() {
for (int i = 0; i < numberOfWorkers; i++) {
new ModuleTester("JUnit Rascal Evaluator " + (i + 1)).start();
}
}
private void fillModuleWorkList() {
for (String prefix: prefixes) {
try {
RascalJUnitTestRunner.getRecursiveModuleList(VF.sourceLocation("std", "", "/" + prefix.replaceAll("::", "/")))
.stream()
.map(m -> prefix + "::" + m)
.forEach(m -> modules.add(m));
}
catch (IOException | URISyntaxException e) {
}
}
}
private void runTests(RunNotifier notifier) {
waitForRunning.release(numberOfWorkers);
int completed = 0;
while (completed < numberOfWorkers) {
try {
if (workersCompleted.tryAcquire(10, TimeUnit.MILLISECONDS)) {
completed++;
}
}
catch (InterruptedException e) {
}
Consumer<RunNotifier> newResult;
while ((newResult = results.poll()) != null) {
newResult.accept(notifier);
}
}
assert results.isEmpty();
}
public class ModuleTester extends Thread {
private GlobalEnvironment heap;
private ModuleEnvironment root;
private PrintWriter stderr;
private PrintWriter stdout;
private Evaluator evaluator;
private final List<Description> testModules = new ArrayList<>();
public ModuleTester(String name) {
super(name);
}
@Override
public void run() {
initializeEvaluator();
processModules();
runTests();
}
private void runTests() {
try {
if (waitForRunSignal()) {
for (Description mod: testModules) {
long start = System.nanoTime();
TestEvaluator runner = new TestEvaluator(evaluator, new Listener(mod));
runner.test(mod.getDisplayName());
long stop = System.nanoTime();
long duration = (stop - start) / 1000_000;
if (duration > 10_000) {
// longer that 10s
System.err.println("Testing module " + mod.getClassName() + " took: " + duration + "ms");
System.err.flush();
}
stdout.flush();
stderr.flush();
}
}
}
finally {
workersCompleted.release();
}
}
private boolean waitForRunSignal() {
try {
waitForRunning.acquire();
return true;
}
catch (InterruptedException e) {
return false;
}
}
private void processModules() {
String module;
try {
while ((module = modules.poll()) != null) {
try {
evaluator.doImport(new NullRascalMonitor(), module);
}
catch (Throwable e) {
synchronized(System.err) {
System.err.println("Could not import " + module + " for testing...");
System.err.println(e.getMessage());
e.printStackTrace(System.err);
}
continue;
}
ModuleEnvironment moduleEnv = heap.getModule(module.replaceAll("\\\\",""));
if (!moduleEnv.getTests().isEmpty()) {
Description modDesc = Description.createSuiteDescription(module);
for (AbstractFunction f : moduleEnv.getTests()) {
modDesc.addChild(Description.createTestDescription(getClass(), RascalJUnitTestRunner.computeTestName(f.getName(), f.getAst().getLocation())));
}
descriptions.add(modDesc);
testModules.add(modDesc);
}
}
// let's shuffle them
Collections.shuffle(testModules);
}
finally {
importsCompleted.release();
}
}
private void initializeEvaluator() {
heap = new GlobalEnvironment();
root = heap.addModule(new ModuleEnvironment("___junit_test___", heap));
stderr = new PrintWriter(System.err);
stdout = new PrintWriter(System.out);
evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), stderr, stdout, root, heap);
evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance());
evaluator.getConfiguration().setErrors(true);
}
}
public class Listener implements ITestResultListener {
private final Description module;
public Listener(Description module) {
this.module = module;
}
private Description getDescription(String name, ISourceLocation loc) {
String testName = RascalJUnitTestRunner.computeTestName(name, loc);
for (Description child : module.getChildren()) {
if (child.getMethodName().equals(testName)) {
return child;
}
}
throw new IllegalArgumentException(name + " test was never registered");
}
@Override
public void start(String context, int count) {
// starting a module
}
@Override
public void done() {
// a module was done
}
@Override
public void report(boolean successful, String test, ISourceLocation loc, String message, Throwable exception) {
Description desc = getDescription(test, loc);
results.add(notifier -> {
notifier.fireTestStarted(desc);
if (!successful) {
notifier.fireTestFailure(new Failure(desc, exception != null ? exception : new Exception(message != null ? message : "no message")));
}
else {
notifier.fireTestFinished(desc);
}
});
}
@Override
public void ignored(String test, ISourceLocation loc) {
Description desc = getDescription(test, loc);
results.add(notifier -> notifier.fireTestIgnored(desc));
}
}
}