Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ xdk-webcli = { group = "org.xtclang", name = "lib-webcli" }
xdk-xenia = { group = "org.xtclang", name = "lib-xenia" }
xdk-xml = { group = "org.xtclang", name = "lib-xml" }
xdk-xunit = { group = "org.xtclang", name = "lib-xunit" }
xdk-xunit-db = { group = "org.xtclang", name = "lib-xunit-db" }
xdk-xunit-engine = { group = "org.xtclang", name = "lib-xunit-engine" }

javatools = { group = "org.xtclang", name = "javatools" }
Expand Down
14 changes: 14 additions & 0 deletions lib_xunit_db/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.xtc)
}

dependencies {
xdkJavaTools(libs.javatools)
xtcModule(libs.xdk.ecstasy)
xtcModule(libs.xdk.aggregate)
xtcModule(libs.xdk.collections)
xtcModule(libs.xdk.json)
xtcModule(libs.xdk.jsondb)
xtcModule(libs.xdk.oodb)
xtcModule(libs.xdk.xunit)
}
15 changes: 15 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* The XUnit DB test framework provides functionality to test Ecstasy database applications.
*
* The XUnit DB module is a `TestEngineExtender` so that it is automatically loaded by the XUnit
* test framework when it is imported into a module.
*/
@TestEngineExtender(extensions.createTestEngineExtensions)
module xunit_db.xtclang.org {

package jsondb import jsondb.xtclang.org;
package oodb import oodb.xtclang.org;
package xunit import xunit.xtclang.org;

import xunit.annotations.TestEngineExtender;
}
36 changes: 36 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db/DatabaseTest.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import oodb.RootSchema;

/**
* An annotation that can be applied to a test fixture to configure a test database.
*
* The test database configurations will be used by any tests in the annotated fixture and its
* subclasses unless overridden by another annotated fixture.
*
* @param scope the scope of the database.
* @param templateDir the path to a directory of files to copy to initialize the test database
* @param configs an optional function that returns a database configurations for a given
* schema type
*/
annotation DatabaseTest(DbConfig.Scope scope = Shared,
Directory? templateDir = Null,
ConfigProvider? configs = Null)
implements DbConfigProvider
into Class | Method | Function {

/**
* A function that takes a schema type and optionally returns a database configuration for the
* schema.
*/
typedef function conditional DbConfig (Type<RootSchema>) as ConfigProvider;

@Override
<Schema extends RootSchema> conditional DbConfig configFor(Type<Schema> schema) {
ConfigProvider? configs = this.configs;
if (configs.is(ConfigProvider)) {
if (DbConfig config := configs(schema)) {
return True, config;
}
}
return True, new DbConfig(scope, templateDir);
}
}
37 changes: 37 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db/DbConfig.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import oodb.RootSchema;

/**
* The configuration for a test database schema.
*
* @param Schema the schema type for the database.
* @param scope the scope of the database.
* @param templateDir the directory of files to copy to initialize the test database data
*/
const DbConfig(Scope scope = Shared,
Directory? templateDir = Null) {
/**
* A flag indicating whether the database should be shared between tests.
*/
@Lazy Boolean shared.calc() = scope == Shared;

DbConfig withScope(Scope scope) = new DbConfig(scope, this.templateDir);

DbConfig withTemplateDir(Directory? templateDir) = new DbConfig(this.scope, templateDir);

/**
* An enum representing the scope of a test database.
*/
enum Scope {
/**
* A new database is created that will be shared for all tests below the annotated test
* fixture. For example if the annotated test fixture is a class, all tests in that class
* will share the same database.
*/
Shared,
/**
* A new database is created for each test method. For example if the annotated test fixture
* is a class, each test method in the class will have its own database.
*/
PerTest
}
}
25 changes: 25 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db/DbConfigProvider.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import oodb.RootSchema;

