fix: contain automation callback failures
This commit is contained in:
@@ -105,11 +105,17 @@ internal sealed record FirstApplyAutomationEvidence
|
|||||||
public bool? PermitAccepted { get; init; }
|
public bool? PermitAccepted { get; init; }
|
||||||
public int? IdleCallbackCount { get; init; }
|
public int? IdleCallbackCount { get; init; }
|
||||||
public int? CoordinatorStartCount { get; init; }
|
public int? CoordinatorStartCount { get; init; }
|
||||||
|
// Local test evidence only. This is intentionally never copied into Git
|
||||||
|
// evidence because exception stacks may contain machine-specific paths.
|
||||||
|
public string? ExceptionText { get; init; }
|
||||||
|
public string? PreviousState { get; init; }
|
||||||
|
public string? CurrentState { get; init; }
|
||||||
public IReadOnlyList<string> StateTransitions { get; init; } = Array.Empty<string>();
|
public IReadOnlyList<string> StateTransitions { get; init; } = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class FirstLimitedApplyAutomationCoordinator
|
internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||||
{
|
{
|
||||||
|
private enum CoordinatorState { Created, Starting, Running, Completed, Failed, ExitQueued }
|
||||||
private sealed record PxpResult(string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted);
|
private sealed record PxpResult(string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted);
|
||||||
private static string? selectedTargetSnapshotId;
|
private static string? selectedTargetSnapshotId;
|
||||||
private static readonly List<PxpResult> pxpResults = [];
|
private static readonly List<PxpResult> pxpResults = [];
|
||||||
@@ -121,21 +127,27 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal))
|
||||||
pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted));
|
pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted));
|
||||||
}
|
}
|
||||||
private FirstApplyAutomationConfiguration config = null!;
|
private FirstApplyAutomationConfiguration? config;
|
||||||
private Document document = null!;
|
private Document? document;
|
||||||
private readonly List<string> completed = [];
|
private readonly List<string> completed = [];
|
||||||
private int step;
|
private int step;
|
||||||
private int? before, after1, after2, afterUndo;
|
private int? before, after1, after2, afterUndo;
|
||||||
private AutomationEntityCounts? baseline, counts1, counts2, countsUndo;
|
private AutomationEntityCounts? baseline, counts1, counts2, countsUndo;
|
||||||
private readonly List<string> transitions = ["Created"];
|
private readonly List<string> transitions = ["Created"];
|
||||||
|
private CoordinatorState state = CoordinatorState.Created;
|
||||||
|
private CoordinatorState previousState = CoordinatorState.Created;
|
||||||
|
private bool documentHandlersSubscribed;
|
||||||
|
private bool lifecycleHandlersSubscribed;
|
||||||
private bool exitQueued;
|
private bool exitQueued;
|
||||||
|
private bool exitHandlerSubscribed;
|
||||||
|
private string? exceptionText;
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Transition(CoordinatorState.Starting, "PostStartupReady");
|
||||||
config = ReadConfiguration();
|
config = ReadConfiguration();
|
||||||
transitions.Add("PostStartupReady");
|
|
||||||
if (string.Equals(config.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(config.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
CompleteLifecycleProbe();
|
CompleteLifecycleProbe();
|
||||||
@@ -157,8 +169,11 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
document.CommandCancelled += OnCommandCancelled;
|
document.CommandCancelled += OnCommandCancelled;
|
||||||
AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated;
|
AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated;
|
||||||
AcadApplication.Idle += OnIdle;
|
AcadApplication.Idle += OnIdle;
|
||||||
|
documentHandlersSubscribed = true;
|
||||||
|
lifecycleHandlersSubscribed = true;
|
||||||
|
Transition(CoordinatorState.Running, "AutomationRunning");
|
||||||
}
|
}
|
||||||
catch (System.Exception ex) { Complete(false, "AUTOMATION_START_FAILED:" + ex.GetType().Name); }
|
catch (System.Exception ex) { FailFromCallback("Start", ex, "AUTOMATION_START_FAILED"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CompleteLifecycleProbe()
|
private void CompleteLifecycleProbe()
|
||||||
@@ -168,18 +183,21 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
RequestControlledExit();
|
RequestControlledExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnIdle(object? sender, EventArgs e)
|
private void OnIdle(object? sender, EventArgs e) => RunCallback("Idle", BeginContextVerification);
|
||||||
{
|
|
||||||
BeginContextVerification();
|
|
||||||
}
|
|
||||||
private void OnDocumentActivated(object sender, DocumentCollectionEventArgs e)
|
private void OnDocumentActivated(object sender, DocumentCollectionEventArgs e)
|
||||||
{
|
{
|
||||||
if (string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) BeginContextVerification();
|
RunCallback("DocumentActivated", () =>
|
||||||
|
{
|
||||||
|
if (document is null || e.Document is null) return;
|
||||||
|
if (string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) BeginContextVerification();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
private void BeginContextVerification()
|
private void BeginContextVerification()
|
||||||
{
|
{
|
||||||
|
if (IsTerminal || document is null) return;
|
||||||
AcadApplication.Idle -= OnIdle;
|
AcadApplication.Idle -= OnIdle;
|
||||||
AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated;
|
AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated;
|
||||||
|
lifecycleHandlersSubscribed = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
transitions.Add("ContextVerificationStarted");
|
transitions.Add("ContextVerificationStarted");
|
||||||
@@ -194,31 +212,36 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
before = Dbmod(); baseline = Counts();
|
before = Dbmod(); baseline = Counts();
|
||||||
Queue("PXP-OA");
|
Queue("PXP-OA");
|
||||||
}
|
}
|
||||||
catch (System.Exception ex) { Complete(false, "AUTOMATION_CONTEXT_FAILED:" + ex.GetType().Name); }
|
catch (System.Exception ex) { FailFromCallback("ContextVerification", ex, "AUTOMATION_CONTEXT_FAILED"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Queue(string command)
|
private void Queue(string command)
|
||||||
{
|
{
|
||||||
|
if (IsTerminal || document is null) return;
|
||||||
transitions.Add("CommandQueued:" + command);
|
transitions.Add("CommandQueued:" + command);
|
||||||
document.SendStringToExecute(command + " ", true, false, false);
|
document.SendStringToExecute(command + " ", true, false, false);
|
||||||
}
|
}
|
||||||
private void OnCommandEnded(object sender, CommandEventArgs e)
|
private void OnCommandEnded(object sender, CommandEventArgs e)
|
||||||
{
|
{
|
||||||
var name = e.GlobalCommandName?.Trim().TrimStart('.', '_').ToUpperInvariant();
|
RunCallback("CommandEnded", () =>
|
||||||
if (name is not ("PXP-OA" or "UNDO" or "U")) return;
|
{
|
||||||
completed.Add(name);
|
var name = e.GlobalCommandName?.Trim().TrimStart('.', '_').ToUpperInvariant();
|
||||||
transitions.Add("CommandCompleted:" + name);
|
if (name is not ("PXP-OA" or "UNDO" or "U")) return;
|
||||||
if (step++ == 0) { after1 = Dbmod(); counts1 = Counts(); Queue("PXP-OA"); return; }
|
completed.Add(name);
|
||||||
if (step == 2 && string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { after2 = Dbmod(); counts2 = Counts(); Queue("_.UNDO _1"); return; }
|
transitions.Add("CommandCompleted:" + name);
|
||||||
if (string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { afterUndo = Dbmod(); countsUndo = Counts(); Complete(true, null); }
|
if (step++ == 0) { after1 = Dbmod(); counts1 = Counts(); Queue("PXP-OA"); return; }
|
||||||
else { after2 = Dbmod(); counts2 = Counts(); Complete(true, null); }
|
if (step == 2 && string.Equals(config?.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { after2 = Dbmod(); counts2 = Counts(); Queue("_.UNDO _1"); return; }
|
||||||
|
if (string.Equals(config?.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { afterUndo = Dbmod(); countsUndo = Counts(); Complete(true, null); }
|
||||||
|
else { after2 = Dbmod(); counts2 = Counts(); Complete(true, null); }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
private void OnCommandFailed(object sender, CommandEventArgs e) => Complete(false, "AUTOMATION_COMMAND_FAILED:" + e.GlobalCommandName);
|
private void OnCommandFailed(object sender, CommandEventArgs e) => RunCallback("CommandFailed", () => Complete(false, "AUTOMATION_COMMAND_FAILED:" + e.GlobalCommandName));
|
||||||
private void OnCommandCancelled(object sender, CommandEventArgs e) => Complete(false, "AUTOMATION_COMMAND_CANCELLED:" + e.GlobalCommandName);
|
private void OnCommandCancelled(object sender, CommandEventArgs e) => RunCallback("CommandCancelled", () => Complete(false, "AUTOMATION_COMMAND_CANCELLED:" + e.GlobalCommandName));
|
||||||
|
|
||||||
private int Dbmod() => Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD"));
|
private int Dbmod() => Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD"));
|
||||||
private AutomationEntityCounts Counts()
|
private AutomationEntityCounts Counts()
|
||||||
{
|
{
|
||||||
|
if (document is null) throw new InvalidOperationException("AUTOMATION_DOCUMENT_UNAVAILABLE");
|
||||||
using var tx = document.Database.TransactionManager.StartOpenCloseTransaction();
|
using var tx = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
var table = (BlockTable)tx.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
var table = (BlockTable)tx.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
var paper = (BlockTableRecord)tx.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
var paper = (BlockTableRecord)tx.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
@@ -250,16 +273,13 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
}
|
}
|
||||||
private void Complete(bool success, string? error)
|
private void Complete(bool success, string? error)
|
||||||
{
|
{
|
||||||
try
|
if (IsTerminal) return;
|
||||||
{
|
Transition(success ? CoordinatorState.Completed : CoordinatorState.Failed, success ? "Completed" : "Failed");
|
||||||
if (document is not null) { document.CommandEnded -= OnCommandEnded; document.CommandFailed -= OnCommandFailed; document.CommandCancelled -= OnCommandCancelled; }
|
UnsubscribeHandlers();
|
||||||
AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated;
|
TryWriteEvidence(success, error);
|
||||||
WriteEvidence(success, error);
|
selectedTargetSnapshotId = null;
|
||||||
if (document is not null) document.CloseAndDiscard();
|
pxpResults.Clear();
|
||||||
RequestControlledExit();
|
RequestControlledExit();
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally { selectedTargetSnapshotId = null; pxpResults.Clear(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoCAD's direct Application.Quit can leave a startup-script host alive.
|
// AutoCAD's direct Application.Quit can leave a startup-script host alive.
|
||||||
@@ -269,32 +289,81 @@ internal sealed class FirstLimitedApplyAutomationCoordinator
|
|||||||
{
|
{
|
||||||
if (exitQueued) return;
|
if (exitQueued) return;
|
||||||
exitQueued = true;
|
exitQueued = true;
|
||||||
transitions.Add("ExitQueued");
|
Transition(CoordinatorState.ExitQueued, "ExitQueued");
|
||||||
AcadApplication.Idle += OnExitIdle;
|
AcadApplication.Idle += OnExitIdle;
|
||||||
|
exitHandlerSubscribed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExitIdle(object? sender, EventArgs args)
|
private void OnExitIdle(object? sender, EventArgs args)
|
||||||
{
|
{
|
||||||
AcadApplication.Idle -= OnExitIdle;
|
RunCallback("ControlledExitIdle", () =>
|
||||||
try
|
|
||||||
{
|
{
|
||||||
|
if (exitHandlerSubscribed) AcadApplication.Idle -= OnExitIdle;
|
||||||
|
exitHandlerSubscribed = false;
|
||||||
|
// Do not close a cached automation document and then reuse it. Resolve
|
||||||
|
// the currently-live document only at the final quiescent callback.
|
||||||
var active = AcadApplication.DocumentManager.MdiActiveDocument;
|
var active = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
if (active is null) { AcadApplication.Quit(); return; }
|
if (active is null) { AcadApplication.Quit(); return; }
|
||||||
transitions.Add("NativeQuitDispatched");
|
transitions.Add("NativeQuitDispatched");
|
||||||
active.SendStringToExecute("_.QUIT _N ", true, false, false);
|
active.SendStringToExecute("_.QUIT _N ", true, false, false);
|
||||||
}
|
});
|
||||||
catch (System.Exception exception)
|
}
|
||||||
|
|
||||||
|
private bool IsTerminal => state is CoordinatorState.Completed or CoordinatorState.Failed or CoordinatorState.ExitQueued;
|
||||||
|
|
||||||
|
private void Transition(CoordinatorState next, string transition)
|
||||||
|
{
|
||||||
|
previousState = state;
|
||||||
|
state = next;
|
||||||
|
transitions.Add(transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunCallback(string callback, Action action)
|
||||||
|
{
|
||||||
|
if (IsTerminal && callback != "ControlledExitIdle") return;
|
||||||
|
try { action(); }
|
||||||
|
catch (System.Exception exception) { FailFromCallback(callback, exception, "AUTOMATION_CALLBACK_FAILED"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FailFromCallback(string callback, System.Exception exception, string code)
|
||||||
|
{
|
||||||
|
exceptionText = exception.ToString();
|
||||||
|
RuntimeDiagnostics.Write("AutomationCallbackFailed", "ERROR", resultStatus: code + ":" + callback, exception: exception);
|
||||||
|
if (!IsTerminal) Complete(false, code + ":" + callback + ":" + exception.GetType().Name);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
RuntimeDiagnostics.Write("AutomationControlledExitFailed", "ERROR", resultStatus: "AUTOMATION_CONTROLLED_EXIT_FAILED", exception: exception);
|
UnsubscribeHandlers();
|
||||||
AcadApplication.Quit();
|
TryWriteEvidence(false, code + ":" + callback + ":" + exception.GetType().Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UnsubscribeHandlers()
|
||||||
|
{
|
||||||
|
if (documentHandlersSubscribed && document is not null)
|
||||||
|
{
|
||||||
|
try { document.CommandEnded -= OnCommandEnded; document.CommandFailed -= OnCommandFailed; document.CommandCancelled -= OnCommandCancelled; }
|
||||||
|
catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationHandlerUnsubscribeFailed", "ERROR", resultStatus: "AUTOMATION_UNSUBSCRIBE_FAILED", exception: exception); }
|
||||||
|
}
|
||||||
|
documentHandlersSubscribed = false;
|
||||||
|
if (lifecycleHandlersSubscribed)
|
||||||
|
{
|
||||||
|
try { AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated; AcadApplication.Idle -= OnIdle; }
|
||||||
|
catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationHandlerUnsubscribeFailed", "ERROR", resultStatus: "AUTOMATION_UNSUBSCRIBE_FAILED", exception: exception); }
|
||||||
|
}
|
||||||
|
lifecycleHandlersSubscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryWriteEvidence(bool success, string? error)
|
||||||
|
{
|
||||||
|
try { WriteEvidence(success, error); }
|
||||||
|
catch (System.Exception exception) { RuntimeDiagnostics.Write("AutomationEvidenceWriteFailed", "ERROR", resultStatus: "AUTOMATION_EVIDENCE_WRITE_FAILED", exception: exception); }
|
||||||
|
}
|
||||||
|
|
||||||
private void WriteEvidence(bool success, string? error)
|
private void WriteEvidence(bool success, string? error)
|
||||||
{
|
{
|
||||||
if (config is null) return;
|
if (config is null) return;
|
||||||
var first = pxpResults.ElementAtOrDefault(0); var second = pxpResults.ElementAtOrDefault(1);
|
var first = pxpResults.ElementAtOrDefault(0); var second = pxpResults.ElementAtOrDefault(1);
|
||||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, PermitAccepted = first?.PermitAccepted, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, StateTransitions = transitions };
|
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, PermitAccepted = first?.PermitAccepted, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, ExceptionText = exceptionText, PreviousState = previousState.ToString(), CurrentState = state.ToString(), StateTransitions = transitions };
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||||
File.WriteAllText(config.EvidencePath, JsonSerializer.Serialize(evidence));
|
File.WriteAllText(config.EvidencePath, JsonSerializer.Serialize(evidence));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,60 @@ public sealed class FoundationTests
|
|||||||
Assert.Contains("active.SendStringToExecute(\"_.QUIT _N \", true, false, false)", source);
|
Assert.Contains("active.SendStringToExecute(\"_.QUIT _N \", true, false, false)", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void Callback_AfterDocumentDestroyed_DoesNotAccessDocument()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("if (document is null || e.Document is null) return;", source);
|
||||||
|
Assert.Contains("if (IsTerminal || document is null) return;", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void Idle_WhenActiveDocumentNull_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("if (active is null) { AcadApplication.Quit(); return; }", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void TerminalState_IgnoresFurtherCallbacks()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("private bool IsTerminal", source);
|
||||||
|
Assert.Contains("if (IsTerminal && callback != \"ControlledExitIdle\") return;", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void HandlerException_ProducesFailedEvidence()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("exceptionText = exception.ToString();", source);
|
||||||
|
Assert.Contains("ExceptionText = exceptionText", source);
|
||||||
|
Assert.Contains("FailFromCallback", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void HandlerException_DoesNotEscapeToAutoCad()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("private void RunCallback", source);
|
||||||
|
Assert.Contains("catch (System.Exception exception) { FailFromCallback", source);
|
||||||
|
Assert.Contains("=> RunCallback(\"Idle\"", source);
|
||||||
|
Assert.Contains("=> RunCallback(\"CommandFailed\"", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void Unsubscribe_IsIdempotent()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("private bool documentHandlersSubscribed", source);
|
||||||
|
Assert.Contains("private bool lifecycleHandlersSubscribed", source);
|
||||||
|
Assert.Contains("private void UnsubscribeHandlers()", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ControlledExit_DoesNotReuseDestroyedDocument()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
var controlledExit = source[source.IndexOf("private void RequestControlledExit", StringComparison.Ordinal)..];
|
||||||
|
Assert.DoesNotContain("document.CloseAndDiscard", controlledExit);
|
||||||
|
Assert.Contains("Resolve", controlledExit, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("MdiActiveDocument", controlledExit);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
|
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
|
||||||
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
|
|||||||
Reference in New Issue
Block a user