Files
IBKRTrader/tests/IBKRTrader.Tests/Trading/CultureIndependenceTests.cs
T
RichardandClaude Opus 5 c87b0195fd Logdateinamen haengen nicht mehr am Kalender der Host-Kultur
Im Logverzeichnis der Test-VM lagen neben 2026-09-03.jsonl auch 1405-06-12.jsonl
und 2569-09-03.jsonl - dieselben Tage im Sonnen-Hidschra- und im buddhistischen
Kalender. Eine interpolierte Zeichenkette formatiert mit CurrentCulture, und die
bringt ihren Kalender mit. Betroffen waren beide Schreibpfade des LoggingService
und die Lesestelle im DossierService, die denselben Namen wieder zusammensetzt.

CA1305 greift hier nicht: die Regel sieht string.Format und ToString, nicht die
interpolierte Form. Die .editorconfig deckt diesen Fall also nicht ab.

FormattableString.Invariant an allen drei Stellen, dazu ein Regressionstest ueber
fa-IR, th-TH und de-DE - mit Gegenbeweis, dass die naive Form tatsaechlich
abweicht, damit der Test nicht eines Tages unbemerkt trivial wird.

Ausserdem: Verweise auf die Bugtracker-Nummern richtiggestellt (488/489).

461 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:31:44 +02:00

154 lines
5.4 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.Globalization;
using FluentAssertions;
using IBKRTrader.Core.Analytics;
using IBKRTrader.Core.Persistence.Entities;
using IBKRTrader.Core.Trading;
namespace IBKRTrader.Tests.Trading;
/// <summary>
/// Zahlen und Zeitpunkte, die in Logs, Dossiers oder in die Datenbank gehen, dürfen nicht an der
/// Kultur des Hosts hängen. Wir betreiben Instanzen in EU und US und vergleichen deren Ausgaben
/// miteinander – "373,67" auf der einen und "373.67" auf der anderen wäre dasselbe Ereignis in
/// zwei Schreibweisen.
///
/// <para>Die Tests setzen bewusst eine Kultur mit Dezimalkomma und prüfen, dass sich davon nichts
/// beeindrucken lässt. Der laufende Prozess stellt zusätzlich die invariante Kultur ein
/// (AppHostBuilder), aber genau darauf soll sich der einzelne Codepfad <b>nicht</b> verlassen.</para>
/// </summary>
[Trait("cat", "unit")]
public class CultureIndependenceTests
{
/// <summary>Führt die Prüfung unter einer Kultur mit Dezimalkomma aus.</summary>
private static void UnderGermanCulture(Action assert)
{
var previous = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
assert();
}
finally
{
CultureInfo.CurrentCulture = previous;
}
}
private static BrokerPosition Position() => new()
{
Symbol = "GOOG",
SecType = "STK",
Currency = "USD",
ConId = 208813720,
Quantity = 10,
AvgCost = 373.67m
};
[Fact]
public void BrokerPosition_ToString_UsesInvariantNumbers()
{
UnderGermanCulture(() =>
Position().ToString().Should().Be("GOOG (STK) 10 @ 373.67 USD"));
}
[Fact]
public void BrokerExecution_ToString_UsesInvariantNumbers()
{
var execution = new BrokerExecution
{
ExecId = "0001",
Time = new DateTime(2026, 9, 3, 18, 26, 13, DateTimeKind.Utc),
Symbol = "AAPL",
SecType = "STK",
Side = TradeSide.Buy,
Quantity = 5,
Price = 327.27m
};
UnderGermanCulture(() =>
execution.ToString().Should().Be("2026-09-03 18:26:13 Buy 5x AAPL @ 327.27"));
}
[Fact]
public void Dossier_WritesInvariantNumbers()
{
var dossier = new TradeDossier(
SignalId: "sig-1",
Decisions: [],
OrderEvents: [],
Trades:
[
new CoreTrade
{
Module = "CT",
Symbol = "AAPL",
Action = "BUY",
Quantity = 3,
Price = 327.27m,
TotalValue = 981.81m,
Status = "Filled",
TradedAt = new DateTime(2026, 9, 3, 18, 26, 13, DateTimeKind.Utc)
}
],
LogLines: []);
UnderGermanCulture(() =>
{
var markdown = DossierBuilder.ToMarkdown(dossier);
markdown.Should().Contain("327.27").And.Contain("981.81");
markdown.Should().NotContain("327,27");
});
}
// ─── Dateinamen (#489) ────────────────────────────────────────────────
/// <summary>
/// Führt die Prüfung unter einer Kultur mit <b>anderem Kalender</b> aus. Das Dezimalkomma ist
/// nur die halbe Gefahr: fa-IR rechnet im Sonnen-Hidschra-Kalender, th-TH im buddhistischen.
/// </summary>
private static void UnderCulture(string name, Action assert)
{
var previous = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(name);
assert();
}
finally
{
CultureInfo.CurrentCulture = previous;
}
}
[Theory]
[InlineData("fa-IR")] // Sonnen-Hidschra: Jahr 1405 statt 2026
[InlineData("th-TH")] // buddhistisch: Jahr 2569 statt 2026
[InlineData("de-DE")]
public void LogFileNames_DoNotDependOnTheHostCalendar(string culture)
{
// Am 2026-09-06 lagen auf der Test-VM neben 2026-09-06.jsonl auch 1405-06-12.jsonl und
// 2569-09-03.jsonl. Eine interpolierte Zeichenkette formatiert mit CurrentCulture, und die
// bringt ihren Kalender mit: Der Supervisor sucht dann eine Datei, die es nicht gibt, und
// die Tagesrotation legt jeden Tag eine neue Reihe an.
var day = new DateTime(2026, 9, 6, 14, 3, 0, DateTimeKind.Utc);
UnderCulture(culture, () =>
{
FormattableString.Invariant($"{day:yyyy-MM-dd}.jsonl").Should().Be("2026-09-06.jsonl");
FormattableString.Invariant($"Info-{day:dd-MM-yy}.txt").Should().Be("Info-06-09-26.txt");
});
}
[Fact]
public void TheNaiveInterpolation_IsWhatWentWrong()
{
// Der Gegenbeweis, damit dieser Test nicht versehentlich trivial wird: Ohne den
// ausdruecklichen Formatanbieter geht es tatsaechlich schief. Faellt diese Zusicherung
// eines Tages, ist die Gefahr weg - dann darf der Test verschwinden, nicht vorher.
var day = new DateTime(2026, 9, 6, 0, 0, 0, DateTimeKind.Utc);
UnderCulture("fa-IR", () => $"{day:yyyy-MM-dd}".Should().NotBe("2026-09-06"));
}
}