/**
* A provider of test database configurations.
*/
interface DbConfigProvider {
/**
* Return the `DbConfig` for the specified schema type.
*
* @return True iff this database is configured for the specified schema type.
* @return the `DbConfig` to use for the specified schema type
*/
<Schema extends RootSchema> conditional DbConfig configFor(Type<Schema> schema);

static DbConfigProvider Default = new DefaultDbConfigProvider();

static const DefaultDbConfigProvider
implements DbConfigProvider {

@Override
<Schema extends RootSchema> conditional DbConfig configFor(Type<Schema> schema) {
return True, new DbConfig();
}
}
}
134 changes: 134 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db/database/json/JsonDbProvider.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import oodb.RootSchema;

import xunit.UniqueId;

/**
* Provides a connection to a JSON database, creating the database if necessary.
*/
service JsonDbProvider<Schema extends RootSchema>(Type<Schema> type, Module dbModule)
implements Closeable {

/**
* A type representing a connection to a database with a specific schema type.
*/
typedef (oodb.Connection<Schema> + Schema) as Connection;

/**
* The database connections being managed by this provider.
*/
private Map<UniqueId, Connection> connections = new HashMap();

/**
* Create a new database connection.
*/
Connection ensureConnection(UniqueId uniqueId, DbConfig config, Directory dir)
= connections.computeIfAbsent(uniqueId, () -> createConnection(config, dir));

/**
* Close any database connection associated with the specified UniqueId.
*
* @param uniqueId the UniqueId associated to the connections to be closed
*/
void close(UniqueId uniqueId) {
if (Connection connection := connections.get(uniqueId)) {
connections.remove(uniqueId);
close(connection);
}
}

@Override
void close(Exception? e = Null) {
for (Connection connection : connections.values) {
close(connection);
}
connections.clear();
}

/**
* Create a Connection.
*
* @param config the DbConfig to use to configure the database
* @param parentDir the parent directory to put the database directory into
*/
private Connection createConnection(DbConfig config, Directory parentDir) {
@Inject Directory testOutputRoot;
@Inject Directory testOutput;

String dbName = dbModule.simpleName;
Directory buildDir = testOutputRoot.dirFor(dbName).ensure();
Directory dataDir = config.shared ? parentDir.dirFor(dbName) : testOutput.dirFor(dbName);

if (dataDir.exists) {
dataDir.deleteRecursively();
}

Directory? templateDir = config.templateDir;
if (templateDir.is(Directory)) {
copy(templateDir, dataDir);
}

dataDir.ensure();
return jsondb.createConnection(dbModule.simpleName, dataDir, buildDir).as(Connection);
}

/**
* Close a Connections, safely catching and logging any exceptions.
*
* @param connection the Connection to close
*/
private void close(Connection connection) {
try {
connection.close();
} catch (Exception e2) {
@Inject Console console;
console.print($"Exception during closing of {connection}: {e2.message}");
}
}

/**
* Recursively copies the contents of the given directory to the given destination.
*
* @param src the source directory
* @param dest the destination directory
*
* @throws FileAlreadyExists if the destination directory already exists and is not empty
* @throws FileNotFound if the source directory does not exist
*/
private void copy(Directory src, Directory dest) {
import ecstasy.fs.FileAlreadyExists;
import ecstasy.fs.FileNotFound;

if (src.exists) {
if (dest.exists && dest.size != 0) {
throw new FileAlreadyExists(dest.path);
}
dest.ensure();
for (File file : src.files()) {
copy(file, dest.fileFor(file.name));
}
for (Directory dir : src.dirs()) {
copy(dir, dest.dirFor(dir.name));
}
} else {
throw new FileNotFound(src.path);
}
}

/**
* Copies the contents of the given file to the given destination.
*
* @param src the source file
* @param dest the destination file
*
* @throws FileAlreadyExists if the destination file already exists
*/
private void copy(File src, File dest) {
import ecstasy.fs.FileAlreadyExists;
if (src.exists) {
if (dest.exists) {
throw new FileAlreadyExists(dest.path);
}
dest.append(src.contents);
}
}
}
13 changes: 13 additions & 0 deletions lib_xunit_db/src/main/x/xunit_db/extensions.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import xunit.extensions.Extension;

/**
* The extensions package contains XUnit extensions for database testing.
*/
package extensions {
/**
* Create the extensions to be registered with the XUnit test.
*/
static Extension[] createTestEngineExtensions() {
return [new DbInjector()];
}
}
Loading
Loading