Skip to content

Commit feb9508

Browse files
committed
Refactor resource fixtures to enforce initialization checks
- Updated `XunitComposeFixture`, `XunitContainerFixture`, `XunitPodmanKubernetesFixture`, `XunitSwarmStackFixture`, `XunitResourceFixture`, and `XunitTopologyFixture` to ensure properties are only accessible after calling `InitializeAsync`, throwing `InvalidOperationException` otherwise. - Modified tests in `XunitAdapterTests`, `XunitComposeTopologyFixtureTests`, and others to reflect the new behavior of throwing exceptions instead of returning null for uninitialized properties. - Enhanced resource lifecycle management in `ResourceLifecycle` to handle exceptions during disposal more gracefully, ensuring proper cleanup. - Added new tests in `BuilderCleanupTests` to verify that resources are cleaned up correctly during failed provisioning scenarios. - Improved error handling in `Builder` and `ContainerBuilder` to prevent resource leaks during failures.
1 parent 1747f0a commit feb9508

15 files changed

Lines changed: 706 additions & 127 deletions

FluentDocker.Testing.Xunit/XunitComposeFixture.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,39 @@ namespace FluentDocker.Testing.Xunit
1818
/// Subclass it and override <see cref="XunitComposeFixtureBase.ConfigureCompose"/>
1919
/// — xUnit handles the async lifecycle automatically.</para>
2020
/// <para>Use this class only when you need programmatic control over
21-
/// initialization (e.g., dynamic configuration, conditional setup).</para>
21+
/// initialization (e.g., dynamic configuration, conditional setup).
22+
/// You <b>must</b> call <see cref="InitializeAsync"/> before accessing
23+
/// any properties — accessing them before initialization throws
24+
/// <see cref="InvalidOperationException"/>.</para>
2225
/// </remarks>
2326
public class XunitComposeFixture : IAsyncDisposable
2427
{
2528
private ComposeResource _resource;
29+
private FluentDockerKernel _kernel;
2630

2731
/// <summary>
2832
/// The underlying compose resource.
2933
/// </summary>
30-
public ComposeResource Resource => _resource;
34+
public ComposeResource Resource
35+
{
36+
get { EnsureInitialized(); return _resource; }
37+
}
3138

3239
/// <summary>
3340
/// The running compose service, available after initialization.
3441
/// </summary>
35-
public IComposeService Service => _resource?.Service;
42+
public IComposeService Service
43+
{
44+
get { EnsureInitialized(); return _resource.Service; }
45+
}
3646

3747
/// <summary>
3848
/// The kernel managing drivers.
3949
/// </summary>
40-
public FluentDockerKernel Kernel { get; private set; }
50+
public FluentDockerKernel Kernel
51+
{
52+
get { EnsureInitialized(); return _kernel; }
53+
}
4154

4255
/// <summary>
4356
/// Configures and initializes the fixture.
@@ -57,7 +70,7 @@ public async Task InitializeAsync(
5770
kernelFactory,
5871
cancellationToken: cancellationToken);
5972

60-
Kernel = kernel;
73+
_kernel = kernel;
6174
_resource = resource;
6275
}
6376

@@ -66,16 +79,20 @@ public async ValueTask DisposeAsync()
6679
{
6780
try
6881
{
69-
if (_resource != null)
70-
await _resource.DisposeAsync();
82+
await ResourceLifecycle.DisposeAsync(_resource, _kernel);
7183
}
7284
finally
7385
{
74-
if (Kernel != null)
75-
await Kernel.DisposeAsync();
7686
_resource = null;
77-
Kernel = null;
87+
_kernel = null;
7888
}
7989
}
90+
91+
private void EnsureInitialized()
92+
{
93+
if (_resource == null)
94+
throw new InvalidOperationException(
95+
"Fixture has not been initialized. Call InitializeAsync first.");
96+
}
8097
}
8198
}

FluentDocker.Testing.Xunit/XunitContainerFixture.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,39 @@ namespace FluentDocker.Testing.Xunit
1818
/// Subclass it and override <see cref="XunitContainerFixtureBase.ConfigureContainer"/>
1919
/// — xUnit handles the async lifecycle automatically.</para>
2020
/// <para>Use this class only when you need programmatic control over
21-
/// initialization (e.g., dynamic configuration, conditional setup).</para>
21+
/// initialization (e.g., dynamic configuration, conditional setup).
22+
/// You <b>must</b> call <see cref="InitializeAsync"/> before accessing
23+
/// any properties — accessing them before initialization throws
24+
/// <see cref="InvalidOperationException"/>.</para>
2225
/// </remarks>
2326
public class XunitContainerFixture : IAsyncDisposable
2427
{
2528
private ContainerResource _resource;
29+
private FluentDockerKernel _kernel;
2630

2731
/// <summary>
2832
/// The underlying container resource.
2933
/// </summary>
30-
public ContainerResource Resource => _resource;
34+
public ContainerResource Resource
35+
{
36+
get { EnsureInitialized(); return _resource; }
37+
}
3138

3239
/// <summary>
3340
/// The running container service, available after initialization.
3441
/// </summary>
35-
public IContainerService Container => _resource?.Container;
42+
public IContainerService Container
43+
{
44+
get { EnsureInitialized(); return _resource.Container; }
45+
}
3646

3747
/// <summary>
3848
/// The kernel managing drivers.
3949
/// </summary>
40-
public FluentDockerKernel Kernel { get; private set; }
50+
public FluentDockerKernel Kernel
51+
{
52+
get { EnsureInitialized(); return _kernel; }
53+
}
4154

4255
/// <summary>
4356
/// Configures and initializes the fixture.
@@ -61,7 +74,7 @@ public async Task InitializeAsync(
6174
kernelFactory,
6275
cancellationToken: cancellationToken);
6376

64-
Kernel = kernel;
77+
_kernel = kernel;
6578
_resource = resource;
6679
}
6780

@@ -70,16 +83,20 @@ public async ValueTask DisposeAsync()
7083
{
7184
try
7285
{
73-
if (_resource != null)
74-
await _resource.DisposeAsync();
86+
await ResourceLifecycle.DisposeAsync(_resource, _kernel);
7587
}
7688
finally
7789
{
78-
if (Kernel != null)
79-
await Kernel.DisposeAsync();
8090
_resource = null;
81-
Kernel = null;
91+
_kernel = null;
8292
}
8393
}
94+
95+
private void EnsureInitialized()
96+
{
97+
if (_resource == null)
98+
throw new InvalidOperationException(
99+
"Fixture has not been initialized. Call InitializeAsync first.");
100+
}
84101
}
85102
}

FluentDocker.Testing.Xunit/XunitPodmanKubernetesFixture.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,38 @@ namespace FluentDocker.Testing.Xunit
1616
/// <para>This fixture requires explicit initialization via
1717
/// <see cref="InitializeAsync"/>. No abstract fixture base is provided for
1818
/// Podman Kubernetes because it uses a config object rather than a builder.</para>
19+
/// <para>You <b>must</b> call <see cref="InitializeAsync"/> before accessing
20+
/// any properties — accessing them before initialization throws
21+
/// <see cref="InvalidOperationException"/>.</para>
1922
/// </remarks>
2023
public class XunitPodmanKubernetesFixture : IAsyncDisposable
2124
{
2225
private PodmanKubernetesResource _resource;
26+
private FluentDockerKernel _kernel;
2327

2428
/// <summary>
2529
/// The underlying Podman Kubernetes resource.
2630
/// </summary>
27-
public PodmanKubernetesResource Resource => _resource;
31+
public PodmanKubernetesResource Resource
32+
{
33+
get { EnsureInitialized(); return _resource; }
34+
}
2835

2936
/// <summary>
3037
/// Path to the Kubernetes YAML file.
3138
/// </summary>
32-
public string YamlPath => _resource?.YamlPath;
39+
public string YamlPath
40+
{
41+
get { EnsureInitialized(); return _resource.YamlPath; }
42+
}
3343

3444
/// <summary>
3545
/// The kernel managing drivers.
3646
/// </summary>
37-
public FluentDockerKernel Kernel { get; private set; }
47+
public FluentDockerKernel Kernel
48+
{
49+
get { EnsureInitialized(); return _kernel; }
50+
}
3851

3952
/// <summary>
4053
/// Configures and initializes the fixture.
@@ -59,7 +72,7 @@ public async Task InitializeAsync(
5972
ResourceLifecycle.CreateDefaultPodmanKernelAsync,
6073
cancellationToken);
6174

62-
Kernel = kernel;
75+
_kernel = kernel;
6376
_resource = resource;
6477
}
6578

@@ -68,16 +81,20 @@ public async ValueTask DisposeAsync()
6881
{
6982
try
7083
{
71-
if (_resource != null)
72-
await _resource.DisposeAsync();
84+
await ResourceLifecycle.DisposeAsync(_resource, _kernel);
7385
}
7486
finally
7587
{
76-
if (Kernel != null)
77-
await Kernel.DisposeAsync();
7888
_resource = null;
79-
Kernel = null;
89+
_kernel = null;
8090
}
8191
}
92+
93+
private void EnsureInitialized()
94+
{
95+
if (_resource == null)
96+
throw new InvalidOperationException(
97+
"Fixture has not been initialized. Call InitializeAsync first.");
98+
}
8299
}
83100
}

FluentDocker.Testing.Xunit/XunitResourceFixture.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,32 @@ namespace FluentDocker.Testing.Xunit
1818
/// <see cref="XunitComposeFixtureBase"/>, <see cref="XunitTopologyFixtureBase"/>)
1919
/// which provide automatic async lifecycle via <c>IAsyncLifetime</c>.</para>
2020
/// <para>Use this class for custom or plugin resource types, or when you need
21-
/// programmatic control over initialization.</para>
21+
/// programmatic control over initialization. You <b>must</b> call
22+
/// <see cref="InitializeAsync"/> before accessing any properties — accessing
23+
/// them before initialization throws <see cref="InvalidOperationException"/>.</para>
2224
/// </remarks>
2325
/// <typeparam name="TResource">The resource type to manage.</typeparam>
2426
public class XunitResourceFixture<TResource> : IAsyncDisposable
2527
where TResource : class, ITestResource
2628
{
2729
private TResource _resource;
30+
private FluentDockerKernel _kernel;
2831

2932
/// <summary>
3033
/// The underlying resource.
3134
/// </summary>
32-
public TResource Resource => _resource;
35+
public TResource Resource
36+
{
37+
get { EnsureInitialized(); return _resource; }
38+
}
3339

3440
/// <summary>
3541
/// The kernel managing drivers.
3642
/// </summary>
37-
public FluentDockerKernel Kernel { get; private set; }
43+
public FluentDockerKernel Kernel
44+
{
45+
get { EnsureInitialized(); return _kernel; }
46+
}
3847

3948
/// <summary>
4049
/// Configures and initializes the fixture.
@@ -55,7 +64,7 @@ public async Task InitializeAsync(
5564
resourceFactory, kernelFactory,
5665
cancellationToken: cancellationToken);
5766

58-
Kernel = kernel;
67+
_kernel = kernel;
5968
_resource = resource;
6069
}
6170

@@ -64,16 +73,20 @@ public async ValueTask DisposeAsync()
6473
{
6574
try
6675
{
67-
if (_resource != null)
68-
await _resource.DisposeAsync();
76+
await ResourceLifecycle.DisposeAsync(_resource, _kernel);
6977
}
7078
finally
7179
{
72-
if (Kernel != null)
73-
await Kernel.DisposeAsync();
7480
_resource = null;
75-
Kernel = null;
81+
_kernel = null;
7682
}
7783
}
84+
85+
private void EnsureInitialized()
86+
{
87+
if (_resource == null)
88+
throw new InvalidOperationException(
89+
"Fixture has not been initialized. Call InitializeAsync first.");
90+
}
7891
}
7992
}

FluentDocker.Testing.Xunit/XunitSwarmStackFixture.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,38 @@ namespace FluentDocker.Testing.Xunit
1616
/// <para>This fixture requires explicit initialization via
1717
/// <see cref="InitializeAsync"/>. No abstract fixture base is provided for
1818
/// Swarm stacks because Swarm mode is not commonly used in unit tests.</para>
19+
/// <para>You <b>must</b> call <see cref="InitializeAsync"/> before accessing
20+
/// any properties — accessing them before initialization throws
21+
/// <see cref="InvalidOperationException"/>.</para>
1922
/// </remarks>
2023
public class XunitSwarmStackFixture : IAsyncDisposable
2124
{
2225
private SwarmStackResource _resource;
26+
private FluentDockerKernel _kernel;
2327

2428
/// <summary>
2529
/// The underlying swarm stack resource.
2630
/// </summary>
27-
public SwarmStackResource Resource => _resource;
31+
public SwarmStackResource Resource
32+
{
33+
get { EnsureInitialized(); return _resource; }
34+
}
2835

2936
/// <summary>
3037
/// The stack name used for deployment.
3138
/// </summary>
32-
public string StackName => _resource?.StackName;
39+
public string StackName
40+
{
41+
get { EnsureInitialized(); return _resource.StackName; }
42+
}
3343

3444
/// <summary>
3545
/// The kernel managing drivers.
3646
/// </summary>
37-
public FluentDockerKernel Kernel { get; private set; }
47+
public FluentDockerKernel Kernel
48+
{
49+
get { EnsureInitialized(); return _kernel; }
50+
}
3851

3952
/// <summary>
4053
/// Configures and initializes the fixture.
@@ -58,7 +71,7 @@ public async Task InitializeAsync(
5871
kernelFactory,
5972
cancellationToken: cancellationToken);
6073

61-
Kernel = kernel;
74+
_kernel = kernel;
6275
_resource = resource;
6376
}
6477

@@ -67,16 +80,20 @@ public async ValueTask DisposeAsync()
6780
{
6881
try
6982
{
70-
if (_resource != null)
71-
await _resource.DisposeAsync();
83+
await ResourceLifecycle.DisposeAsync(_resource, _kernel);
7284
}
7385
finally
7486
{
75-
if (Kernel != null)
76-
await Kernel.DisposeAsync();
7787
_resource = null;
78-
Kernel = null;
88+
_kernel = null;
7989
}
8090
}
91+
92+
private void EnsureInitialized()
93+
{
94+
if (_resource == null)
95+
throw new InvalidOperationException(
96+
"Fixture has not been initialized. Call InitializeAsync first.");
97+
}
8198
}
8299
}

0 commit comments

Comments
 (0)