package restx.tests;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Files;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import restx.RestxContext;
import restx.config.Settings;
import restx.config.SettingsKey;
import restx.classloader.ClasspathResourceEvent;
import restx.classloader.CompilationFinishedEvent;
import restx.common.UUIDGenerator;
import restx.exceptions.ErrorCode;
import restx.exceptions.ErrorField;
import restx.exceptions.RestxErrors;
import restx.factory.AutoStartable;
import restx.factory.Factory;
import restx.factory.NamedComponent;
import restx.factory.SingletonFactoryMachine;
import restx.server.WebServer;
import restx.server.WebServerSupplier;
import restx.specs.HotReloadRestxSpecRepository;
import restx.specs.RestxSpecLoader;
import restx.specs.RestxSpecRepository;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
/**
* User: xavierhanin
* Date: 7/30/13
* Time: 9:23 PM
*/
public class RestxSpecTestServer {
/**
* A shortcut for new RestxSpecRule("/api", 8076, queryByClass(WebServerSupplier.class), Factory.getInstance())
*/
public static RestxSpecTestServer newInstance() {
Factory f = Factory.getInstance();
return new RestxSpecTestServer("/api", 8076, f.getComponent(WebServerSupplier.class), f);
}
/**
* A shortcut for new RestxSpecRule("/api", 8076, webServerSupplier, Factory.getInstance())
*/
public static RestxSpecTestServer newInstance(WebServerSupplier webServerSupplier) {
return new RestxSpecTestServer("/api", 8076, webServerSupplier, Factory.getInstance());
}
private final String routerPath;
private final int port;
private final WebServerSupplier webServerSupplier;
private final Factory factory;
@Settings
public static interface RunningServerSettings {
@SettingsKey(key = "restx.targetTestsRoot", defaultValue = "target/restx/tests")
String targetTestsRoot();
}
public static class RunningServer {
private static final Logger logger = LoggerFactory.getLogger(RunningServer.class);
private final WebServer server;
private final RestxSpecRunner runner;
private final RestxSpecRepository repository;
private final RestxErrors errors;
private final UUIDGenerator uuidGenerator;
private final ExecutorService testRequestExecutor = newSingleThreadExecutor();
private final ListeningExecutorService testExecutor = listeningDecorator(newFixedThreadPool(4));
private final Path storeLocation;
private final ObjectMapper objectMapper;
private final Map<String, TestResultSummary> lastResults;
private final PrintStream sysout = System.out;
private final PrintStream syserr = System.err;
private final ThreadLocalPrintStream out = new ThreadLocalPrintStream(sysout);
private final ThreadLocalPrintStream err = new ThreadLocalPrintStream(syserr);
public RunningServer(WebServer server, RestxSpecRunner runner, RestxSpecRepository repository,
UUIDGenerator uuidGenerator,
RestxErrors errors,
RunningServerSettings settings) {
this.server = server;
this.runner = runner;
this.repository = repository;
this.uuidGenerator = uuidGenerator;
this.errors = errors;
storeLocation = Paths.get(settings.targetTestsRoot());
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JodaModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
lastResults = loadLastResults();
System.setOut(out);
System.setErr(err);
Factory.LocalMachines.contextLocal(server.getServerId()).addMachine(
new SingletonFactoryMachine<>(0, NamedComponent.of(RunningServer.class, "RunningServer", this)));
}
public WebServer getServer() {
return server;
}
public void stop() throws Exception {
runner.dispose();
server.stop();
System.setOut(sysout);
System.setErr(syserr);
}
public TestRequest submitTestRequest(TestRequest testRequest) {
if (testRequest.getTest().startsWith("specs")) {
final String requestKey = uuidGenerator.doGenerate();
logger.info("queuing test request {}", testRequest);
testRequest.setKey(requestKey);
testRequest.setRequestTime(DateTime.now());
testRequest.setStatus(TestRequest.Status.QUEUED);
store(testRequest);
testRequestExecutor.submit(new Runnable() {
@Override
public void run() {
Optional<TestRequest> requestOptional = getRequestByKey(requestKey);
if (!requestOptional.isPresent()) {
logger.warn("test request not found when trying to execute it: {}", requestKey);
return;
}
Stopwatch stopwatch = Stopwatch.createStarted();
TestRequest testRequest = requestOptional.get();
logger.info("running test request {}", testRequest);
testRequest.setStatus(TestRequest.Status.RUNNING);
store(testRequest);
List<ListenableFuture<String>> futureResultKeys = new ArrayList<>();
String spec = testRequest.getTest();
if (spec.endsWith("*")) {
// clear last results when we run all tests
// this is currently the only way to cleanup last results
if (spec.equals("specs/*")) {
synchronized (lastResults) {
lastResults.clear();
}
}
String prefix = spec.substring(0, spec.length() - 1);
for (String s : repository.findAll()) {
if (s.startsWith(prefix)) {
futureResultKeys.add(testExecutor.submit(runSpecTest(s)));
}
}
} else {
futureResultKeys.add(testExecutor.submit(runSpecTest(spec)));
}
List<String> resultKeys = Futures.getUnchecked(Futures.allAsList(futureResultKeys));
testRequest.setStatus(TestRequest.Status.DONE);
testRequest.setTestResultKey(Joiner.on(",").join(resultKeys));
store(testRequest);
logger.info("completed test request {} in {}: {}", testRequest.getKey(), stopwatch.stop(), testRequest);
}
});
return testRequest;
} else {
throw errors.on(Rules.InvalidTest.class)
.set(Rules.InvalidTest.TEST, testRequest.getTest())
.set(Rules.InvalidTest.DESCRIPTION, "can only run spec test, test field must start with 'specs'")
.raise();
}
}
private Callable<String> runSpecTest(final String spec) {
return new Callable<String>() {
@Override
public String call() throws Exception {
logger.info("spec test {} >> STARTING", spec);
Stopwatch stopWatch = Stopwatch.createStarted();
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
final PrintStream outPrintStream = new PrintStream(outStream);
out.setCurrent(outPrintStream);
ByteArrayOutputStream errStream = new ByteArrayOutputStream();
final PrintStream errPrintStream = new PrintStream(errStream);
err.setCurrent(errPrintStream);
// propagate streams on server
Factory.LocalMachines.threadLocal()
.set("OutPrintStreamComponent", new LocalStreamComponent(out, outPrintStream))
.set("ErrPrintStreamComponent", new LocalStreamComponent(err, errPrintStream))
;
TestResultSummary.Status status = TestResultSummary.Status.ERROR;
long start = System.currentTimeMillis();
try {
runner.runTest(repository.findSpecById(spec).get());
status = TestResultSummary.Status.SUCCESS;
} catch (AssertionError e) {
status = TestResultSummary.Status.FAILURE;
System.err.println(e.getMessage());
} catch (Throwable e) {
e.printStackTrace(System.err);
} finally {
out.clearCurrent();
err.clearCurrent();
}
TestResult result = new TestResult()
.setSummary(new TestResultSummary()
.setKey(uuidGenerator.doGenerate())
.setName(spec)
.setStatus(status)
.setTestDuration(System.currentTimeMillis() - start)
.setTestTime(new DateTime(start))
)
.setStdOut(new String(outStream.toByteArray()))
.setStdErr(new String(errStream.toByteArray()))
;
store(result);
logger.info("spec test {} >> END {}", spec, stopWatch);
return result.getSummary().getKey();
}
};
}
private void store(TestRequest testRequest) {
try {
File file = testRequestFile(testRequest.getKey());
file.getParentFile().mkdirs();
objectMapper.writeValue(file, testRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Optional<TestRequest> getRequestByKey(String key) {
File file = testRequestFile(key);
if (!file.exists()) {
return Optional.absent();
}
try {
TestRequest testRequest = objectMapper.readValue(file, TestRequest.class);
return Optional.of(testRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File testRequestFile(String key) {
return storeLocation.resolve(
"requests/" + key + ".json").toFile();
}
private Map<String,TestResultSummary> loadLastResults() {
Map<String, TestResultSummary> results = new HashMap<>();
File src = lastResultSummariesFile();
if (!src.exists()) {
return results;
}
try {
Collection<TestResultSummary> summaries = objectMapper.readValue(src,
new TypeReference<Collection<TestResultSummary>>() { });
for (TestResultSummary summary : summaries) {
results.put(summary.getName(), summary);
}
} catch (IOException e) {
logger.error("error reading last result summaries file - will start with empty data", e);
results.clear();
}
return results;
}
private void store(TestResult result) {
try {
String key = result.getSummary().getKey();
File resultFile = testResultSummaryFile(key);
resultFile.getParentFile().mkdirs();
Files.write(result.getStdOut(), testResultStdOutFile(key), Charsets.UTF_8);
Files.write(result.getStdErr(), testResultStdErrFile(key), Charsets.UTF_8);
objectMapper.writeValue(resultFile, result.getSummary());
synchronized (lastResults) {
lastResults.put(result.getSummary().getName(), result.getSummary());
testRequestExecutor.submit(new Runnable() {
@Override
public void run() {
try {
objectMapper.writeValue(lastResultSummariesFile(), lastResults.values());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Optional<TestResult> getResultByKey(String key) {
File file = testResultSummaryFile(key);
if (!file.exists()) {
return Optional.absent();
}
try {
TestResult testResult = new TestResult()
.setSummary(objectMapper.readValue(file, TestResultSummary.class))
.setStdOut(Files.toString(testResultStdOutFile(key), Charsets.UTF_8))
.setStdErr(Files.toString(testResultStdErrFile(key), Charsets.UTF_8))
;
return Optional.of(testResult);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File lastResultSummariesFile() {
return storeLocation.resolve(
"results/last.summaries.json").toFile();
}
private File testResultStdOutFile(String key) {
return storeLocation.resolve(
"results/" + key + ".stdout.txt").toFile();
}
private File testResultStdErrFile(String key) {
return storeLocation.resolve(
"results/" + key + ".stderr.txt").toFile();
}
private File testResultSummaryFile(String key) {
return storeLocation.resolve(
"results/" + key + ".summary.json").toFile();
}
public Iterable<TestResultSummary> findCurrentTestResults() {
synchronized (lastResults) {
return new ArrayList<>(lastResults.values());
}
}
public static class Rules {
@ErrorCode(code = "TEST-001", description = "invalid test")
public static enum InvalidTest {
@ErrorField("requested test") TEST,
@ErrorField("description why the test is invalid") DESCRIPTION
}
}
private static class LocalStreamComponent implements AutoStartable, AutoCloseable {
private final ThreadLocalPrintStream localPrintStream;
private final PrintStream stream;
private LocalStreamComponent(ThreadLocalPrintStream localPrintStream, PrintStream stream) {
this.localPrintStream = localPrintStream;
this.stream = stream;
}
@Override
public void start() {
localPrintStream.setCurrent(stream);
}
@Override
public void close() throws Exception {
localPrintStream.clearCurrent();
}
}
}
/**
* Constructs a new RestxSpecRule.
*
* @param routerPath the path at which restx router is mounted. eg '/api'
* @param webServerSupplier a supplier of WebServer, you can use #jettyWebServerSupplier for jetty.
* @param factory the restx Factory to use to find GivenSpecRuleSupplier s when executing the spec.
* This is not used for the server itself.
*/
public RestxSpecTestServer(String routerPath, int port, WebServerSupplier webServerSupplier, Factory factory) {
this.routerPath = routerPath;
this.port = port;
this.webServerSupplier = webServerSupplier;
this.factory = factory;
}
public RunningServer start() throws Exception {
System.setProperty("restx.mode", RestxContext.Modes.INFINIREST);
WebServer server = webServerSupplier.newWebServer(port);
server.start();
RestxSpecLoader specLoader = new RestxSpecLoader(factory);
RestxSpecRunner runner = new RestxSpecRunner(specLoader, routerPath, server.getServerId(), server.baseUrl(), factory);
RestxSpecRepository repository = new HotReloadRestxSpecRepository(specLoader);
final RunningServer runningServer = new RunningServer(
server, runner, repository,
factory.getComponent(UUIDGenerator.class),
factory.getComponent(RestxErrors.class),
factory.getComponent(RunningServerSettings.class));
Factory.getFactory(server.getServerId()).get().getComponent(EventBus.class).register(new Object() {
@Subscribe
public void onCompilationFinished(
CompilationFinishedEvent event) {
runningServer.submitTestRequest(new TestRequest().setTest("specs/*"));
}
@Subscribe
public void onResourceEvent(ClasspathResourceEvent event) {
if (event.getResourcePath().startsWith("specs")) {
runningServer.submitTestRequest(new TestRequest().setTest(event.getResourcePath()));
} else {
runningServer.submitTestRequest(new TestRequest().setTest("specs/*"));
}
}
});
return runningServer;
}
public static void main(String[] args) throws Exception {
RestxSpecTestServer.newInstance().start();
}
}