Improve installer diagnostics and rollback reporting

This commit is contained in:
2026-08-11 11:52:34 +02:00
parent a74528e5c6
commit bb2d2f8484
14 changed files with 746 additions and 97 deletions
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -25,6 +26,9 @@ namespace BizTalkPlatformManagementTool.Tests
Run("InstallerDoesNotMutateOnStagingFailure", InstallerDoesNotMutateOnStagingFailure);
Run("InstallerRollsBackFailedActivatedSelfTest", InstallerRollsBackFailedActivatedSelfTest);
Run("InstallerUninstallRemovesProgramDirectory", InstallerUninstallRemovesProgramDirectory);
Run("InstallerDiagnosticLogContainsContextAndExceptionChain", InstallerDiagnosticLogContainsContextAndExceptionChain);
Run("InstallerDiagnosticLogFallsBackToTemp", InstallerDiagnosticLogFallsBackToTemp);
Run("InstallerSurvivesUiReportFailure", InstallerSurvivesUiReportFailure);
Console.WriteLine(failures == 0 ? "ALL TESTS PASSED" : failures + " TEST(S) FAILED");
return failures == 0 ? 0 : 1;
}
@@ -153,10 +157,18 @@ namespace BizTalkPlatformManagementTool.Tests
var install = Path.Combine(directory, "install"); Directory.CreateDirectory(install);
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName), "old");
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => false);
Expect<InvalidOperationException>(() => engine.Install(false, null));
var data = Path.Combine(directory, "data");
var messages = new List<string>();
var engine = new InstallerEngine(package, install, data, false, path => false);
var exception = Capture<InvalidOperationException>(() => engine.Install(false, messages.Add));
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "staging failure modified the installed payload");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup was created before staging passed");
Assert(exception.Message.Contains("Fehlercode=SETUP-STAGING-VALIDATION"), "staging error code missing");
Assert(exception.Message.Contains("Kein Rollback erforderlich"), "pre-mutation failure reported a rollback");
Assert(messages.Any(x => x.Contains("Kein Rollback erforderlich")), "operator output did not distinguish pre-mutation failure");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=self_test_completed label=staging"), "staging self-test result missing from log");
Assert(log.Contains("event=rollback_summary result=not_required"), "no-rollback evidence missing from log");
});
}
@@ -170,10 +182,12 @@ namespace BizTalkPlatformManagementTool.Tests
File.WriteAllText(Path.Combine(install, InstallerEngine.ApplicationExeName + ".config"), "old-config");
var calls = 0;
var engine = new InstallerEngine(package, install, Path.Combine(directory, "data"), false, path => ++calls == 1);
Expect<InvalidOperationException>(() => engine.Install(false, null));
var exception = Capture<InvalidOperationException>(() => engine.Install(false, null));
Assert(File.ReadAllText(Path.Combine(install, InstallerEngine.ApplicationExeName)) == "old", "previous payload was not restored");
Assert(!Directory.GetDirectories(directory, "install.staging.*").Any(), "staging directory remained");
Assert(!Directory.GetDirectories(directory, "install.backup.*").Any(), "backup directory remained");
Assert(exception.Message.Contains("Fehlercode=SETUP-ACTIVATED-SELFTEST"), "activated self-test error code missing");
Assert(exception.Message.Contains("Rollback erfolgreich"), "successful rollback not reported");
});
}
@@ -191,6 +205,62 @@ namespace BizTalkPlatformManagementTool.Tests
});
}
private static void InstallerDiagnosticLogContainsContextAndExceptionChain()
{
InTemp(directory =>
{
var log = SetupOperationLog.Create(directory, "diagnostic-test");
Assert(!string.IsNullOrEmpty(log.FilePath), "diagnostic log was not created");
Exception failure;
try { throw new InvalidOperationException("outer", new IOException("inner")); }
catch (Exception ex) { failure = ex; }
log.WriteException("TEST-CODE", "Test phase", failure);
var content = File.ReadAllText(log.FilePath);
Assert(content.Contains("event=setup_started"), "setup context header missing");
Assert(content.Contains("process_bitness="), "process bitness missing");
Assert(content.Contains("identity="), "identity missing");
Assert(content.Contains("error_code=TEST-CODE"), "error code missing");
Assert(content.Contains("inner[1]"), "inner exception missing");
Assert(content.Contains("hresult=0x"), "HRESULT missing");
});
}
private static void InstallerSurvivesUiReportFailure()
{
InTemp(directory =>
{
var package = CreatePackage(directory, "new");
var install = Path.Combine(directory, "install");
var data = Path.Combine(directory, "data");
var engine = new InstallerEngine(package, install, data, false, path => true);
engine.Install(false, message => { throw new InvalidOperationException("UI unavailable"); });
Assert(File.Exists(Path.Combine(install, InstallerEngine.ApplicationExeName)), "UI report failure interrupted installation");
var log = File.ReadAllText(Directory.GetFiles(Path.Combine(data, "InstallerLogs"), "setup-*.log").Single());
Assert(log.Contains("event=ui_report_failed"), "UI report failure was not diagnosed");
});
}
private static void InstallerDiagnosticLogFallsBackToTemp()
{
InTemp(directory =>
{
var blockedDataDirectory = Path.Combine(directory, "blocked-data-directory");
File.WriteAllText(blockedDataDirectory, "not a directory");
var log = SetupOperationLog.Create(blockedDataDirectory, "fallback-test");
try
{
Assert(log.IsFallback, "diagnostic log did not use the Temp fallback");
Assert(!string.IsNullOrEmpty(log.CreationError), "primary logging failure was not retained");
Assert(File.Exists(log.FilePath), "fallback diagnostic log was not created");
Assert(File.ReadAllText(log.FilePath).Contains("event=primary_log_creation_failed"), "fallback reason missing from log");
}
finally
{
if (File.Exists(log.FilePath)) File.Delete(log.FilePath);
}
});
}
private static BizTalkSnapshot Snapshot(string application, string port, int state)
{
var result = new BizTalkSnapshot { ToolVersion = "test", CreatedAt = DateTimeOffset.Now.ToString("o"), Server = Environment.MachineName };
@@ -225,5 +295,12 @@ namespace BizTalkPlatformManagementTool.Tests
catch (T) { return; }
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
}
private static T Capture<T>(Action action) where T : Exception
{
try { action(); }
catch (T ex) { return ex; }
throw new InvalidOperationException("Expected exception " + typeof(T).Name);
}
}
}