Kultur - Ausgaben und Parsen haengen nicht mehr am Host:
- PdfExporter formatierte Betraege mit ToString("N2") ohne Formatanbieter,
also CurrentCulture. Auf dem deutschen Desktop "1.234,56", in einem
Container mit LANG=C "1,234.56" - dieselbe Zahl, fuer einen Leser eine
andere. Fuer ein ausdruecklich pruefbares Dokument jetzt fest de-DE.
- CapitolTradesScraper.ParseDate nutzte DateOnly.TryParse ohne
Formatanbieter. Das ist nicht theoretisch: gemessen wurde aus
"2026-08-04" unter th-TH das Jahr 1483 (buddhistischer Kalender), unter
fa-IR das Jahr 2647 (persischer Kalender), unter ar-SA schlug das Parsen
ganz fehl. de-DE und en-US kommen mit ISO klar - genau deshalb faellt so
etwas auf dem Entwicklungsrechner nie auf. Jetzt TryParseExact mit
InvariantCulture; ein Formatwechsel der Quelle faellt damit auf, statt
still ein falsches Datum zu erzeugen. Regressionstest ueber vier Kulturen.
- IBKRGatewayService baute den Query-Parameter mit .ToString().ToLower()
(Tuerkisch-I) - jetzt fest "true"/"false".
PDF-Schriften: PDFsharp 6 loest auf Nicht-Windows-Plattformen nichts von
selbst auf, "Segoe UI" gibt es dort nicht - der Export waere zur Laufzeit
gescheitert. Neuer DocumentFontResolver: unter Windows bleibt die Plattform
zustaendig (unveraenderte Optik), auf Linux wird eine freie Systemschrift
gesucht (DejaVu/Liberation/Noto/FreeSans). Bewusst keine Schrift im Repo -
das erspart eine Lizenzfrage; fehlt sie, nennt die Fehlermeldung das zu
installierende Paket.
BackupWorker:
- Suchte "mysqldump.exe" in C:\Program Files\... und splittete PATH mit ';'.
Auf Linux ist das Trennzeichen ':' - der gesamte PATH waere als ein
Eintrag gelesen worden. Jetzt Path.PathSeparator, plattformabhaengige
Suchpfade und zusaetzlich "mariadb-dump" (MariaDB hat mysqldump ab 10.5
umbenannt).
- Das DB-Passwort stand als Kommandozeilenargument im Prozessbaum. Unter
Linux ist /proc/<pid>/cmdline fuer jeden lokalen Nutzer lesbar - das waere
eine neue Offenlegung gewesen, die es unter Windows so nicht gab. Jetzt
ueber MYSQL_PWD, nur an den Kindprozess vererbt. Argumente einzeln statt
als Zeichenkette (kein Quoting-Problem bei Pfaden mit Leerzeichen).
Verifiziert: 188 Tests gruen (+5), Build 0 Fehler/0 Warnungen, Core + 3
Module + Tests bauen fuer linux-x64, --smoke-ui konstruiert alle 7 Fenster.
Offen aus L1b und nach L2 verschoben: IAppPaths (Logs/Backups/settings.json/
master.key liegen neben der Binaerdatei; unter /opt ist das nicht schreibbar).
Gehoert zum Daemon, wo die Pfade tatsaechlich gebraucht werden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
3.6 KiB
C#
94 lines
3.6 KiB
C#
using System.Globalization;
|
||
using FluentAssertions;
|
||
using IBKRTrader.Core.Logging;
|
||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||
|
||
namespace IBKRTrader.Tests.Modules;
|
||
|
||
[Trait("cat", "unit")]
|
||
public class CapitolTradesScraperTests
|
||
{
|
||
private static string LoadFixture()
|
||
{
|
||
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "ct_raw.html");
|
||
File.Exists(path).Should().BeTrue($"Fixture fehlt: {path}");
|
||
return File.ReadAllText(path);
|
||
}
|
||
|
||
[Fact]
|
||
public void ParseTradesFromHtml_ExtractsTradesFromRscStream()
|
||
{
|
||
var scraper = new CapitolTradesScraper(new LoggingService());
|
||
|
||
var (trades, memberIds) = scraper.ParseTradesFromHtml(LoadFixture());
|
||
|
||
trades.Should().NotBeEmpty("die Fixture enthält einen RSC-Datenstrom mit Trades");
|
||
memberIds.Should().NotBeEmpty();
|
||
|
||
var first = trades[0];
|
||
first.TradeId.Should().NotBeNullOrWhiteSpace();
|
||
first.TradeType.Should().NotBeNullOrWhiteSpace();
|
||
}
|
||
|
||
[Fact]
|
||
public void ParseTradesFromHtml_MapsMemberSnapshotConsistently()
|
||
{
|
||
var scraper = new CapitolTradesScraper(new LoggingService());
|
||
|
||
var (trades, memberIds) = scraper.ParseTradesFromHtml(LoadFixture());
|
||
|
||
// Jeder Member-Snapshot muss eine BioId haben, die auch im MemberBioIds-Set liegt.
|
||
foreach (var trade in trades.Where(t => t.MemberSnapshot is not null))
|
||
{
|
||
trade.MemberSnapshot!.BioId.Should().NotBeNullOrWhiteSpace();
|
||
memberIds.Should().Contain(trade.MemberBioId);
|
||
}
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("de-DE")] // Tag.Monat.Jahr
|
||
[InlineData("th-TH")] // buddhistischer Kalender
|
||
[InlineData("fa-IR")] // persischer Kalender
|
||
[InlineData("ar-SA")] // Hidschri-Kalender
|
||
[InlineData("")] // Invariant – so verhält sich ein Container ohne LANG
|
||
public void ParseTradesFromHtml_LiefertDieselbenDaten_UnabhaengigVonDerHostKultur(string culture)
|
||
{
|
||
// Die Datumsfelder kamen ueber DateOnly.TryParse OHNE Formatanbieter herein, also mit der
|
||
// Kultur des Rechners. Bei Kulturen mit eigenem Kalender ist das nicht theoretisch:
|
||
// aus "2026-08-04" wurde unter th-TH das Jahr 1483 und unter fa-IR das Jahr 2647,
|
||
// unter ar-SA schlug das Parsen ganz fehl. de-DE und en-US kommen mit ISO klar - genau
|
||
// deshalb faellt so ein Fehler auf dem Entwicklungsrechner nie auf.
|
||
var previous = CultureInfo.CurrentCulture;
|
||
try
|
||
{
|
||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
|
||
|
||
var (trades, _) = new CapitolTradesScraper(new LoggingService()).ParseTradesFromHtml(LoadFixture());
|
||
|
||
trades.Should().NotBeEmpty();
|
||
trades.Should().Contain(t => t.TradeDate != null,
|
||
"die Fixture enthaelt Handelsdaten im Format yyyy-MM-dd");
|
||
|
||
// Alle geparsten Daten muessen plausibel sein – ein kulturbedingter Tag/Monat-Dreher
|
||
// faellt hier auf, weil Tage > 12 sonst gar nicht parsen wuerden.
|
||
foreach (var t in trades.Where(t => t.TradeDate != null))
|
||
t.TradeDate!.Value.Year.Should().BeInRange(2000, 2100);
|
||
}
|
||
finally
|
||
{
|
||
CultureInfo.CurrentCulture = previous;
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void ParseTradesFromHtml_EmptyHtml_ReturnsNoTrades()
|
||
{
|
||
var scraper = new CapitolTradesScraper(new LoggingService());
|
||
|
||
var (trades, memberIds) = scraper.ParseTradesFromHtml("<html><body>nichts</body></html>");
|
||
|
||
trades.Should().BeEmpty();
|
||
memberIds.Should().BeEmpty();
|
||
}
|
||
}
|