Files
IBKRTrader/IBKRTrader.Tests/Workers/WorkerBaseTests.cs
T
Richard af398353f0 @
Phase 1: Modul-System formalisiert (IModule + ModuleRegistry)

- Core/Modules/IModule.cs: Vertrag (Key, DisplayName, Description, Version,
  RegisterServices, InitializeAsync, GetWorkers, CreateWindow)
- Core/Modules/ModuleRegistry.cs
- CongressTradingModule auf IModule umgestellt (instanzbasiert, Platzhalter-Fenster)
- Program.cs iteriert ueber IModule[] + Registry; Form1 initialisiert Module ueber Registry
- Testbarkeit: WorkerBase DB-Log-Seam (virtuell); CapitolTradesScraper.ParseTradesFromHtml
  extrahiert (offline gegen ct_raw.html testbar)
- Tests: ModuleRegistry, CongressTradingModule, WorkerBase, CapitolTradesScraper -> 15/15 gruen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-27 10:07:47 +02:00

122 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using FluentAssertions;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Workers;
namespace IBKRTrader.Tests.Workers;
[Trait("cat", "unit")]
public class WorkerBaseTests
{
/// <summary>
/// Testbarer Worker: überschreibt den DB-Log-Seam (kein MySQL-Zugriff) und
/// zählt seine Läufe. Zustände werden per Polling geprüft, weil WorkerBase
/// Info.Status intern (und nach dem Log-Abschluss) setzt.
/// </summary>
private sealed class TestWorker : WorkerBase
{
private readonly TimeSpan? _interval;
private readonly Func<CancellationToken, Task> _body;
public int Runs;
public override string Name => "TestWorker";
public override string Module => "TEST";
protected override TimeSpan? Interval => _interval;
public TestWorker(TimeSpan? interval, Func<CancellationToken, Task> body)
: base(new LoggingService(),
new DatabaseService(new SettingsService(), new LoggingService()))
{
_interval = interval;
_body = body;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
Interlocked.Increment(ref Runs);
await _body(ct);
}
// Seam überschreiben → kein DB-Zugriff. Positive Id, damit die End-Log-Aufrufe
// (in WorkerBase mit `logId > 0` geschützt) auch im Fehlerpfad laufen.
protected override Task<long> BeginRunLogAsync() => Task.FromResult(1L);
protected override Task EndRunLogAsync(long logId, bool success, string? message = null)
=> Task.CompletedTask;
}
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
private static async Task WaitUntilAsync(Func<bool> condition, string because)
{
var sw = Stopwatch.StartNew();
while (!condition())
{
if (sw.Elapsed > Timeout)
throw new TimeoutException($"Bedingung nicht innerhalb {Timeout.TotalSeconds}s erfüllt: {because}");
await Task.Delay(15);
}
}
[Fact]
public async Task RunsOnce_WhenIntervalIsNull_AndReportsIdle()
{
var worker = new TestWorker(interval: null, _ => Task.CompletedTask);
await worker.StartAsync(CancellationToken.None);
// Hinweis: WorkerBase startet bereits im Zustand Idle deshalb auf den
// abgeschlossenen Lauf warten (Runs == 1 UND wieder Idle).
await WaitUntilAsync(
() => worker.Runs == 1 && worker.Info.Status == WorkerStatus.Idle,
"ein Lauf ist abgeschlossen und Status zurück auf Idle");
worker.Runs.Should().Be(1);
await worker.StopAsync();
}
[Fact]
public async Task Trigger_ForcesImmediateRun_BeforeIntervalElapses()
{
// Langes Intervall → ein zweiter Lauf kann nur durch Trigger entstehen.
var worker = new TestWorker(TimeSpan.FromMinutes(10), _ => Task.CompletedTask);
await worker.StartAsync(CancellationToken.None);
await WaitUntilAsync(() => worker.Runs == 1, "erster Lauf erfolgt");
await worker.TriggerAsync();
await WaitUntilAsync(() => worker.Runs == 2, "Trigger löst zweiten Lauf aus");
await worker.StopAsync();
}
[Fact]
public async Task Exception_SetsStatusError_AndCapturesMessage()
{
var worker = new TestWorker(interval: null,
_ => throw new InvalidOperationException("boom"));
await worker.StartAsync(CancellationToken.None);
await WaitUntilAsync(() => worker.Info.Status == WorkerStatus.Error, "Status wird Error");
worker.Runs.Should().Be(1);
worker.Info.Info.Should().Contain("boom");
await worker.StopAsync();
}
[Fact]
public async Task Stop_SetsStatusStopped()
{
var worker = new TestWorker(TimeSpan.FromMinutes(10), _ => Task.CompletedTask);
await worker.StartAsync(CancellationToken.None);
await WaitUntilAsync(() => worker.Runs == 1, "erster Lauf erfolgt");
await worker.StopAsync();
worker.Info.Status.Should().Be(WorkerStatus.Stopped);
}
}