diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index c31dd05e048..d9ac5ec368d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -15,6 +15,7 @@ body: options: - Core - ActiveMQ + - ArcadeDB - Azure - Cassandra - ChromaDB diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 9b9a06ecf6a..8b0f936639b 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -15,6 +15,7 @@ body: options: - Core - ActiveMQ + - ArcadeDB - Azure - Cassandra - ChromaDB diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index b655b4ac505..9ab49a81e19 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -15,6 +15,7 @@ body: options: - Core - ActiveMQ + - ArcadeDB - Azure - Cassandra - ChromaDB diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 72a6d9110b6..c2107f19d32 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,6 +37,11 @@ updates: schedule: interval: "monthly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/arcadedb" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/azure" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index f4649bd7f99..bc74faccade 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -23,6 +23,10 @@ - changed-files: - any-glob-to-any-file: - modules/activemq/**/* +"modules/arcadedb": + - changed-files: + - any-glob-to-any-file: + - modules/arcadedb/**/* "modules/azure": - changed-files: - any-glob-to-any-file: diff --git a/docs/modules/databases/arcadedb.md b/docs/modules/databases/arcadedb.md new file mode 100644 index 00000000000..9a2765d13ef --- /dev/null +++ b/docs/modules/databases/arcadedb.md @@ -0,0 +1,56 @@ +# ArcadeDB Module + +Testcontainers module for [ArcadeDB](https://hub.docker.com/u/arcadedata) + +## Usage example + +You can start an ArcadeDB container instance from any Java application by using: + + +[Container creation](../../../modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java) inside_block:container + + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" +```groovy +testImplementation "org.testcontainers:arcadedb:25.3.2" +``` +=== "Maven" +```xml + + org.testcontainers + orientdb + 25.3.2 + test + +``` + +!!! hint +Add the following dependencies if you plan to access the Testcontainer: + + === "Gradle" + ```groovy + compile "com.arcadedb:arcadedb-engine:25.3.2" + compile "com.arcadedb:arcadedb-network:25.3.2" + ``` + + === "Maven" + ```xml + + com.arcadedb + arcadedb-engine + 25.7.1 + + + com.arcadedb + arcadedb-network + 25.7.1 + + ``` + + + + diff --git a/modules/arcadedb/build.gradle b/modules/arcadedb/build.gradle new file mode 100644 index 00000000000..8a5ae2abfa1 --- /dev/null +++ b/modules/arcadedb/build.gradle @@ -0,0 +1,17 @@ +description = "Testcontainers :: ArcadeDB" + +dependencies { + api project(":testcontainers") + + api "com.arcadedb:arcadedb-engine:25.3.2" + api "com.arcadedb:arcadedb-network:25.3.2" + testImplementation 'org.assertj:assertj-core:3.27.4' + testImplementation 'org.apache.tinkerpop:gremlin-driver:3.7.3' + testImplementation "com.arcadedb:arcadedb-gremlin:25.3.2" +} + +tasks.japicmp { + classExcludes = [ + "org.testcontainers.containers.ArcadeDBContainer" + ] +} diff --git a/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java b/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java new file mode 100644 index 00000000000..3adfc42bfc9 --- /dev/null +++ b/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java @@ -0,0 +1,185 @@ +package org.testcontainers.containers; + +import com.arcadedb.remote.RemoteDatabase; +import com.arcadedb.remote.RemoteServer; +import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.Getter; +import lombok.NonNull; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Testcontainers implementation for ArcadeDB. + *

+ * Supported image: {@code arcadedb} with JDK version up to 17 + *

+ * Exposed ports: + *

