Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ csharp_space_around_binary_operators = before_and_after
# ──────────────────────────────────────────────
# Expression-bodied members
# ──────────────────────────────────────────────
csharp_style_expression_bodied_methods = false:suggestion
csharp_style_expression_bodied_methods = true:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_operators = false:suggestion
csharp_style_expression_bodied_properties = true:suggestion
Expand Down
33 changes: 20 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# Changelog

## v5.0.0
### 💥 Breaking Changes

- Deleted support for Extensions, use dependency injection instead.
-
### 🐛 Bug Fixes

- Fixed a bug where SqliteInfrastructure could not delete the file because it was still used by another process. Triggering DoggyDog cleaning.

## v4.1.1

### 🛠 Technical

- Internal packages (such as NotoriousTest.Runtimes|Watchdog|SqliteRegistry|TestSettings) are now includes in NotoriousTest.
And can no longer be downloaded via Nuget Packages.


## v4.1.0

### ✨ Features
Expand All @@ -18,9 +28,6 @@
"DisableWatchdog": false
}
}
"Watchdog": {
"ManualLaunch": false
}
```

### 🛠 Technical
Expand All @@ -30,7 +37,7 @@
```json
{
"Watchdog": {
"ManualLaunch": false
"ManualLaunch": false
}
}
```
Expand All @@ -57,7 +64,7 @@ Launch DoggyDog with the --from-env flag to read those parameters.
#### DoggyDog 🐶🐶🐶 💥NEW💥

NotoriousTest now has a new mascot, the DoggyDog ! 🐶🐶🐶
DoggyDog is a watchdog that clean infrastructures that may have been left dirty by previous tests,
DoggyDog is a watchdog that clean infrastructures that may have been left dirty by previous tests,
and make sure that your tests are running in a clean environment.

- Introducing DoggyDog - an executable that clean your infrastructures left behind a test campaign that have been killed unexpectedly.
Expand All @@ -66,17 +73,17 @@ and make sure that your tests are running in a clean environment.

#### Infrastructure extensions 💥NEW💥

**Infrastructure extensions** introduce a composition model for adding behaviors to your infrastructures. Instead of creating specialized subclasses to combine multiple concerns (configuration, database seeding, respawn, etc. )
**Infrastructure extensions** introduce a composition model for adding behaviors to your infrastructures. Instead of creating specialized subclasses to combine multiple concerns (configuration, database seeding, respawn, etc. )
You register extensions on any infrastructure via `EnsureExtension<TExtension>()`.
Each extension is self-contained, reusable across infrastructures, and hooks into the infrastructure lifecycle through dedicated callbacks such as `OnBeforeInitialize`.


- Introducing a new concept called infrastructure extension, meant to be used to react to infrastructure setup.
- Introducing a new concept called infrastructure extension, meant to be used to react to infrastructure setup.
- New interface `IInfrastructureExtension`, provide hooks such as `OnBeforeInitialize` to extends Infrastructure.
- Configuration is now handled by extensions classes.
- Use `EnsureExtension<MyExtension>()` or `EnsureExtension(new MyExtension())` to register an extension.
- Built-in extensions :
- Core
- Built-in extensions :
- Core
- `OutputConfigurationExtension<TOutputConfiguration>` : Provide a way to output configuration. Included in `Infrastructure` base class.
- `SettingsExtension<TSettings>`: Load from `testsettings.json` your infrastructure configuration. Config key default to infrastructure name, and can be override.
- Database
Expand All @@ -92,8 +99,8 @@ Each extension is self-contained, reusable across infrastructures, and hooks int
- Output configuration is now handled by an extension built-in Infrastructure base class.
- Adding a configuration output will now be made by calling `AddEntry(key, config)`.
- Environment will gather all configuration under all keys and pass to all `IConfigurationConsumer` infrastructures, such as `WebApplicationInfrastructure`.
- `WebApplicationInfrastructure` now maps configuration entries to appsettings format automatically. Generating the section path from the key and config structure.
- e.g.
- `WebApplicationInfrastructure` now maps configuration entries to appsettings format automatically. Generating the section path from the key and config structure.
- e.g.
```json
// Entry: "Example:Test" → { "Host": "localhost", "Port": 5432 }
// appsettings.json
Expand Down Expand Up @@ -127,7 +134,7 @@ Each extension is self-contained, reusable across infrastructures, and hooks int
- Every framework has it's own `ITestLogger` implementation that is registered in DI.

#### Web
- Web support has been moved to `NotoriousTest.Web`.
- Web support has been moved to `NotoriousTest.Web`.
- `Environment` now has extensions to retrieve the WebApplication or add a WebApplication. You can find them in the `NotoriousTest.Web` packages, under the `NotoriousTest.Web.Environment.WebEnvironmentExtensions`.
- `WebEnvironment` has been deleted. Use extensions to retrieve/add WebApplication.

