Harden shutdown against logging exceptions

This commit is contained in:
2026-08-19 18:51:33 +02:00
parent 1228f71765
commit 4af64d8b41
14 changed files with 113 additions and 20 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
## Projekt und Sicherheitsziel ## Projekt und Sicherheitsziel
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.1. Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.2.
## Installerinvarianten ## Installerinvarianten
@@ -31,6 +31,7 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
- Ein älterer kompatibler Snapshot, insbesondere aus 2.1.3, darf nicht allein anhand seines `ToolVersion`-Werts abgelehnt werden. - Ein älterer kompatibler Snapshot, insbesondere aus 2.1.3, darf nicht allein anhand seines `ToolVersion`-Werts abgelehnt werden.
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot. - Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter. - Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
- Datei-Logging und optionale GUI-Logweiterleitung sind Diagnosekanäle und dürfen niemals einen fachlichen Planschritt oder dessen Fehlerbehandlung unterbrechen.
## Versionierung ## Versionierung
+7
View File
@@ -1,6 +1,13 @@
# Changelog # Changelog
## [2.2.2] - 2026-08-19
### Fixed
- A failure in the optional GUI log sink can no longer escape step-level error handling or stop later independent shutdown/restore steps.
### Added
- Failure-injection coverage for multiple consecutive unexpected exception types and a permanently failing log-display sink.
## [2.2.1] - 2026-08-19 ## [2.2.1] - 2026-08-19
### Added ### Added
- WMI-free **Validate State** preflight with snapshot metadata, server matching and artifact counts. - WMI-free **Validate State** preflight with snapshot metadata, server matching and artifact counts.
+1 -1
View File
@@ -89,7 +89,7 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
## Emergency Restore nach einem Teilabbruch ## Emergency Restore nach einem Teilabbruch
Version 2.2.1 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Version 2.2.2 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Auch ein Fehler der GUI-Logweiterleitung ist vom fachlichen Executor isoliert und kann die späteren Planschritte nicht abbrechen.
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern. 1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist. 2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
@@ -3,11 +3,11 @@
Stand: 19.08.2026 Stand: 19.08.2026
Ausführung: Donnerstag, 20.08.2026, oder Montag, 24.08.2026 Ausführung: Donnerstag, 20.08.2026, oder Montag, 24.08.2026
Zielserver: `AV23AGPWBI01` Zielserver: `AV23AGPWBI01`
Benötigte Toolversion: `2.2.1` Benötigte Toolversion: `2.2.2`
## Kurzantwort ## Kurzantwort
Ja. Die vorhandene, mit Version 2.1.3 erzeugte `before.json` kann von Version 2.2.1 direkt als Recovery-Quelle verwendet werden. Zusätzliche Dateien des abgebrochenen Shutdowns sind nicht erforderlich. Die Kompatibilität wird durch einen Regressionstest abgedeckt; die echte BizTalk-/ACC-Ausführung muss noch vor Ort bestätigt werden. Ja. Die vorhandene, mit Version 2.1.3 erzeugte `before.json` kann von Version 2.2.2 direkt als Recovery-Quelle verwendet werden. Zusätzliche Dateien des abgebrochenen Shutdowns sind nicht erforderlich. Die Kompatibilität wird durch einen Regressionstest abgedeckt; die echte BizTalk-/ACC-Ausführung muss noch vor Ort bestätigt werden.
Für diesen Teilzustand ausschließlich **Emergency Restore** verwenden. Dieser Modus startet zuerst `ENTSSO`, arbeitet danach den gespeicherten Sollzustand idempotent ab, setzt nach Einzelfehlern fort und überschreibt die Quell-`before.json` nicht. Für diesen Teilzustand ausschließlich **Emergency Restore** verwenden. Dieser Modus startet zuerst `ENTSSO`, arbeitet danach den gespeicherten Sollzustand idempotent ab, setzt nach Einzelfehlern fort und überschreibt die Quell-`before.json` nicht.
@@ -31,7 +31,7 @@ type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
- [ ] Nur fortfahren, wenn beide ZIP-Hashes exakt gleich sind. - [ ] Nur fortfahren, wenn beide ZIP-Hashes exakt gleich sind.
- [ ] ZIP in einen neuen Ordner entpacken, die laufende Toolinstanz schließen und `Setup.exe` als Administrator starten. - [ ] ZIP in einen neuen Ordner entpacken, die laufende Toolinstanz schließen und `Setup.exe` als Administrator starten.
- [ ] Installation/Update auf Version `2.2.1` vollständig abschließen. - [ ] Installation/Update auf Version `2.2.2` vollständig abschließen.
- [ ] Ein neues, leeres Ausgabeverzeichnis anlegen, zum Beispiel `C:\BizTalk-Recovery\2026-08-20` oder `C:\BizTalk-Recovery\2026-08-24`. - [ ] Ein neues, leeres Ausgabeverzeichnis anlegen, zum Beispiel `C:\BizTalk-Recovery\2026-08-20` oder `C:\BizTalk-Recovery\2026-08-24`.
Wichtig: Die Recovery-Quelle außerhalb dieses Ausgabeverzeichnisses lassen. Während der Recovery nicht **Snapshot Before** anklicken, damit kein neuer Teilzustand als vermeintlicher Sollzustand gespeichert wird. Wichtig: Die Recovery-Quelle außerhalb dieses Ausgabeverzeichnisses lassen. Während der Recovery nicht **Snapshot Before** anklicken, damit kein neuer Teilzustand als vermeintlicher Sollzustand gespeichert wird.
@@ -86,7 +86,7 @@ Die reale Reihenfolge ist:
4. Orchestrations abgleichen; im Snapshot nur `Bound` gebliebene Orchestrations werden bewusst zur manuellen Kontrolle markiert. 4. Orchestrations abgleichen; im Snapshot nur `Bound` gebliebene Orchestrations werden bewusst zur manuellen Kontrolle markiert.
5. Receive Locations zuletzt abgleichen. 5. Receive Locations zuletzt abgleichen.
Vor jeder Mutation liest Version 2.2.1 den Istzustand. Ein bereits korrekter Zustand wird als `AlreadySatisfied` protokolliert und nicht erneut per WMI geändert. Ein einzelner Fehler wird als `Failed` gespeichert; alle späteren unabhängigen Schritte werden trotzdem versucht. Vor jeder Mutation liest Version 2.2.2 den Istzustand. Ein bereits korrekter Zustand wird als `AlreadySatisfied` protokolliert und nicht erneut per WMI geändert. Ein einzelner Fehler wird als `Failed` gespeichert; alle späteren unabhängigen Schritte werden trotzdem versucht. Auch ein Fehler der optionalen GUI-Loganzeige kann die fachliche Ausführung nicht unterbrechen.
## 5. Ergebnis abnehmen ## 5. Ergebnis abnehmen
@@ -112,8 +112,8 @@ Wenn sie deaktiviert ist, obwohl `before.json` sie aktiviert erwartet, versucht
## Wenn der Lauf erneut unterbrochen wird ## Wenn der Lauf erneut unterbrochen wird
Die ursprüngliche `before.json` bleibt unverändert. Version 2.2.1 kann mit derselben Recovery-Quelle erneut gestartet werden, weil bereits erreichte Zustände als `AlreadySatisfied` übersprungen werden. Vor dem Wiederholen immer den jüngsten Ergebnisreport sichern und prüfen, welcher Schritt tatsächlich fehlgeschlagen ist. Die ursprüngliche `before.json` bleibt unverändert. Version 2.2.2 kann mit derselben Recovery-Quelle erneut gestartet werden, weil bereits erreichte Zustände als `AlreadySatisfied` übersprungen werden. Vor dem Wiederholen immer den jüngsten Ergebnisreport sichern und prüfen, welcher Schritt tatsächlich fehlgeschlagen ist.
## Go/No-Go für PROD ## Go/No-Go für PROD
ACC ist erst ein belastbarer PROD-Nachweis, wenn der echte Emergency Restore abgeschlossen wurde, der Ergebnisreport vollständig ist, der automatische Diff geprüft wurde und jeder verbliebene `Failed`-Schritt fachlich behandelt wurde. Die lokalen 24 Regressionstests und der Self-Test ersetzen diesen realen BizTalk-/Windows-Nachweis nicht. ACC ist erst ein belastbarer PROD-Nachweis, wenn der echte Emergency Restore abgeschlossen wurde, der Ergebnisreport vollständig ist, der automatische Diff geprüft wurde und jeder verbliebene `Failed`-Schritt fachlich behandelt wurde. Die lokalen 27 Regressionstests und der Self-Test ersetzen diesen realen BizTalk-/Windows-Nachweis nicht.
@@ -55,6 +55,10 @@ Der neue Button **Emergency Restore** benötigt außer der erhaltenen `before.js
- Ein 2.1.3-Snapshot wird ausdrücklich als kompatible Recovery-Quelle regressionsgetestet; `ToolVersion` ist keine Versionssperre. - Ein 2.1.3-Snapshot wird ausdrücklich als kompatible Recovery-Quelle regressionsgetestet; `ToolVersion` ist keine Versionssperre.
- Nach einem echten Emergency Restore wird bei erfolgreichem Nachher-Snapshot automatisch `emergency-restore-diff-*` als JSON, CSV und HTML geschrieben. - Nach einem echten Emergency Restore wird bei erfolgreichem Nachher-Snapshot automatisch `emergency-restore-diff-*` als JSON, CSV und HTML geschrieben.
## Zusätzliche Exception-Grenze in Version 2.2.2
Ein erneuter Audit des gesamten Shutdown-Kontrollflusses bestätigte, dass WMI-Lookup, Istzustandsprüfung, Methodenaufruf, Zustandswartezeit und Objektfreigabe innerhalb der Per-Step-Exception-Grenze liegen. Zusätzlich ist nun auch die optionale GUI-Weiterleitung des Loggers vollständig isoliert: Selbst wenn die Anzeige eines Logeintrags eine Exception wirft, laufen der aktuelle fachliche Schritt und alle späteren unabhängigen Schritte weiter. Failure-Injection-Tests decken mehrere aufeinanderfolgende `InvalidOperationException`-, `IOException`- und `TimeoutException`-Fehler sowie einen dauerhaft werfenden Log-Sink ab.
### Globale Reihenfolge ### Globale Reihenfolge
Shutdown: Shutdown:
@@ -86,11 +90,11 @@ Die portable Regressionstestsuite enthält ab Version 2.2.0 unter anderem folgen
- globale Shutdown-/Restore-Reihenfolge mit mehreren Anwendungen, - globale Shutdown-/Restore-Reihenfolge mit mehreren Anwendungen,
- Kurzname/FQDN-Gleichheit für Host-Instance-Schritte. - Kurzname/FQDN-Gleichheit für Host-Instance-Schritte.
Die lokale Suite umfasst ab Version 2.2.1 insgesamt 24 Tests einschließlich der expliziten 2.1.3-Snapshot-Kompatibilität. Die abschließende Freigabe für PROD benötigt weiterhin einen Windows-/BizTalk-Test auf ACC: echter Scheduler-Fehlerpfad, vollständige Fortsetzung, Ergebnisreport, Emergency-Dry-run und realer Wiederanlauf aus einer Kopie der vorhandenen `before.json`. Die lokale Suite umfasst ab Version 2.2.2 insgesamt 27 Tests einschließlich der expliziten 2.1.3-Snapshot-Kompatibilität, zusätzlicher Failure-Injection für unerwartete Exceptions und eines vollständigen Shutdown-Fortsetzungstests über Receive Location, Orchestration, Send Port und Host Instance. Die abschließende Freigabe für PROD benötigt weiterhin einen Windows-/BizTalk-Test auf ACC: echter Scheduler-Fehlerpfad, vollständige Fortsetzung, Ergebnisreport, Emergency-Dry-run und realer Wiederanlauf aus einer Kopie der vorhandenen `before.json`.
## PROD-Freigabekriterien ## PROD-Freigabekriterien
- Version 2.2.1 installieren und Installer-Self-Test bestätigen. - Version 2.2.2 installieren und Installer-Self-Test bestätigen.
- `Diagnose` und einen vollständigen Shutdown-Dry-run ausführen. - `Diagnose` und einen vollständigen Shutdown-Dry-run ausführen.
- Kontrolliert nachweisen, dass eine isolierte Scheduler-Exception spätere Schritte nicht blockiert. - Kontrolliert nachweisen, dass eine isolierte Scheduler-Exception spätere Schritte nicht blockiert.
- `shutdown-result.json` und Nachher-Snapshot prüfen. - `shutdown-result.json` und Nachher-Snapshot prüfen.
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool"; private const string ProductName = "BizTalk Platform Management Tool";
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary> /// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
private const string ProductVersion = "2.2.1"; private const string ProductVersion = "2.2.2";
/// <summary> /// <summary>
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen. /// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{ {
AutoSize = true, AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold), Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.2.1" Text = "BizTalk Platform Management Tool 2.2.2"
}); });
root.Controls.Add(new Label root.Controls.Add(new Label
{ {
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")] [assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")] [assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
[assembly: AssemblyVersion("2.2.1.0")] [assembly: AssemblyVersion("2.2.2.0")]
[assembly: AssemblyFileVersion("2.2.1.0")] [assembly: AssemblyFileVersion("2.2.2.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")] [assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.2.1.0" name="BizTalkPlatformManagementTool.Setup" /> <assemblyIdentity version="2.2.2.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security> <security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo> </trustInfo>
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")] [assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.2.1.0")] [assembly: AssemblyVersion("2.2.2.0")]
[assembly: AssemblyFileVersion("2.2.1.0")] [assembly: AssemblyFileVersion("2.2.2.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")] [assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary> /// <summary>
/// Current tool version written into generated snapshots. /// Current tool version written into generated snapshots.
/// </summary> /// </summary>
public const string Version = "2.2.1-net461"; public const string Version = "2.2.2-net461";
/// <summary> /// <summary>
/// Fallback application name used when WMI does not expose an application property. /// Fallback application name used when WMI does not expose an application property.
@@ -171,8 +171,23 @@ namespace BizTalkPlatformManagementTool.Services
return; return;
} }
try
{
_sink(entry); _sink(entry);
} }
catch (Exception ex)
{
// Auch die optionale GUI-Weiterleitung ist nur ein Diagnosekanal. Ein Fehler
// dort darf insbesondere nicht die Exception-Behandlung eines BizTalk-Schritts
// erneut unterbrechen und dadurch alle späteren Planschritte verhindern.
WriteToFile(new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Warning,
Message = "Log display sink failed; runtime operation continues. Error: " + ex.Message
});
}
}
/// <summary> /// <summary>
/// Appends one entry to the current daily log file. /// Appends one entry to the current daily log file.
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.2.1.0" name="BizTalkPlatformManagementTool" /> <assemblyIdentity version="2.2.2.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
@@ -27,9 +27,12 @@ namespace BizTalkPlatformManagementTool.Tests
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery); Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder); Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder); Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
Run("ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure", ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure);
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst); Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer); Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure); Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
Run("PlanExecutionContinuesAfterMultipleUnexpectedExceptions", PlanExecutionContinuesAfterMultipleUnexpectedExceptions);
Run("LoggerSinkFailureCannotAbortPlanExecution", LoggerSinkFailureCannotAbortPlanExecution);
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState); Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure); Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues); Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
@@ -163,6 +166,22 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(lastSendPort < firstHost, "host instances were not globally last"); Assert(lastSendPort < firstHost, "host instances were not globally last");
} }
/// <summary>Prüft den vollständigen Shutdown-Fortgang von Receive Location bis Host Instance nach einem frühen Fehler.</summary>
private static void ShutdownContinuesAcrossArtifactCategoriesAfterReceiveLocationFailure()
{
var snapshot = Snapshot("APP", "SEND", ArtifactStates.SendPortStarted);
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RV_PMP_Trigger_Schedule", Enabled = true });
snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCHESTRATION", OrchestrationStatus = ArtifactStates.OrchestrationStarted });
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" };
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
Assert(runtime.Calls.SequenceEqual(new[] { "RV_PMP_Trigger_Schedule", "ORCHESTRATION", "SEND", "HOST:SERVER" }), "shutdown did not continue through every later artifact category");
Assert(report.FailedCount == 1 && report.SucceededCount == 3, "cross-category shutdown outcome is incomplete");
}
/// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary> /// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary>
private static void EmergencyRestorePlanStartsSsoFirst() private static void EmergencyRestorePlanStartsSsoFirst()
{ {
@@ -202,6 +221,27 @@ namespace BizTalkPlatformManagementTool.Tests
Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "nested Scheduler exception missing from report"); Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "nested Scheduler exception missing from report");
} }
/// <summary>Prüft die Fortsetzung auch bei mehreren unterschiedlichen, nicht adapterspezifischen Exceptions.</summary>
private static void PlanExecutionContinuesAfterMultipleUnexpectedExceptions()
{
var runtime = new MultipleFailureOperationStepRuntime();
var report = new OperationPlanExecutor(null).Execute(TestPlan("INVALID", "IO", "TIMEOUT", "CONTINUE"), TestOptions(false), runtime);
Assert(runtime.Calls.SequenceEqual(new[] { "INVALID", "IO", "TIMEOUT", "CONTINUE" }), "an unexpected exception stopped later independent steps");
Assert(report.FailedCount == 3 && report.SucceededCount == 1, "multiple failure summary is incomplete");
Assert(report.Steps.All(x => !string.IsNullOrWhiteSpace(x.FinishedAt)), "a failure result has no completion timestamp");
}
/// <summary>Prüft, dass selbst eine defekte GUI-Logweiterleitung keine fachlichen Schritte verhindert.</summary>
private static void LoggerSinkFailureCannotAbortPlanExecution()
{
var logger = new OperationLogger(entry => { throw new InvalidOperationException("simulated UI sink failure"); });
var runtime = new FakeOperationStepRuntime { FailingName = "TWO" };
var report = new OperationPlanExecutor(logger).Execute(TestPlan("ONE", "TWO", "THREE"), TestOptions(false), runtime);
Assert(runtime.Calls.SequenceEqual(new[] { "ONE", "TWO", "THREE" }), "logging failure stopped plan execution");
Assert(report.SucceededCount == 2 && report.FailedCount == 1, "logging failure changed business outcomes");
Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "business failure was lost while the log sink was failing");
}
/// <summary>Prüft idempotentes Überspringen eines bereits erreichten Sollzustands.</summary> /// <summary>Prüft idempotentes Überspringen eines bereits erreichten Sollzustands.</summary>
private static void PlanExecutionSkipsAlreadySatisfiedState() private static void PlanExecutionSkipsAlreadySatisfiedState()
{ {
@@ -610,6 +650,32 @@ namespace BizTalkPlatformManagementTool.Tests
} }
} }
/// <summary>Wirft verschiedene Exception-Typen und liefert danach wieder ein erfolgreiches Ergebnis.</summary>
private sealed class MultipleFailureOperationStepRuntime : IOperationStepRuntime
{
/// <summary>Initialisiert die vollständige Aufrufliste.</summary>
public MultipleFailureOperationStepRuntime()
{
Calls = new List<string>();
}
/// <summary>Gets all attempted step names.</summary>
public List<string> Calls { get; private set; }
/// <summary>Wirft je nach Testschritt eine andere unerwartete Ausnahme.</summary>
/// <param name="step">Der auszuführende Testschritt.</param>
/// <param name="options">Die ungenutzten Testoptionen.</param>
/// <returns>Erfolg für den abschließenden Fortsetzungsschritt.</returns>
public RuntimeStepOutcome Execute(OperationStep step, OperationOptions options)
{
Calls.Add(step.Name);
if (step.Name == "INVALID") throw new InvalidOperationException("unexpected state error");
if (step.Name == "IO") throw new IOException("unexpected provider I/O error");
if (step.Name == "TIMEOUT") throw new TimeoutException("unexpected provider timeout");
return RuntimeStepOutcome.Succeeded;
}
}
/// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary> /// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary>
/// <param name="application">Der Name der Testanwendung.</param> /// <param name="application">Der Name der Testanwendung.</param>
/// <param name="port">Der Name des Test-Send-Ports.</param> /// <param name="port">Der Name des Test-Send-Ports.</param>