+ */ +public class ArcadeDBContainer extends GenericContainer { + + private static final Logger LOGGER = LoggerFactory.getLogger(ArcadeDBContainer.class); + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("arcadedata/arcadedb"); + + private static final String DEFAULT_SERVER_PASSWORD = "playwithdata"; + + private static final String DEFAULT_DATABASE_NAME = "testcontainers"; + + private static final int DEFAULT_BINARY_PORT = 2424; + + private static final int DEFAULT_HTTP_PORT = 2480; + + @Getter + private String databaseName; + + private String serverPassword; + + private int serverPort; + + private Optional scriptPath = Optional.empty(); + + private RemoteServer remoteServer; + + private RemoteDatabase database; + + public ArcadeDBContainer(@NonNull String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public ArcadeDBContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + serverPort = DEFAULT_HTTP_PORT; + serverPassword = DEFAULT_SERVER_PASSWORD; + databaseName = DEFAULT_DATABASE_NAME; + + waitStrategy = Wait.forLogMessage(".*ArcadeDB Server started.*", 1); + + addExposedPorts(DEFAULT_BINARY_PORT, DEFAULT_HTTP_PORT); + } + + @Override + protected void configure() { + final String javaOpts = String.format("-Darcadedb.server.rootPassword=%s", serverPassword); + + addEnv("JAVA_OPTS", javaOpts); + } + + public synchronized RemoteDatabase getDatabase() { + final String host = getHost(); + final Integer port = getMappedPort(serverPort); + if (remoteServer == null) { + try { + remoteServer = new RemoteServer(host, port, "root", serverPassword); + } catch (Exception e) { + final String msg = String.format( + "Could not connect to server %s:%d with user 'root' due to %s", + host, + port, + e.getMessage() + ); + LOGGER.error(msg, e); + throw new IllegalStateException(msg, e); + } + } + + if (!remoteServer.exists(getDatabaseName())) { + remoteServer.create(getDatabaseName()); + } + + if (database != null && database.isOpen()) { + return database; + } + + try { + database = new RemoteDatabase(host, port, getDatabaseName(), "root", serverPassword); + scriptPath.ifPresent(path -> loadScript(path, database)); + return database; + } catch (Exception e) { + final String msg = String.format( + "Could not connect to database %s on server %s:%d due to %s", + getDatabaseName(), + host, + port, + e.getMessage() + ); + LOGGER.error(msg, e); + throw new IllegalStateException(msg, e); + } + } + + public ArcadeDBContainer withDatabaseName(final String databaseName) { + this.databaseName = databaseName; + return self(); + } + + public ArcadeDBContainer withServerPassword(final String serverPassword) { + this.serverPassword = serverPassword; + return self(); + } + + public ArcadeDBContainer withServerPort(final int serverPort) { + this.serverPort = serverPort; + return self(); + } + + public ArcadeDBContainer withScriptPath(String scriptPath) { + this.scriptPath = Optional.of(scriptPath); + return self(); + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + final String host = getHost(); + final Integer port = getMappedPort(serverPort); + + try { + remoteServer = new RemoteServer(host, port, "root", serverPassword); + } catch (Exception e) { + final String msg = String.format( + "Could not connect to server %s:%d with user 'root' due to %s", + host, + port, + e.getMessage() + ); + LOGGER.error(msg, e); + throw new IllegalStateException(msg, e); + } + } + + private void loadScript(String path, RemoteDatabase db) { + try { + URL resource = getClass().getClassLoader().getResource(path); + + if (resource == null) { + LOGGER.warn("Could not load classpath init script: {}", scriptPath); + throw new RuntimeException( + "Could not load classpath init script: " + scriptPath + ". Resource not found." + ); + } + + String script = IOUtils.toString(resource, StandardCharsets.UTF_8); + + db.command("sqlscript", script); + } catch (IOException e) { + LOGGER.warn("Could not load classpath init script: {}", scriptPath); + throw new RuntimeException("Could not load classpath init script: " + scriptPath, e); + } catch (UnsupportedOperationException e) { + LOGGER.error("Error while executing init script: {}", scriptPath, e); + throw new RuntimeException("Error while executing init script: " + scriptPath, e); + } + } +} diff --git a/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java b/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java new file mode 100644 index 00000000000..d8611d64dcf --- /dev/null +++ b/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java @@ -0,0 +1,59 @@ +package org.testcontainers.containers; + +import com.arcadedb.remote.RemoteDatabase; +import org.junit.Test; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArcadeDBContainerTest { + + private static final DockerImageName ARCADEDB_IMAGE = DockerImageName + .parse("arcadedata/arcadedb:24.4.1") + .asCompatibleSubstituteFor("arcadedb"); + + @Test + public void shouldReturnTheSameSession() { + try ( // container { + ArcadeDBContainer arcadedb = new ArcadeDBContainer("arcadedata/arcadedb:25.3.2") + // } + ) { + arcadedb.start(); + + final RemoteDatabase database = arcadedb.getDatabase(); + final RemoteDatabase database2 = arcadedb.getDatabase(); + + assertThat(database).isSameAs(database2); + } + } + + @Test + public void shouldInitializeWithCommands() { + try (ArcadeDBContainer arcadedb = new ArcadeDBContainer(ARCADEDB_IMAGE)) { + arcadedb.start(); + + final RemoteDatabase db = arcadedb.getDatabase(); + + db.command("sql", "create vertex type Person"); + db.command("sql", "INSERT INTO Person set name='john'"); + db.command("sql", "INSERT INTO Person set name='jane'"); + + assertThat(db.query("sql", "SELECT FROM Person").stream()).hasSize(2); + } + } + + @Test + public void shouldInitializeDatabaseFromScript() { + try ( + ArcadeDBContainer arcadedb = new ArcadeDBContainer(ARCADEDB_IMAGE) + .withScriptPath("initscript.sql") + .withDatabaseName("persons") + ) { + arcadedb.start(); + + final RemoteDatabase database = arcadedb.getDatabase(); + + assertThat(database.query("sql", "SELECT FROM Person").stream()).hasSize(4); + } + } +} diff --git a/modules/arcadedb/src/test/resources/initscript.sql b/modules/arcadedb/src/test/resources/initscript.sql new file mode 100644 index 00000000000..4fc582de942 --- /dev/null +++ b/modules/arcadedb/src/test/resources/initscript.sql @@ -0,0 +1,6 @@ +CREATE VERTEX Type Person; + +INSERT INTO Person set name="john"; +INSERT INTO Person set name="paul"; +INSERT INTO Person set name="luke"; +INSERT INTO Person set name="albert"; diff --git a/modules/arcadedb/src/test/resources/logback-test.xml b/modules/arcadedb/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..d91138d8b74 --- /dev/null +++ b/modules/arcadedb/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + + +