From 094ae50f7f801ca1f44f82a57f04e39dd77ce714 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Thu, 7 Nov 2024 14:11:05 -0600 Subject: [PATCH] Use TestContainers to allow for local testing. (#76) * Use Testcontainers for easier to run tests. * Adjust Workflow script. This is not compatible with Clusters at this time. * Tentative support for Clusters down the line. It does not look like a Cluster can be tested with the base image for ArangoDB. * Don't start so many containers please. * Be explicit about Cluster vs Single, so it's ready to go for later down the line. Edit Build.yml to run against 3.11, which was the original tested-against server. * Minor cleanup items. * Readme. * Remove unnecessary items. --- .github/workflows/build.yml | 21 +-- Core.Arango.Tests/Core.Arango.Tests.csproj | 11 +- Core.Arango.Tests/Core/TestBase.cs | 157 +++++++++++++++--- Core.Arango.Tests/IndexTest.cs | 2 +- Core.Arango.Tests/LinqTest.cs | 20 ++- Core.Arango.Tests/LinqTest_BasicOperations.cs | 5 + Core.Arango.Tests/LinqTest_String.cs | 4 +- Core.Arango.Tests/QueryStatisticTest.cs | 1 + Core.Arango.Tests/Serializer.cs | 2 - Core.Arango.sln | 1 + Core.Arango/Core.Arango.csproj | 12 +- README.md | 9 + 12 files changed, 190 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e17a3498f8..9eb3cb0993 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,25 +21,17 @@ jobs: - 8.x arango: - "arangodb:3.11" -# - "arangodb:3.10" -# - "arangodb/enterprise:3.9" + - "arangodb:latest" +# - "arangodb/enterprise:3.9" topology: - single # - cluster steps: - - uses: actions/checkout@v3 - - - name: chmod - run: find ./docker -name '*.sh' | xargs chmod +x - - - name: arango - run: ./docker/start_db_${{ matrix.topology }}_retry_fail.sh ${{ matrix.arango }} - env: - ARANGO_LICENSE_KEY: ${{ secrets.ARANGO_LICENSE_KEY }} - + - uses: actions/checkout@v4 + - name: dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet }} @@ -49,4 +41,5 @@ jobs: - name: test run: dotnet test --configuration Release env: - ARANGODB_CONNECTION: "Server=http://172.28.3.1:8529;Realm=CI-{UUID};User=root;Password=test;" + ARANGODB_VERSION: ${{ matrix.arango }} + ARANGODB_TOPOLOGY: ${{ matrix.topology }} diff --git a/Core.Arango.Tests/Core.Arango.Tests.csproj b/Core.Arango.Tests/Core.Arango.Tests.csproj index 921f908de5..c4f027632d 100644 --- a/Core.Arango.Tests/Core.Arango.Tests.csproj +++ b/Core.Arango.Tests/Core.Arango.Tests.csproj @@ -10,21 +10,22 @@ - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Core.Arango.Tests/Core/TestBase.cs b/Core.Arango.Tests/Core/TestBase.cs index db9ee489f8..23fed73f2a 100644 --- a/Core.Arango.Tests/Core/TestBase.cs +++ b/Core.Arango.Tests/Core/TestBase.cs @@ -7,29 +7,148 @@ using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; +using Testcontainers.ArangoDb; +using System.Collections.Generic; +using System.IO; +using DotNet.Testcontainers.Builders; namespace Core.Arango.Tests.Core { public abstract class TestBase : IAsyncLifetime { + private const string ARANGO_LICENSE_KEY_ENVAR = "ARANGO_LICENSE_KEY"; + private const string ARANGODB_VERSION_ENVAR = "ARANGODB_VERSION"; + private const string ARANGODB_TOPOLOGY_ENVAR = "ARANGODB_TOPOLOGY"; + private const string DefaultImage = "arangodb:latest"; + private const string DefaultImagePassword = "password"; + private const string DefaultImageUser = "root"; + private const string ClusterValue = "cluster"; + public IArangoContext Arango { get; protected set; } + private readonly static Lazy>> Containers = new(async () => + Environment.GetEnvironmentVariable(ARANGODB_TOPOLOGY_ENVAR) == ClusterValue + ? await SetupClusterServer() + : await SetupSingleServer()); - public virtual Task InitializeAsync() + public virtual async Task InitializeAsync() { - return Task.CompletedTask; + foreach (var container in await Containers.Value) + { + await container.StartAsync() + .ConfigureAwait(false); + } } - public async Task DisposeAsync() + private static (Dictionary Environment, string Version) GetVersionAndEnvironment() { - try + var environment = new Dictionary(); + + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(ARANGO_LICENSE_KEY_ENVAR))) { - foreach (var db in await Arango.Database.ListAsync()) - await Arango.Database.DropAsync(db); + environment.Add(ARANGO_LICENSE_KEY_ENVAR, Environment.GetEnvironmentVariable(ARANGO_LICENSE_KEY_ENVAR)); } - catch + + var version = string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(ARANGODB_VERSION_ENVAR)) + ? DefaultImage + : Environment.GetEnvironmentVariable(ARANGODB_VERSION_ENVAR); + + return (environment, version); + } + + private static Task> SetupSingleServer() + { + var (environment, version) = GetVersionAndEnvironment(); + return Task.FromResult(new List{new ArangoDbBuilder() + .WithImage(version) + .WithEnvironment(environment) + .WithPassword(DefaultImagePassword) + .Build() }); + } + + private static async Task> SetupClusterServer() + { + var (environment, version) = GetVersionAndEnvironment(); + + // This is not yet tested. But is a largely compatible replication of the start_db_cluster script. + // 2024-10-29 20:11:32 Error while processing command-line options for arangod: + // 2024 - 10 - 29 20:11:32 unknown option '--cluster.start-dbserver false' + // I suspect that's because Cluster is only available for Enterprise? + var arangoDBNetwork = new NetworkBuilder() + .WithName($"ArangoDB-{Guid.NewGuid():D}") + .Build(); + + var jwt = Path.GetTempFileName(); + File.Move(jwt, jwt + ".jwt"); + jwt += ".jwt"; + File.AppendAllText(jwt, "Averysecretword"); + + var leadAgent = $"leadagent-{Guid.NewGuid():N}"; + var extraAgents = Enumerable.Range(0, 2); + var dbs = Enumerable.Range(0, 2); + var coordinators = Enumerable.Range(0, 2); + + var containers = new List(); + + var DefaultContainerConfiguration = new ArangoDbBuilder() + .WithImage(version) + .WithResourceMapping(jwt, "/jwtSecret") + .WithEnvironment(environment) + .WithPassword(DefaultImagePassword) + .WithNetwork(arangoDBNetwork); + + containers.Add(DefaultContainerConfiguration + .WithName(leadAgent) + .WithCommand( + "--cluster.start-dbserver", "false", + "--cluster.start-coordinator", "false", + "--auth.jwt-secret", "/jwtSecret") + .Build()); + + foreach (var _ in extraAgents) { - // + containers.Add(DefaultContainerConfiguration + .WithName($"{extraAgents}-{Guid.NewGuid():N}") + .WithCommand( + "--cluster.start-dbserver", "false", + "--cluster.start-coordinator", "false", + "--auth.jwt-secret", "/jwtSecret", + "--starter.join", leadAgent) + .Build()); } + + foreach (var _ in dbs) + { + containers.Add(DefaultContainerConfiguration + .WithName($"{dbs}-{Guid.NewGuid():N}") + .WithCommand( + "--cluster.start-dbserver", "true", + "--cluster.start-coordinator", "false", + "--auth.jwt-secret", "/jwtSecret", + "--starter.join", leadAgent) + .Build()); + } + + foreach (var _ in coordinators) + { + containers.Add(DefaultContainerConfiguration + .WithName($"{coordinators}-{Guid.NewGuid():N}") + .WithCommand( + "--cluster.start-dbserver", "false", + "--cluster.start-coordinator", "true", + "--auth.jwt-secret", "/jwtSecret", + "--starter.join", leadAgent) + .Build()); + } + + await arangoDBNetwork.CreateAsync() + .ConfigureAwait(false); + + return containers; + } + + public async Task DisposeAsync() + { + await Task.CompletedTask; } public async Task SetupAsync(string serializer, string createDatabase = "test") @@ -47,20 +166,20 @@ public async Task SetupAsync(string serializer, string createDatabase = "test") }); if (!string.IsNullOrEmpty(createDatabase)) - await Arango.Database.CreateAsync("test"); + { + var databaseCreateSuccessful = await Arango.Database.CreateAsync("test"); + if (databaseCreateSuccessful == false) + { + throw new Exception("Database creation failed"); + } + } } - protected string UniqueTestRealm() - { - var cs = Environment.GetEnvironmentVariable("ARANGODB_CONNECTION"); - - if (string.IsNullOrWhiteSpace(cs)) - cs = "Server=http://localhost:8529;Realm=CI-{UUID};User=root;Password=;"; - - return cs.Replace("{UUID}", Guid.NewGuid().ToString("D")); - } + protected static string UniqueTestRealm() + // Last to get the Coordinators for clusters, or the only existing one for a single server. + => $"Server={Containers.Value.Result.Last().GetTransportAddress()};User={DefaultImageUser};Realm=CI-{Guid.NewGuid():D};Password={DefaultImagePassword};"; - protected void PrintQuery(IQueryable query, ITestOutputHelper output) + protected static void PrintQuery(IQueryable query, ITestOutputHelper output) { var aql = query.ToAql(); output.WriteLine("QUERY:"); diff --git a/Core.Arango.Tests/IndexTest.cs b/Core.Arango.Tests/IndexTest.cs index b77a83e136..573efbab9a 100644 --- a/Core.Arango.Tests/IndexTest.cs +++ b/Core.Arango.Tests/IndexTest.cs @@ -17,7 +17,7 @@ public async Task DropAll(string serializer) await Arango.Index.CreateAsync("test", "test", new ArangoIndex { - Fields = new List {"test"}, + Fields = ["test"], Type = ArangoIndexType.Hash }); diff --git a/Core.Arango.Tests/LinqTest.cs b/Core.Arango.Tests/LinqTest.cs index 2e7fa9ccee..3a536451a2 100644 --- a/Core.Arango.Tests/LinqTest.cs +++ b/Core.Arango.Tests/LinqTest.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using Core.Arango.Protocol; @@ -10,7 +8,6 @@ using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -using System.Linq.Expressions; namespace Core.Arango.Tests { @@ -70,7 +67,7 @@ await Arango.Query("test") } [Fact] - public async Task MultiFilter() + public Task MultiFilter() { var q = Arango.Query("test") .Where(x => x.Name == "Project A") @@ -80,16 +77,20 @@ public async Task MultiFilter() .Skip(0).Take(1); Assert.Equal("FOR `x` IN `Project` FILTER ( `x`.`Name` == @P1 ) SORT `x`.`Name` ASC FILTER ( `x`.`Name` == @P2 ) SORT `x`.`Name` DESC LIMIT @P3 , @P4 RETURN `x`", q.ToAql().aql.Trim()); + + return Task.CompletedTask; } [Fact] - public async Task FilterOrder() + public Task FilterOrder() { var q = Arango.Query("test") .Where(x => (x.Name == "Project A" || x.Name == "Project B") && x.Budget <= 0); Assert.Equal("FOR `x` IN `Project` FILTER ( ( ( `x`.`Name` == @P1 ) OR ( `x`.`Name` == @P2 ) ) AND ( `x`.`Budget` <= @P3 ) ) RETURN `x`", q.ToAql().aql.Trim()); + + return Task.CompletedTask; } [Fact] @@ -117,6 +118,7 @@ public async Task GroupBy() _output.WriteLine(q.ToAql().aql); _output.WriteLine(""); + await Task.CompletedTask; //_output.WriteLine(JsonConvert.SerializeObject(await q.ToListAsync(), Formatting.Indented)); } @@ -132,6 +134,7 @@ public async Task MathAbs() _output.WriteLine(q.ToAql().aql); _output.WriteLine(""); + await Task.CompletedTask; //_output.WriteLine(JsonConvert.SerializeObject(await q.ToListAsync(), Formatting.Indented)); } @@ -147,6 +150,7 @@ public async Task Ternary() _output.WriteLine(q.ToAql().aql); _output.WriteLine(""); + await Task.CompletedTask; //_output.WriteLine(JsonConvert.SerializeObject(await q.ToListAsync(), Formatting.Indented)); } @@ -174,6 +178,7 @@ public async Task Remove() .Remove().In().Select(x => x.Key); _output.WriteLine(q.ToAql().aql); + await Task.CompletedTask; } [Fact] @@ -217,10 +222,13 @@ public async Task QueryableContains() .Contains("CB"); Assert.True(q); + await Task.CompletedTask; } public override async Task InitializeAsync() { + await base.InitializeAsync(); + Arango = new ArangoContext(UniqueTestRealm()); await Arango.Database.CreateAsync(D); await Arango.Collection.CreateAsync(D, nameof(Client), ArangoCollectionType.Document); @@ -300,6 +308,7 @@ public async Task StringContains() var aql = q.ToAql().aql.Trim(); Assert.Equal("FOR `x` IN `Project` FILTER CONTAINS( `x`.`Name` , @P1 ) RETURN `x`", aql); + await Task.CompletedTask; } [Fact] @@ -309,6 +318,7 @@ public async Task StringConcat() var aql = q.ToAql().aql.Trim(); Assert.Equal("FOR `x` IN `Project` FILTER ( CONCAT( `x`.`Name` , @P1 ) == @P2 ) RETURN `x`", aql); + await Task.CompletedTask; } } } \ No newline at end of file diff --git a/Core.Arango.Tests/LinqTest_BasicOperations.cs b/Core.Arango.Tests/LinqTest_BasicOperations.cs index 46d68ebb63..bf59042db1 100644 --- a/Core.Arango.Tests/LinqTest_BasicOperations.cs +++ b/Core.Arango.Tests/LinqTest_BasicOperations.cs @@ -48,6 +48,7 @@ class InnerChain public class LinqTest_BasicOperations : TestBase { + private const string D = "test"; private readonly ITestOutputHelper _output; public LinqTest_BasicOperations(ITestOutputHelper output) @@ -381,6 +382,7 @@ public async Task Min() var min = Arango.Query("test").Select(x => x.Revenue).Min(); Assert.Equal(3.4m, min); + await Task.CompletedTask; } [Fact] @@ -389,6 +391,7 @@ public async Task Max() var max = Arango.Query("test").Select(x => x.Revenue).Max(); Assert.Equal(4.4m, max); + await Task.CompletedTask; } [Fact] @@ -398,6 +401,7 @@ public async Task Sum() var sum = Arango.Query("test").Select(x => x.Revenue).Sum(); Assert.Equal(expectedSum, sum); + await Task.CompletedTask; } /*[Fact] @@ -492,6 +496,7 @@ public async Task Take() public override async Task InitializeAsync() { + await base.InitializeAsync(); Arango = new ArangoContext(UniqueTestRealm()); await Arango.Database.CreateAsync(D); await Arango.Collection.CreateAsync(D, nameof(Activity), ArangoCollectionType.Document); diff --git a/Core.Arango.Tests/LinqTest_String.cs b/Core.Arango.Tests/LinqTest_String.cs index a7726a3d22..45791a2d8a 100644 --- a/Core.Arango.Tests/LinqTest_String.cs +++ b/Core.Arango.Tests/LinqTest_String.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using Core.Arango.Protocol; using Core.Arango.Linq; using Core.Arango.Tests.Core; -using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -227,6 +224,7 @@ public async Task StringToUpper() public override async Task InitializeAsync() { + await base.InitializeAsync(); Arango = new ArangoContext(UniqueTestRealm()); await Arango.Database.CreateAsync(D); await Arango.Collection.CreateAsync(D, nameof(Client), ArangoCollectionType.Document); diff --git a/Core.Arango.Tests/QueryStatisticTest.cs b/Core.Arango.Tests/QueryStatisticTest.cs index 2ab326ba37..e769af386a 100644 --- a/Core.Arango.Tests/QueryStatisticTest.cs +++ b/Core.Arango.Tests/QueryStatisticTest.cs @@ -20,6 +20,7 @@ public QueryStatisticTest(ITestOutputHelper output) public override async Task InitializeAsync() { + await base.InitializeAsync(); Arango = new ArangoContext(UniqueTestRealm(), new ArangoConfiguration diff --git a/Core.Arango.Tests/Serializer.cs b/Core.Arango.Tests/Serializer.cs index 806be072af..5ebc565ef9 100644 --- a/Core.Arango.Tests/Serializer.cs +++ b/Core.Arango.Tests/Serializer.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; using Core.Arango.Serialization; using Core.Arango.Serialization.Json; using Core.Arango.Serialization.Newtonsoft; diff --git a/Core.Arango.sln b/Core.Arango.sln index a5b9ad6783..69e6ea547b 100644 --- a/Core.Arango.sln +++ b/Core.Arango.sln @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{13D67C1C-3066-4412-A1F6-9C9AD13B7C81}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + README.md = README.md EndProjectSection EndProject Global diff --git a/Core.Arango/Core.Arango.csproj b/Core.Arango/Core.Arango.csproj index 65d3977ab3..5518fff4ac 100644 --- a/Core.Arango/Core.Arango.csproj +++ b/Core.Arango/Core.Arango.csproj @@ -37,7 +37,7 @@ - + @@ -46,14 +46,14 @@ - - + + - - - + + + diff --git a/README.md b/README.md index 34fa313eb1..d24f0632dd 100644 --- a/README.md +++ b/README.md @@ -667,3 +667,12 @@ await Arango.Backup.RestoreAsync(backup.Id); await Arango.Backup.DeleteAsync(backup.Id); ``` + +# Building and Testing +## Build +- Clone the repository. +- Run `dotnet build` to build the solution, or use your preferred IDE to build the solution. + +## Testing +- Install [Docker Desktop](https://www.docker.com/products/docker-desktop/), this is required to run the integration tests via TestContainers. +- Run `dotnet test` to run the tests, or use your preferred IDE to run the tests. \ No newline at end of file