/*
* Copyright (C) 2014-2015 ULYSSIS VZW
*
* This file is part of i++.
*
* i++ is free software: you can redistribute it and/or modify
* it under the terms of version 3 of the GNU Affero General Public License
* as published by the Free Software Foundation. No other versions apply.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package org.ulyssis.ipp.integrationtests;
import com.google.common.io.Files;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.ulyssis.ipp.config.Config;
import org.ulyssis.ipp.control.CommandDispatcher;
import org.ulyssis.ipp.control.commands.AddTagCommand;
import org.ulyssis.ipp.control.commands.PingCommand;
import org.ulyssis.ipp.control.commands.SetEndTimeCommand;
import org.ulyssis.ipp.control.commands.SetStartTimeCommand;
import org.ulyssis.ipp.processor.Database;
import org.ulyssis.ipp.processor.Processor;
import org.ulyssis.ipp.processor.ProcessorOptions;
import org.ulyssis.ipp.snapshot.Event;
import org.ulyssis.ipp.snapshot.Snapshot;
import org.ulyssis.ipp.status.StatusMessage;
import org.ulyssis.ipp.updates.TagUpdate;
import org.ulyssis.ipp.utils.JedisHelper;
import org.ulyssis.ipp.utils.Serialization;
import org.ulyssis.ipp.TagId;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.Semaphore;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.ulyssis.ipp.processor.Database.ConnectionFlags.READ_WRITE;
import static org.ulyssis.ipp.processor.Database.ConnectionFlags.READ_ONLY;
public class TestWithRedis {
private static Process redisProcess;
private static Jedis jedis;
private BinaryJedisPubSub pubSub;
private Semaphore semaphore;
private Thread newSnapshotThread;
private List<Thread> runningThreads = new ArrayList<>();
public static String connectionURI = System.getProperty("testJDBCURI", "jdbc:h2:mem:ipp");
@BeforeClass
public static void startRedis() throws Exception {
// Place a "redispath" file with the path to Redis in the resources folder
Path redisPathPath = Paths.get("src", "test", "resources", "redispath");
File redisPathFile = redisPathPath.toFile();
Path redisPath = null;
File redisFile = null;
boolean foundRedis = false;
if (redisPathFile.exists() && redisPathFile.isFile() && redisPathFile.canRead()) {
String firstLine = Files.readFirstLine(redisPathFile, StandardCharsets.UTF_8);
firstLine = firstLine.trim();
redisPath = Paths.get(firstLine);
redisFile = redisPath.toFile();
if (redisFile.exists() && redisFile.isFile() && redisFile.canExecute()) {
foundRedis = true;
}
}
String pathEnv = System.getenv("PATH");
String os = System.getProperty("os.name");
String[] paths;
if (os.startsWith("Windows")) {
paths = pathEnv.split(";");
} else {
paths = pathEnv.split(":");
}
for (int i = 0; i < paths.length && !foundRedis; ++i) {
if (os.startsWith("Windows")) {
redisPath = Paths.get(paths[i], "redis-server.exe");
} else {
redisPath = Paths.get(paths[i], "redis-server");
}
redisFile = redisPath.toFile();
if (redisFile.exists() && redisFile.isFile() && redisFile.canExecute()) {
foundRedis = true;
}
}
if (!os.startsWith("Windows")) {
if (redisFile == null || !redisFile.exists() || !redisFile.isFile() || !redisFile.canExecute()) {
redisPath = Paths.get("/usr/bin/redis-server");
redisFile = redisPath.toFile();
}
if (redisFile == null || !redisFile.exists() || !redisFile.isFile() || !redisFile.canExecute()) {
redisPath = Paths.get("/usr/sbin/redis-server");
redisFile = redisPath.toFile();
}
if (redisFile == null || !redisFile.exists() || !redisFile.isFile() || !redisFile.canExecute()) {
redisPath = Paths.get("/usr/local/bin/redis-server");
redisFile = redisPath.toFile();
}
}
if (redisFile == null || !redisFile.exists() || !redisFile.isFile() || !redisFile.canExecute()) {
// Can't start Redis?
System.err.println("Failed to start Redis: no executable file");
return;
}
ProcessBuilder builder = new ProcessBuilder(redisPath.toString(),
"--port", "12345",
"--daemonize", "no",
"--bind", "127.0.0.1",
"--databases", "10",
"--appendonly", "no",
"--loglevel", "debug");
builder.redirectError(ProcessBuilder.Redirect.INHERIT);
builder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
redisProcess = builder.start();
Thread.sleep(2000L);
jedis = JedisHelper.get(URI.create("redis://127.0.0.1:12345"));
selfTest();
}
private static Connection globalConnection;
@BeforeClass
public static void configDb() throws Exception {
Database.setDatabaseURI(URI.create(connectionURI));
if (connectionURI.startsWith("jdbc:h2")) {
globalConnection = Database.createConnection(EnumSet.of(READ_WRITE));
}
try (Connection connection = Database.createConnection(EnumSet.of(READ_WRITE))) {
Database.clearDb(connection);
Database.initDb(connection);
connection.commit();
}
}
@AfterClass
public static void closeConnection() throws Exception {
if (globalConnection != null) {
globalConnection.close();
}
}
@Before
public void clearDb() throws Exception {
try (Connection connection = Database.createConnection(EnumSet.of(READ_WRITE))) {
try (Statement stmt = connection.createStatement()) {
stmt.execute("DELETE FROM \"snapshots\"");
}
try (Statement stmt = connection.createStatement()) {
stmt.execute("DELETE FROM \"tagSeenEvents\"");
}
try (Statement stmt = connection.createStatement()) {
stmt.execute("DELETE FROM \"events\"");
}
connection.commit();
}
}
public static void selfTest() throws Exception {
jedis.set("Bla", "Bloe");
assertThat("Bloe", equalTo(jedis.get("Bla")));
assertThat(1, equalTo(jedis.keys("*").size()));
}
@AfterClass
public static void stopRedis() throws IOException {
jedis.shutdown();
jedis.close();
try {
if (redisProcess.isAlive()) {
Thread.sleep(2000L);
}
if (redisProcess.isAlive()) {
redisProcess.destroy();
}
redisProcess.waitFor();
} catch (InterruptedException ignored) {
}
}
@Before
@After
public void clearRedis() {
jedis.flushAll();
}
@After
public void stopRunningThreads() {
runningThreads.stream().forEach(thread -> {
thread.interrupt();
try {
thread.join(1000L);
// TODO: Cancel test run if thread not dead?
} catch (InterruptedException ignored) {
}
});
runningThreads.clear();
}
@Before
public void startNewSnapshotThread() {
semaphore = new Semaphore(0);
pubSub = new JedisHelper.BinaryCallBackPubSub();
((JedisHelper.BinaryCallBackPubSub) pubSub).addOnMessageListener((channel, message) -> {
try {
StatusMessage msg = Serialization.getJsonMapper().readValue(message, StatusMessage.class);
if (msg.getType() == StatusMessage.MessageType.NEW_SNAPSHOT) {
semaphore.release();
}
} catch (IOException ignored) {
}
});
newSnapshotThread = new Thread(() -> {
try {
Jedis jedis = JedisHelper.get(new URI("redis://127.0.0.1:12345/0"));
jedis.subscribe(pubSub, "status:0".getBytes());
jedis.close();
} catch (URISyntaxException ignored) {
}
});
newSnapshotThread.start();
}
@After
public void endNewSnapshotThread() throws Exception {
pubSub.unsubscribe();
newSnapshotThread.join();
}
private void waitForSnapshot() throws Exception {
semaphore.acquire();
}
private void setConfig(String start, String... rest) {
Config config = Config.fromConfigurationFile(Paths.get(start, rest)).get();
Config.setCurrentConfig(config);
}
private CommandDispatcher spawnDefaultProcessor() throws InterruptedException {
return spawnProcessor("--redis","redis://127.0.0.1:12345/0","--database",connectionURI);
}
private CommandDispatcher spawnProcessor(String... args) throws InterruptedException {
ProcessorOptions options = ProcessorOptions.processorOptionsFromArgs(args).get();
Processor processor = new Processor(options);
Semaphore sem = new Semaphore(0);
processor.addOnStartedCallback(p -> sem.release());
Thread thread = new Thread(processor);
runningThreads.add(thread);
thread.start();
sem.acquire();
Config config = Config.getCurrentConfig();
CommandDispatcher dispatcher = new CommandDispatcher(options.getRedisUri(), config.getControlChannel(), config.getStatusChannel());
Thread dispatcherThread = new Thread(dispatcher);
runningThreads.add(dispatcherThread);
dispatcherThread.start();
// TODO: Find the reason why this sleep is necessary to make it work on single core systems!
Thread.sleep(1000L);
return dispatcher;
}
@Test
public void testPing() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
CommandDispatcher.Result result = dispatcher.send(new PingCommand());
assertThat(result, equalTo(CommandDispatcher.Result.SUCCESS));
}
@Test
public void testAddTag() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
CommandDispatcher.Result result = dispatcher.send(
new AddTagCommand(new TagId("abcd"), 0));
assertThat(result, equalTo(CommandDispatcher.Result.SUCCESS));
try (Connection connection = Database.createConnection(EnumSet.of(READ_ONLY))) {
Optional<Snapshot> snapshot = Snapshot.loadLatest(connection);
assertThat(snapshot.isPresent(), equalTo(true));
assertThat(snapshot.get().getTeamTagMap().tagToTeam("abcd").get(), equalTo(0));
try (Statement statement = connection.createStatement()) {
ResultSet rs = statement.executeQuery("SELECT count(*) FROM \"snapshots\"");
rs.next();
assertThat(rs.getLong(1), equalTo(1L));
}
}
}
@Test
public void testSetStartTime() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
CommandDispatcher.Result result = dispatcher.send(
new SetStartTimeCommand(Instant.EPOCH));
assertThat(result, equalTo(CommandDispatcher.Result.SUCCESS));
try (Connection connection = Database.createConnection(EnumSet.of(READ_ONLY))) {
Optional<Snapshot> latestSnapshot = Snapshot.loadLatest(connection);
assertThat(latestSnapshot.isPresent(), equalTo(true));
assertThat(latestSnapshot.get().getStartTime(), equalTo(Instant.EPOCH));
try (Statement stmt = connection.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM \"snapshots\"");
rs.next();
assertThat(rs.getLong(1), equalTo(1L));
}
}
}
@Test
public void testSetStopTime() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
CommandDispatcher.Result result = dispatcher.send(
new SetEndTimeCommand(Instant.EPOCH));
assertThat(result, equalTo(CommandDispatcher.Result.SUCCESS));
try (Connection connection = Database.createConnection(EnumSet.of(READ_ONLY))) {
Optional<Snapshot> latestSnapshot = Snapshot.loadLatest(connection);
assertThat(latestSnapshot.isPresent(), equalTo(true));
assertThat(latestSnapshot.get().getEndTime(), equalTo(Instant.EPOCH));
try (Statement stmt = connection.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM \"snapshots\"");
rs.next();
assertThat(rs.getLong(1), equalTo(1L));
}
}
}
@Test
public void testSingleTeamNormalLap() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
dispatcher.send(new SetStartTimeCommand(Instant.EPOCH));
waitForSnapshot();
dispatcher.send(new SetEndTimeCommand(Instant.EPOCH.plus(15, ChronoUnit.MINUTES)));
waitForSnapshot();
dispatcher.send(new AddTagCommand(Instant.EPOCH, new TagId("abcd"), 0));
waitForSnapshot();
Jedis readerJedis = JedisHelper.get(new URI("redis://127.0.0.1:12345/1"));
TagUpdate update = new TagUpdate(0, 0, Instant.EPOCH.plus(10, ChronoUnit.SECONDS), new TagId("abcd"));
readerJedis.rpush("updates".getBytes(), Serialization.getJsonMapper().writeValueAsBytes(update));
readerJedis.publish("update:1", "0");
waitForSnapshot();
readerJedis.select(2);
update = new TagUpdate(1, 0, Instant.EPOCH.plus(6, ChronoUnit.MINUTES), new TagId("abcd"));
readerJedis.rpush("updates".getBytes(), Serialization.getJsonMapper().writeValueAsBytes(update));
readerJedis.publish("update:2", "0");
waitForSnapshot();
readerJedis.select(3);
update = new TagUpdate(2, 0, Instant.EPOCH.plus(9, ChronoUnit.MINUTES), new TagId("abcd"));
readerJedis.rpush("updates".getBytes(), Serialization.getJsonMapper().writeValueAsBytes(update));
readerJedis.publish("update:3", "0");
waitForSnapshot();
readerJedis.select(1);
update = new TagUpdate(0, 1, Instant.EPOCH.plus(12, ChronoUnit.MINUTES), new TagId("abcd"));
readerJedis.rpush("updates".getBytes(), Serialization.getJsonMapper().writeValueAsBytes(update));
readerJedis.publish("update:1", "1");
waitForSnapshot();
readerJedis.close();
assertThat(semaphore.availablePermits(), equalTo(0));
try (Connection connection = Database.createConnection(EnumSet.of(READ_ONLY))) {
Optional<Snapshot> snapshot = Snapshot.loadLatest(connection);
assertThat(snapshot.isPresent(), equalTo(true));
assertThat(snapshot.get().getTeamTagMap().tagToTeam("abcd").get(), equalTo(0));
assertThat(snapshot.get().getTeamStates().getStateForTeam(0).get().getTagFragmentCount(), equalTo(3));
assertThat(snapshot.get().getTeamStates().getNbLapsForTeam(0), equalTo(1));
}
}
@Test
public void testOnlyOneEvent() throws Exception {
setConfig("src", "test", "resources", "config1.json");
CommandDispatcher dispatcher = spawnDefaultProcessor();
dispatcher.send(new SetStartTimeCommand(Instant.EPOCH));
waitForSnapshot();
dispatcher.send(new SetStartTimeCommand(Instant.EPOCH.plus(20, ChronoUnit.SECONDS)));
waitForSnapshot();
try (Connection connection = Database.createConnection(EnumSet.of(READ_ONLY))) {
try (Statement stmt = connection.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM \"snapshots\"");
rs.next();
assertThat(rs.getLong(1), equalTo(1L));
}
try (Statement stmt = connection.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM \"events\"");
rs.next();
assertThat(rs.getLong(1), equalTo(2L));
}
List<Event> events = Event.loadAll(connection);
assertThat(events.get(0).isRemoved(), equalTo(true));
Optional<Snapshot> snapshot = Snapshot.loadLatest(connection);
assertThat(snapshot.isPresent(), equalTo(true));
assertThat(snapshot.get().getStartTime(), equalTo(Instant.EPOCH.plus(20, ChronoUnit.SECONDS)));
}
}
}