Expand All @@ -148,7 +155,7 @@ Each extension is self-contained, reusable across infrastructures, and hooks int
- Find them within `NotoriousTest.XUnit`, `NotoriousTest.NUnit`, `NotoriousTest.MSTest` and `NotoriousTest.TUnit` packages.
- New samples for every frameworks are available in the samples folder.

## v3.1.0
## v3.1.0

### ✨ Features

Expand Down
184 changes: 0 additions & 184 deletions Documentation/2-core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,190 +63,6 @@ public class MyInfrastructure : Infrastructure

---

## 🧩 Infrastructure Extensions

**Extensions** introduce a composition model for adding reusable behaviors to your infrastructures without subclassing. Instead of creating specialized subclasses for every concern (configuration output, database seeding, settings loading...), you register extensions via `EnsureExtension<T>()`.

Each extension hooks into the infrastructure lifecycle through dedicated callbacks:

```csharp
public interface IInfrastructureExtension
{
Task OnBeforeInitialize(IInfrastructure infrastructure);
Task OnAfterInitialize(IInfrastructure infrastructure);
Task OnBeforeReset(IInfrastructure infrastructure);
Task OnAfterReset(IInfrastructure infrastructure);
Task OnBeforeDestroy(IInfrastructure infrastructure);
Task OnAfterDestroy(IInfrastructure infrastructure);
}
```

Register an extension from within an infrastructure's constructor or `Initialize()`:

```csharp
public class MyInfrastructure : Infrastructure
{
public MyInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry,
ITestSettingsProvider settings)
: base(contextId, logger, registry)
{
// Register a built-in extension
EnsureExtension(new SettingsExtension<MySettings>(settings));
}
}
```

`EnsureExtension<T>()` is idempotent: if an extension of type `T` is already registered, the existing instance is returned.

---

### SettingsExtension

`SettingsExtension<TSettings>` loads configuration from a `testsettings.json` file into a typed settings object, **before** `Initialize()` runs.

**1. Create a `testsettings.json`** in your test project and set `Copy to Output Directory: PreserveNewest`:

```json
{
"MyInfrastructure": {
"Host": "localhost",
"Port": 1433
}
}
```

**2. Define a settings class:**

```csharp
public class MySettings
{
public string Host { get; set; }
public int Port { get; set; }
}
```

**3. Register the extension in your infrastructure:**

```csharp
public class MyInfrastructure : Infrastructure
{
private SettingsExtension<MySettings> _settings;

public MyInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry,
ITestSettingsProvider settingsProvider)
: base(contextId, logger, registry)
{
_settings = EnsureExtension(new SettingsExtension<MySettings>(settingsProvider));
}

public override Task Initialize()
{
// Settings are loaded before Initialize() is called
var host = _settings.Settings.Host;
var port = _settings.Settings.Port;
return Task.CompletedTask;
}
}
```

📌 **Key Points:**
- The section key defaults to the infrastructure's class name. You can override it via the `SectionName` property.
- `ITestSettingsProvider` is automatically registered in the DI container by the environment.

---

### OutputConfigurationExtension

`OutputConfigurationExtension<TConfig>` allows an infrastructure to **expose configuration** (e.g., a connection string, a base URL) that other infrastructures or the web application can consume.

It is built into the `Infrastructure<TOutputConfiguration, TMetadata>` base class, so you don't need to register it manually when you use that base class.

**Example:** an infrastructure that outputs a connection string:

```csharp
public class MyDatabaseInfrastructure : Infrastructure<ConnectionStringConfig, object>
{
public MyDatabaseInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry)
: base(contextId, logger, registry) { }

public override async Task Initialize()
{
// ... start database ...
string connectionString = "Server=localhost;Database=...";

// Expose a configuration entry — the key is free-form, interpreted by the consumer
AddEntry("ConnectionStrings:MyDb", new ConnectionStringConfig(connectionString));
}
}
```

📌 **Key Points:**
- The key is a free-form string — its meaning is entirely determined by the **consumer**.
- The environment collects all `ConfigurationEntry` objects and passes them as a flat list to every `IConfigurationConsumer` infrastructure.
- **`WebApplicationInfrastructure`** (from `NotoriousTest.Web`) is the built-in consumer: it interprets keys as appsettings paths and maps entries accordingly:

```json
// Entry key: "Example:Database" → value: { "Host": "localhost", "Port": 5432 }
// Injected into the web app as:
{
"Example": {
"Database": {
"Host": "localhost",
"Port": 5432
}
}
}
```

Any infrastructure implementing `IConfigurationConsumer` receives the same list and can interpret the keys however it needs.

---

### Custom Extensions

An extension is a self-contained class that encapsulates a specific, reusable behavior. The canonical use case is to **strongly type it against the infrastructure it targets** using `IInfrastructureExtension<T>`, giving it direct access to all infrastructure members.

**Example:** a seeding extension that runs reference data inserts after a database is initialized:

```csharp
public class ReferenceDataSeedExtension : IInfrastructureExtension<MyDatabaseInfrastructure>
{
public async Task OnAfterInitialize(MyDatabaseInfrastructure infrastructure)
{
using var connection = infrastructure.GetDatabaseConnection();
await connection.OpenAsync();

using var command = connection.CreateCommand();
command.CommandText = @"
INSERT INTO Countries (Code, Name) VALUES ('FR', 'France');
INSERT INTO Countries (Code, Name) VALUES ('DE', 'Germany');
INSERT INTO Countries (Code, Name) VALUES ('US', 'United States');
";
await command.ExecuteNonQueryAsync();
}
}
```

Register it in your infrastructure:

```csharp
public class MyDatabaseInfrastructure : Infrastructure
{
public MyDatabaseInfrastructure(EnvironmentId contextId, ITestLogger logger, IRegistry registry)
: base(contextId, logger, registry)
{
EnsureExtension<ReferenceDataSeedExtension>();
}
}
```

📌 **Key Points:**
- The extension has its own state and dependencies — it is a proper class, not a delegate wrapper.
- `IInfrastructureExtension<T>` gives the extension typed access to the infrastructure without casting.
- Any hook not overridden has a default no-op implementation — only implement what you need.

---

## ⚙️ Configuration Propagation

`OutputConfigurationExtension` and `Infrastructure<TConfig, TMetadata>` are built on top of two simple interfaces that define how infrastructures communicate configuration to each other:
Expand Down
26 changes: 12 additions & 14 deletions DoggyDog/Arguments/ArgumentParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,23 @@ public static class ArgumentsParser
return ParseInternal<T>(prop => dict.TryGetValue(prop, out var v) ? v : null);
}

public static T ParseFromEnv<T>(string envPrefix) where T : new()
{
return ParseInternal<T>(prop =>
public static T ParseFromEnv<T>(string envPrefix) where T : new() =>
ParseInternal<T>(prop =>
{
var envKey = $"{envPrefix}_{prop.Replace("-", "_").ToUpperInvariant()}";
string envKey = $"{envPrefix}_{prop.Replace("-", "_").ToUpperInvariant()}";
return Environment.GetEnvironmentVariable(envKey, EnvironmentVariableTarget.User);
});
}

private static T ParseInternal<T>(Func<string, string?> resolver) where T : new()
{
var errors = 0;
int errors = 0;
var instance = new T();
foreach (var prop in typeof(T).GetProperties())
foreach (PropertyInfo prop in typeof(T).GetProperties())
{
var attr = prop.GetCustomAttribute<CliArgumentAttribute>();
CliArgumentAttribute? attr = prop.GetCustomAttribute<CliArgumentAttribute>();
if (attr is null) continue;

var raw = resolver(attr.Name);
string? raw = resolver(attr.Name);

if (raw is null)
{
Expand All @@ -43,13 +41,13 @@ public static class ArgumentsParser

try
{
var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
Type targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;

object converted = targetType switch
{
_ when targetType == typeof(Guid) => Guid.Parse(raw.ToString()!),
_ when targetType == typeof(DateTimeOffset) => DateTimeOffset.Parse(raw.ToString()!),
_ when targetType.IsEnum => Enum.Parse(targetType, raw.ToString()!),
_ when targetType == typeof(Guid) => Guid.Parse(raw),
_ when targetType == typeof(DateTimeOffset) => DateTimeOffset.Parse(raw),
_ when targetType.IsEnum => Enum.Parse(targetType, raw),
_ when targetType == typeof(string[]) => raw.Split("|"),
_ => Convert.ChangeType(raw, targetType)
};
Expand All @@ -72,4 +70,4 @@ public static class ArgumentsParser

return instance;
}
}
}
4 changes: 1 addition & 3 deletions DoggyDog/DoggyDog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
</PropertyGroup>

<Message Text="Publishing DoggyDog to $(DoggyDogArtifactsDir)" Importance="high" />
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="Publish"
Properties="RuntimeIdentifier=$(DoggyDogRuntime);SelfContained=true;PublishSingleFile=true;PublishDir=$(DoggyDogArtifactsDir);NoBuild=true" />
<Exec Command="dotnet publish &quot;$(MSBuildProjectFullPath)&quot; -r $(DoggyDogRuntime) --no-build --self-contained true /p:PublishSingleFile=true /p:PublishDir=&quot;$(DoggyDogArtifactsDir)&quot;" />
</Target>

</Project>
Loading
Loading