Skip to content

Commit b9d67b6

Browse files
committed
no-mistakes(review): Scope Kotlin MySQL drop; share C# MySQL container
1 parent 42437da commit b9d67b6

5 files changed

Lines changed: 92 additions & 56 deletions

File tree

‎server/csharp/MetaObjects.IntegrationTests/Api/ApiContractReportConformanceTest.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
namespace MetaObjects.IntegrationTests.Api;
2020

21-
public sealed class ApiContractReportConformanceTest
21+
[Collection(MySqlCollection.Name)]
22+
public sealed class ApiContractReportConformanceTest(MySqlServer mysqlServer)
2223
{
2324
[Theory]
2425
[MemberData(nameof(Scenarios))]
@@ -53,7 +54,7 @@ public async Task Api_contract_report_generated_on_sqlite(string scenarioPath)
5354
public async Task Api_contract_report_generated_on_mysql(string scenarioPath)
5455
{
5556
var scenario = ApiContractScenarioLoader.LoadScenario(scenarioPath);
56-
await using var mysql = await MySqlDatabase.StartAsync();
57+
await using var mysql = await mysqlServer.CreateDatabaseAsync();
5758
await using var server = await ReportGeneratedServerFactory.StartMySqlAsync(mysql);
5859
await server.ApplySeedAsync();
5960
await RunAsync(scenario, server.BaseUrl);

‎server/csharp/MetaObjects.IntegrationTests/QueryScenarioMySqlTests.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313

1414
namespace MetaObjects.IntegrationTests;
1515

16-
public sealed class QueryScenarioMySqlTests
16+
[Collection(MySqlCollection.Name)]
17+
public sealed class QueryScenarioMySqlTests(MySqlServer server)
1718
{
1819
[Theory]
1920
[MemberData(nameof(Scenarios))]
2021
public async Task Report_scenario_on_mysql(string scenarioPath)
2122
{
2223
var scenario = ScenarioLoader.LoadQuery(scenarioPath);
23-
await using var mysql = await MySqlDatabase.StartAsync();
24+
await using var mysql = await server.CreateDatabaseAsync();
2425
await QueryScenarioRunner.RunMySqlAsync(scenario, mysql.ConnectionString);
2526
}
2627

‎server/csharp/MetaObjects.IntegrationTests/Runner/MySqlDatabase.cs‎

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,103 @@
1-
// MySqlDatabase — a fresh, isolated MySQL database for one scenario.
1+
// MySqlServer — one MySQL server shared by the MySQL test collection, and MySqlDatabase — a
2+
// fresh, isolated database on it for one scenario.
23
//
3-
// Two modes, mirroring PostgresContainer:
4-
// 1. Existing server: METAOBJECTS_TEST_MYSQL_URL names a running MySQL as an ADO.NET
5-
// connection string (an admin login). A uniquely-named database is created per
6-
// instance and dropped on dispose.
7-
// 2. Per-container (default): start a mysql:8.4 Testcontainers container.
4+
// The server is, in order:
5+
// 1. Existing server: METAOBJECTS_TEST_MYSQL_URL names a running MySQL as
6+
// mysql://user:pass@host:port/db (an admin login), the form the TypeScript suite reads.
7+
// 2. Otherwise one mysql:8.4 Testcontainers container, started on first use and removed when
8+
// the collection finishes.
9+
// Either way each scenario gets a uniquely-named database, dropped on dispose.
810

911
using MySqlConnector;
1012
using Testcontainers.MySql;
13+
using Xunit;
1114

1215
namespace MetaObjects.IntegrationTests.Runner;
1316

14-
public sealed class MySqlDatabase : IAsyncDisposable
17+
public sealed class MySqlServer : IAsyncLifetime
1518
{
1619
private const string ExistingServerEnv = "METAOBJECTS_TEST_MYSQL_URL";
1720

18-
public string ConnectionString { get; }
21+
private readonly Lazy<Task<string>> _adminConnectionString;
22+
private MySqlContainer? _container; // null until started, and when an existing server is used
1923

20-
private readonly MySqlContainer? _container; // null when an existing server is used
21-
private readonly string? _adminConnectionString;
22-
private readonly string? _createdDb;
24+
public MySqlServer()
25+
{
26+
_adminConnectionString = new Lazy<Task<string>>(StartAsync);
27+
}
2328

24-
private MySqlDatabase(MySqlContainer container)
29+
private async Task<string> StartAsync()
2530
{
26-
_container = container;
27-
ConnectionString = container.GetConnectionString();
31+
var existing = Environment.GetEnvironmentVariable(ExistingServerEnv);
32+
if (!string.IsNullOrWhiteSpace(existing)) return FromUrl(existing);
33+
34+
_container = new MySqlBuilder().WithImage("mysql:8.4").WithUsername("root").Build();
35+
await _container.StartAsync();
36+
return _container.GetConnectionString();
2837
}
2938

30-
private MySqlDatabase(string connectionString, string adminConnectionString, string createdDb)
39+
private static string FromUrl(string url)
3140
{
32-
ConnectionString = connectionString;
33-
_adminConnectionString = adminConnectionString;
34-
_createdDb = createdDb;
41+
var uri = new Uri(url);
42+
var userInfo = uri.UserInfo.Split(':', 2);
43+
var builder = new MySqlConnectionStringBuilder
44+
{
45+
Server = uri.Host,
46+
Port = uri.Port == -1 ? 3306u : (uint)uri.Port,
47+
UserID = Uri.UnescapeDataString(userInfo[0]),
48+
Password = userInfo.Length > 1 ? Uri.UnescapeDataString(userInfo[1]) : "",
49+
};
50+
var database = uri.AbsolutePath.TrimStart('/');
51+
if (database.Length > 0) builder.Database = Uri.UnescapeDataString(database);
52+
return builder.ConnectionString;
3553
}
3654

37-
public static async Task<MySqlDatabase> StartAsync()
55+
/// <summary>A fresh, empty database on this server, dropped when the result is disposed.</summary>
56+
public async Task<MySqlDatabase> CreateDatabaseAsync()
3857
{
39-
var existing = Environment.GetEnvironmentVariable(ExistingServerEnv);
40-
if (!string.IsNullOrWhiteSpace(existing))
58+
var admin = await _adminConnectionString.Value;
59+
var createdDb = "mo_test_cs_" + Guid.NewGuid().ToString("N");
60+
await using (var connection = new MySqlConnection(admin))
4161
{
42-
var createdDb = "mo_test_cs_" + Guid.NewGuid().ToString("N");
43-
await using (var admin = new MySqlConnection(existing))
44-
{
45-
await admin.OpenAsync();
46-
// Generated database name — no user input; safe to inline.
47-
await using var cmd = new MySqlCommand($"CREATE DATABASE `{createdDb}`", admin);
48-
await cmd.ExecuteNonQueryAsync();
49-
}
50-
var scenario = new MySqlConnectionStringBuilder(existing) { Database = createdDb };
51-
return new MySqlDatabase(scenario.ConnectionString, existing, createdDb);
62+
await connection.OpenAsync();
63+
// Generated database name — no user input; safe to inline.
64+
await using var cmd = new MySqlCommand($"CREATE DATABASE `{createdDb}`", connection);
65+
await cmd.ExecuteNonQueryAsync();
5266
}
67+
var scenario = new MySqlConnectionStringBuilder(admin) { Database = createdDb };
68+
return new MySqlDatabase(scenario.ConnectionString, admin, createdDb);
69+
}
70+
71+
public Task InitializeAsync() => Task.CompletedTask;
72+
73+
public async Task DisposeAsync()
74+
{
75+
if (_container is not null) await _container.DisposeAsync();
76+
}
77+
}
78+
79+
[CollectionDefinition(Name)]
80+
public sealed class MySqlCollection : ICollectionFixture<MySqlServer>
81+
{
82+
public const string Name = "MySQL";
83+
}
84+
85+
public sealed class MySqlDatabase : IAsyncDisposable
86+
{
87+
public string ConnectionString { get; }
88+
89+
private readonly string _adminConnectionString;
90+
private readonly string _createdDb;
5391

54-
var container = new MySqlBuilder().WithImage("mysql:8.4").Build();
55-
await container.StartAsync();
56-
return new MySqlDatabase(container);
92+
internal MySqlDatabase(string connectionString, string adminConnectionString, string createdDb)
93+
{
94+
ConnectionString = connectionString;
95+
_adminConnectionString = adminConnectionString;
96+
_createdDb = createdDb;
5797
}
5898

5999
public async ValueTask DisposeAsync()
60100
{
61-
if (_container is not null)
62-
{
63-
await _container.DisposeAsync();
64-
return;
65-
}
66101
try
67102
{
68103
await using var admin = new MySqlConnection(_adminConnectionString);

‎server/csharp/MetaObjects.IntegrationTests/Runner/ScenarioEngine.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static class ScenarioEngineExtensions
3939
public static string Quote(this ScenarioEngine engine) =>
4040
engine == ScenarioEngine.MySql ? "`" : "\"";
4141

42-
/// <summary>The server version Pomelo generates against. Keep in step with the mysql:8.4 image MySqlDatabase starts.</summary>
42+
/// <summary>The server version Pomelo generates against. Keep in step with the mysql:8.4 image MySqlServer starts.</summary>
4343
public static readonly Version MySqlProviderVersion = new(8, 4, 0);
4444

4545
/// <summary>Point this engine's EF Core provider at <paramref name="connString"/> (mutates and returns the builder).</summary>

‎server/java/integration-tests-kotlin/src/test/kotlin/com/metaobjects/integration/kotlin/QueryScenarioRunner.kt‎

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ object QueryScenarioRunner {
103103
// projection views). Executed verbatim on a direct JDBC connection --
104104
// schema authority is the TS-produced artifact, not Exposed. A shared server
105105
// (MySQL) starts each scenario from an empty schema.
106-
if (engine == ScenarioEngine.MYSQL) dropMysqlSchema(jdbcUrl, username, password)
107106
val schemaDdl = ScenarioLoader.readCanonicalSchema(corpus, engine.schemaArtifact)
107+
if (engine == ScenarioEngine.MYSQL) dropMysqlSchema(jdbcUrl, username, password, schemaDdl)
108108
execSql(jdbcUrl, username, password, schemaDdl, engine)
109109

110110
// 2. Seed via the YAML's raw SQL.
@@ -150,21 +150,20 @@ object QueryScenarioRunner {
150150
}
151151
}
152152

153-
/** Drop every view and table in the connected MySQL database. */
154-
private fun dropMysqlSchema(jdbcUrl: String, username: String, password: String) {
153+
private val MYSQL_CREATE = Regex("^CREATE (VIEW|TABLE) `([^`]+)`")
154+
155+
/**
156+
* Drop every view and table [schemaDdl] creates (views first, tables in reverse order), by
157+
* name: `METAOBJECTS_TEST_MYSQL_URL` may point at a shared database.
158+
*/
159+
private fun dropMysqlSchema(jdbcUrl: String, username: String, password: String, schemaDdl: String) {
160+
val created = ScenarioEngine.splitStatements(schemaDdl).mapNotNull { MYSQL_CREATE.find(it)?.groupValues }
161+
val views = created.filter { it[1] == "VIEW" }.map { it[2] }
162+
val tables = created.filter { it[1] == "TABLE" }.map { it[2] }.reversed()
155163
DriverManager.getConnection(jdbcUrl, username, password).use { c ->
156-
val views = mutableListOf<String>()
157-
val tables = mutableListOf<String>()
158-
c.createStatement().use { st ->
159-
st.executeQuery("SHOW FULL TABLES").use { rs ->
160-
while (rs.next()) (if (rs.getString(2) == "VIEW") views else tables).add(rs.getString(1))
161-
}
162-
}
163164
c.createStatement().use { st ->
164-
st.execute("SET FOREIGN_KEY_CHECKS = 0")
165165
views.forEach { st.execute("DROP VIEW IF EXISTS `$it`") }
166166
tables.forEach { st.execute("DROP TABLE IF EXISTS `$it`") }
167-
st.execute("SET FOREIGN_KEY_CHECKS = 1")
168167
}
169168
}
170169
}

0 commit comments

Comments
 (0)