Skip to content

Commit d49fec8

Browse files
Add Elastic.XunitV3.Elasticsearch — xUnit V3 integration for ephemeral Elasticsearch clusters (#77)
* Add `Elastic.Xunitv3.Elasticsearch` — xUnit v3 integration for ephemeral Elasticsearch clusters Introduces two new packages mirroring the TUnit integration, built on `Nullean.Xunit.Partitions.v3` for cluster lifecycle management: - `Elastic.Xunitv3.Elasticsearch.Core` — core integration (no client dependency) implementing `IPartitionLifetime` for serial cluster startup, external cluster support, version-based skip via `BeforeAfterTestAttribute` + `Assert.SkipUnless`, and ANSI bootstrap diagnostics. - `Elastic.Xunitv3.Elasticsearch` — convenience layer with a pre-configured `ElasticsearchClient` and per-test output routing. Also removes verbose (`-v`) flag from tar extraction in Ephemeral to suppress per-file output during bootstrap. Co-authored-by: Cursor <cursoragent@cursor.com> * Rename Xunitv3 to XunitV3 casing and add comprehensive NuGet READMEs Follow the V3 casing convention used by other xUnit V3 integration packages. Renames all folders, csproj files, namespaces, and references. Expands the Elastic.XunitV3.Elasticsearch README to serve as the NuGet landing page with quick start, configuration, external clusters, version skipping, filtering, and CLI usage. Co-authored-by: Cursor <cursoragent@cursor.com> * Refine README wording for partition rationale Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2e96230 commit d49fec8

30 files changed

Lines changed: 1741 additions & 1 deletion

Elastic.Abstractions.slnx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<Project Path="examples/Elastic.Xunit.ExampleMinimal/Elastic.Xunit.ExampleMinimal.csproj" />
1919
<Project Path="examples/Elastic.TUnit.ExampleMinimal/Elastic.TUnit.ExampleMinimal.csproj" />
2020
<Project Path="examples/Elastic.TUnit.ExampleComplex/Elastic.TUnit.ExampleComplex.csproj" />
21+
<Project Path="examples/Elastic.XunitV3.ExampleMinimal/Elastic.XunitV3.ExampleMinimal.csproj" />
22+
<Project Path="examples/Elastic.XunitV3.ExampleComplex/Elastic.XunitV3.ExampleComplex.csproj" />
2123
<Project Path="examples/ScratchPad/ScratchPad.csproj" />
2224
</Folder>
2325
<Folder Name="/src/">
@@ -27,6 +29,8 @@
2729
<Project Path="src/Elastic.TUnit.Elasticsearch.Core/Elastic.TUnit.Elasticsearch.Core.csproj" />
2830
<Project Path="src/Elastic.TUnit.Elasticsearch/Elastic.TUnit.Elasticsearch.csproj" />
2931
<Project Path="src/Elastic.Elasticsearch.Xunit/Elastic.Elasticsearch.Xunit.csproj" />
32+
<Project Path="src/Elastic.XunitV3.Elasticsearch.Core/Elastic.XunitV3.Elasticsearch.Core.csproj" />
33+
<Project Path="src/Elastic.XunitV3.Elasticsearch/Elastic.XunitV3.Elasticsearch.csproj" />
3034
<Project Path="src/Elastic.Stack.ArtifactsApi/Elastic.Stack.ArtifactsApi.csproj" />
3135
</Folder>
3236
</Solution>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Clients.Elasticsearch;
6+
using Elastic.XunitV3.Elasticsearch.Core;
7+
using Nullean.Xunit.Partitions.v3.Sdk;
8+
9+
namespace Elastic.XunitV3.ExampleComplex;
10+
11+
public abstract class ClusterTestClassBase<TCluster>(TCluster cluster)
12+
: IClusterFixture<TCluster>
13+
where TCluster : ElasticsearchCluster<ElasticsearchConfiguration>, IPartitionLifetime
14+
{
15+
protected TCluster Cluster { get; } = cluster;
16+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using Elastic.Clients.Elasticsearch;
7+
using Elastic.Transport;
8+
using Elastic.XunitV3.Elasticsearch;
9+
using Elastic.XunitV3.Elasticsearch.Core;
10+
11+
namespace Elastic.XunitV3.ExampleComplex;
12+
13+
/// <summary>
14+
/// Shared base — inherits the default Client from <see cref="ElasticsearchCluster" />.
15+
/// Env-var detection (TEST_ELASTICSEARCH_URL / TEST_ELASTICSEARCH_API_KEY) is handled
16+
/// automatically by the base class — no code changes needed.
17+
/// </summary>
18+
public abstract class MyClusterBase : ElasticsearchCluster
19+
{
20+
protected MyClusterBase() : base(new ElasticsearchConfiguration("latest-9")
21+
{
22+
ShowElasticsearchOutputAfterStarted = false,
23+
})
24+
{
25+
}
26+
}
27+
28+
public class TestCluster : MyClusterBase
29+
{
30+
protected override void SeedCluster()
31+
{
32+
var response = Client.Info();
33+
}
34+
}
35+
36+
/// <summary>
37+
/// Uses the generic base directly — needs its own Client property since
38+
/// <see cref="ElasticsearchCluster{TConfiguration}" /> does not provide one.
39+
/// </summary>
40+
public class TestGenericCluster : ElasticsearchCluster<ElasticsearchConfiguration>
41+
{
42+
public TestGenericCluster() : base(new ElasticsearchConfiguration("latest-9"))
43+
{
44+
}
45+
46+
public ElasticsearchClient Client => this.GetOrAddClient((c, output) =>
47+
{
48+
var pool = new StaticNodePool(c.NodesUris());
49+
var settings = new ElasticsearchClientSettings(pool)
50+
.EnableDebugMode()
51+
.OnRequestCompleted(call => output.WriteLine(call.DebugInformation));
52+
53+
if (ExternalApiKey != null)
54+
settings = settings.Authentication(new ApiKey(ExternalApiKey));
55+
56+
return new ElasticsearchClient(settings);
57+
});
58+
59+
protected override void SeedCluster()
60+
{
61+
var response = Client.Info();
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Demonstrates the programmatic hook — override
67+
/// <see cref="ElasticsearchCluster{TConfiguration}.TryUseExternalCluster" />
68+
/// to point at a specific cluster from code (e.g. read from a config file, service
69+
/// discovery, etc.). Falls through to ephemeral startup when returning null.
70+
/// </summary>
71+
public class ProgrammaticExternalCluster : ElasticsearchCluster
72+
{
73+
public ProgrammaticExternalCluster() : base(new ElasticsearchConfiguration("latest-9"))
74+
{
75+
}
76+
77+
protected override ExternalClusterConfiguration TryUseExternalCluster()
78+
{
79+
var url = Environment.GetEnvironmentVariable("MY_DEV_CLUSTER_URL");
80+
if (string.IsNullOrEmpty(url))
81+
return null;
82+
83+
return new ExternalClusterConfiguration(
84+
new Uri(url),
85+
Environment.GetEnvironmentVariable("MY_DEV_CLUSTER_KEY")
86+
);
87+
}
88+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<IsPackable>False</IsPackable>
6+
<!-- xUnit1041: partition framework injects fixtures at runtime, not visible to static analysis -->
7+
<NoWarn>$(NoWarn);xUnit1041;xUnit1051</NoWarn>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="xunit.v3" Version="3.2.2" />
11+
<PackageReference Include="FluentAssertions" Version="7.2.0" />
12+
</ItemGroup>
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\Elastic.XunitV3.Elasticsearch\Elastic.XunitV3.Elasticsearch.csproj" />
15+
</ItemGroup>
16+
</Project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Stack.ArtifactsApi;
6+
using Elastic.XunitV3.Elasticsearch.Core;
7+
using Elastic.XunitV3.ExampleComplex;
8+
using Xunit;
9+
10+
[assembly: TestFramework(typeof(ElasticTestFramework))]
11+
[assembly: ElasticXunitConfiguration(typeof(MyRunOptions))]
12+
13+
namespace Elastic.XunitV3.ExampleComplex;
14+
15+
/// <summary>
16+
/// Allows us to control the xUnit v3 test pipeline
17+
/// </summary>
18+
public class MyRunOptions : ElasticXunitRunOptions
19+
{
20+
public MyRunOptions()
21+
{
22+
Version = TestVersion;
23+
}
24+
25+
public static ElasticVersion TestVersion { get; } = "latest-9";
26+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Threading.Tasks;
6+
using Elastic.XunitV3.Elasticsearch.Core;
7+
using FluentAssertions;
8+
using Xunit;
9+
10+
namespace Elastic.XunitV3.ExampleComplex;
11+
12+
public class MyTestClass(TestCluster cluster)
13+
: ClusterTestClassBase<TestCluster>(cluster)
14+
{
15+
[Fact]
16+
public void SomeTest()
17+
{
18+
var info = Cluster.Client.Info();
19+
20+
info.IsValidResponse.Should().BeTrue();
21+
}
22+
}
23+
24+
public class Tests1(TestCluster cluster)
25+
: ClusterTestClassBase<TestCluster>(cluster)
26+
{
27+
[Fact] public void Unit1Test() => (1 + 1).Should().Be(2);
28+
[Fact] public void Unit1Test1() => (1 + 1).Should().Be(2);
29+
[Fact] public void Unit1Test2() => (1 + 1).Should().Be(2);
30+
[Fact] public void Unit1Test3() => (1 + 1).Should().Be(2);
31+
[Fact] public void Unit1Test4() => (1 + 1).Should().Be(2);
32+
[Fact] public void Unit1Test5() => (1 + 1).Should().Be(2);
33+
[Fact] public void Unit1Test6() => (1 + 1).Should().Be(2);
34+
}
35+
36+
public class Tests3
37+
{
38+
[Fact] public void Unit3Test() => (1 + 1).Should().Be(2);
39+
[Fact] public void Unit3Test1() => (1 + 1).Should().Be(2);
40+
[Fact] public void Unit3Test2() => (1 + 1).Should().Be(2);
41+
[Fact] public void Unit3Test3() => (1 + 1).Should().Be(2);
42+
[Fact] public void Unit3Test4() => (1 + 1).Should().Be(2);
43+
[Fact] public void Unit3Test5() => (1 + 1).Should().Be(2);
44+
[Fact] public void Unit3Test6() => (1 + 1).Should().Be(2);
45+
}
46+
47+
public class Tests2(TestCluster cluster)
48+
: ClusterTestClassBase<TestCluster>(cluster)
49+
{
50+
[Fact] public void Unit2Test() => (1 + 1).Should().Be(2);
51+
[Fact] public void Unit2Test1() => (1 + 1).Should().Be(2);
52+
[Fact] public void Unit2Test2() => (1 + 1).Should().Be(2);
53+
[Fact] public void Unit2Test3() => (1 + 1).Should().Be(2);
54+
[Fact] public void Unit2Test4() => (1 + 1).Should().Be(2);
55+
[Fact] public void Unit2Test5() => (1 + 1).Should().Be(2);
56+
[Fact] public void Unit2Test6() => (1 + 1).Should().Be(2);
57+
}
58+
59+
public class MyGenericTestClass(TestGenericCluster cluster)
60+
: ClusterTestClassBase<TestGenericCluster>(cluster)
61+
{
62+
[Fact]
63+
public void SomeTest()
64+
{
65+
var info = Cluster.Client.Info();
66+
67+
info.IsValidResponse.Should().BeTrue();
68+
}
69+
70+
[Fact] public void MyGenericUnitTest() => (1 + 1).Should().Be(2);
71+
[Fact] public void MyGenericUnitTest1() => (1 + 1).Should().Be(2);
72+
[Fact] public void MyGenericUnitTest2() => (1 + 1).Should().Be(2);
73+
[Fact] public void MyGenericUnitTest3() => (1 + 1).Should().Be(2);
74+
[Fact] public void MyGenericUnitTest4() => (1 + 1).Should().Be(2);
75+
[Fact] public void MyGenericUnitTest5() => (1 + 1).Should().Be(2);
76+
[Fact] public void MyGenericUnitTest6() => (1 + 1).Should().Be(2);
77+
}
78+
79+
[SkipVersion("<6.2.0", "")]
80+
public class SkipTestClass(TestGenericCluster cluster)
81+
: ClusterTestClassBase<TestGenericCluster>(cluster)
82+
{
83+
[Fact]
84+
public async Task SomeTest()
85+
{
86+
var info = await Cluster.Client.InfoAsync();
87+
88+
info.IsValidResponse.Should().BeTrue();
89+
}
90+
91+
[Fact]
92+
public void UnitTest() => (1 + 1).Should().Be(2);
93+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<IsPackable>False</IsPackable>
6+
<!-- xUnit1041: partition framework injects fixtures at runtime, not visible to static analysis -->
7+
<NoWarn>$(NoWarn);xUnit1041;xUnit1051</NoWarn>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="xunit.v3" Version="3.2.2" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\Elastic.XunitV3.Elasticsearch\Elastic.XunitV3.Elasticsearch.csproj" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Threading.Tasks;
6+
using Elastic.XunitV3.Elasticsearch;
7+
using Elastic.XunitV3.Elasticsearch.Core;
8+
using Xunit;
9+
10+
[assembly: TestFramework(typeof(ElasticTestFramework))]
11+
12+
namespace Elastic.XunitV3.ExampleMinimal;
13+
14+
/// <summary> One-liner cluster — the default Client is provided by the base class. </summary>
15+
public class MyTestCluster() : ElasticsearchCluster("latest-9");
16+
17+
public class ExampleTest(MyTestCluster cluster) : IClusterFixture<MyTestCluster>
18+
{
19+
[Fact]
20+
public async Task SomeTest()
21+
{
22+
var info = await cluster.Client.InfoAsync();
23+
24+
Assert.NotNull(info.Name);
25+
}
26+
}

src/Elastic.Elasticsearch.Ephemeral/Tasks/IClusterComposeTask.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ private static void ExtractTarGz(string file, string toFolder)
241241
}
242242
else
243243
//SharpZipLib loses permissions when untarring
244-
Proc.Exec("tar", "-zxvf", file, "-C", toFolder);
244+
Proc.Exec("tar", "-zxf", file, "-C", toFolder);
245245
}
246246

247247
private static void ExtractZip(string file, string toFolder) =>

0 commit comments

Comments
 (0)