fix: correlate script-driven PXP-OA command events
This commit is contained in:
@@ -23,6 +23,7 @@ internal sealed record FirstApplyAutomationConfiguration
|
||||
internal sealed record AutomationEntityCounts(int Total, int PipeBlocks, int PxpOaXdata, int Owned);
|
||||
internal sealed record PxpOaDiscoveryEvidence(bool Found, string? GlobalName, string? GroupName, string? Flags, string? DeclaringType, string? DeclaringAssembly, string? AssemblyLocation, string? AssemblySha256, bool AssemblyLoaded, bool CommandClassIncluded, bool BundleHashMatched, int DeclarationCount);
|
||||
internal sealed record AutomationSentinelEvidence(string Stage, string? DocumentIdentity, string? DatabaseIdentity, DateTimeOffset TimestampUtc);
|
||||
internal sealed record AutomationCommandCorrelationEvidence(string EventKind, string RawCommandName, string NormalizedCommandName, bool CommandNameMatched, bool PreparedStateMatched, bool DocumentMatched, bool DatabaseMatched, bool FixturePathMatched, bool SequenceAccepted, string? RejectionReason, string? SenderType, string? SenderDocumentPath, string? ExpectedDocumentPath, string? ActiveDocumentPath, int RunCount, DateTimeOffset TimestampUtc);
|
||||
|
||||
internal static class FirstLimitedApplyAutomationBootstrap
|
||||
{
|
||||
@@ -127,6 +128,7 @@ internal sealed record FirstApplyAutomationEvidence
|
||||
public PxpOaDiscoveryEvidence? PxpOaDiscovery { get; init; }
|
||||
public IReadOnlyList<AutomationSentinelEvidence> Sentinels { get; init; } = Array.Empty<AutomationSentinelEvidence>();
|
||||
public IReadOnlyList<string> CommandEvents { get; init; } = Array.Empty<string>();
|
||||
public IReadOnlyList<AutomationCommandCorrelationEvidence> CommandCorrelations { get; init; } = Array.Empty<AutomationCommandCorrelationEvidence>();
|
||||
}
|
||||
|
||||
internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||
|
||||
+26
-6
@@ -24,6 +24,7 @@ internal static class ScriptDrivenFirstApplyAutomation
|
||||
private static readonly List<string> transitions = [];
|
||||
private static readonly List<string> commandEvents = [];
|
||||
private static readonly List<AutomationSentinelEvidence> sentinels = [];
|
||||
private static readonly List<AutomationCommandCorrelationEvidence> correlations = [];
|
||||
private static PxpOaDiscoveryEvidence? discovery;
|
||||
private static bool prepared;
|
||||
private static bool terminal;
|
||||
@@ -104,7 +105,9 @@ internal static class ScriptDrivenFirstApplyAutomation
|
||||
Guard(() =>
|
||||
{
|
||||
commandEvents.Add("WillStart:" + args.GlobalCommandName);
|
||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count >= 2) return;
|
||||
var correlation = Correlate("WillStart", sender, args.GlobalCommandName, runs.Count < 2);
|
||||
correlations.Add(correlation);
|
||||
if (correlation.RejectionReason is not null) return;
|
||||
runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null));
|
||||
transitions.Add("PxpOaWillStart:" + runs.Count);
|
||||
});
|
||||
@@ -116,8 +119,11 @@ internal static class ScriptDrivenFirstApplyAutomation
|
||||
{
|
||||
Guard(() =>
|
||||
{
|
||||
commandEvents.Add((failed ? "Failed:" : cancelled ? "Cancelled:" : "Ended:") + args.GlobalCommandName);
|
||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count == 0) return;
|
||||
var kind = failed ? "Failed" : cancelled ? "Cancelled" : "Ended";
|
||||
commandEvents.Add(kind + ":" + args.GlobalCommandName);
|
||||
var correlation = Correlate(kind, sender, args.GlobalCommandName, runs.Count > 0);
|
||||
correlations.Add(correlation);
|
||||
if (correlation.RejectionReason is not null) return;
|
||||
var last = runs[^1];
|
||||
if (last.Ended || last.Failed || last.Cancelled) return;
|
||||
runs[^1] = last with { Ended = !failed && !cancelled, Failed = failed, Cancelled = cancelled, DbmodAfter = Dbmod(), After = Counts(database!) };
|
||||
@@ -125,8 +131,22 @@ internal static class ScriptDrivenFirstApplyAutomation
|
||||
Write(false, null);
|
||||
});
|
||||
}
|
||||
private static bool IsFixture(object sender) => !terminal && fixture is not null && ReferenceEquals(sender, fixture) && database is not null && ReferenceEquals(fixture.Database, database);
|
||||
private static bool IsPxp(string name) => string.Equals(name.TrimStart('.', '_'), "PXP-OA", StringComparison.OrdinalIgnoreCase);
|
||||
private static AutomationCommandCorrelationEvidence Correlate(string kind, object sender, string rawName, bool sequenceAccepted)
|
||||
{
|
||||
var normalized = rawName.Trim();
|
||||
var command = string.Equals(normalized, "PXP-OA", StringComparison.OrdinalIgnoreCase);
|
||||
var state = prepared && !terminal && config is not null && fixture is not null && database is not null;
|
||||
var senderDocument = sender as Document;
|
||||
var senderPath = senderDocument?.Name;
|
||||
var expectedPath = config?.OrthoDrawingPath;
|
||||
var activePath = AcadApplication.DocumentManager.MdiActiveDocument?.Name;
|
||||
var fixturePath = senderPath is not null && expectedPath is not null && SamePath(senderPath, expectedPath);
|
||||
var activeFixture = activePath is not null && expectedPath is not null && SamePath(activePath, expectedPath);
|
||||
var document = fixturePath && activeFixture;
|
||||
var databaseMatch = senderDocument?.Database is not null && database is not null && SamePath(senderDocument.Database.Filename, database.Filename);
|
||||
var rejection = !command ? "COMMAND_NAME_MISMATCH" : !state ? "NOT_PREPARED" : !document ? "DOCUMENT_IDENTITY_MISMATCH" : !databaseMatch ? "DATABASE_IDENTITY_MISMATCH" : !sequenceAccepted ? "UNEXPECTED_SEQUENCE" : null;
|
||||
return new(kind, rawName, normalized, command, state, document, databaseMatch, fixturePath, sequenceAccepted, rejection, sender.GetType().FullName, senderPath, expectedPath, activePath, runs.Count, DateTimeOffset.UtcNow);
|
||||
}
|
||||
private static void Guard(Action action) { try { if (!terminal) action(); } catch (System.Exception ex) { Fail("AUTOMATION_EVENT_HANDLER_FAILED", ex); } }
|
||||
private static void Fail(string code, System.Exception exception) { if (terminal) return; terminal = true; transitions.Add("Failed:" + code); try { Write(false, code + ":" + exception); } finally { Unsubscribe(); } }
|
||||
private static void Unsubscribe() { if (fixture is null) return; try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { } }
|
||||
@@ -154,7 +174,7 @@ internal static class ScriptDrivenFirstApplyAutomation
|
||||
{
|
||||
if (config is null) return;
|
||||
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").ToArray(), 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 = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null, EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents };
|
||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").ToArray(), 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 = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null, EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents, CommandCorrelations = correlations };
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||
var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true);
|
||||
}
|
||||
|
||||
@@ -28,4 +28,9 @@ public sealed class PxpOaRegistrationTests
|
||||
[Fact] public void Prepare_StopsOnPxpOaDiscoveryFailure() => Assert.True(Observer.IndexOf("RequireDiscovery(discovery)", StringComparison.Ordinal) < Observer.IndexOf("PlantProject.LoadProject", StringComparison.Ordinal));
|
||||
[Fact] public void AutomationSentinel_DoesNotModifyDatabase() => Assert.DoesNotContain("StartTransaction", Entry[Entry.IndexOf("AutomationBeforePxp1Command", StringComparison.Ordinal)..Entry.IndexOf("private static void RunDiagnostic", StringComparison.Ordinal)]);
|
||||
[Fact] public void AutomationSentinel_IsTestOnly() => Assert.Contains("RequireGate", Observer);
|
||||
[Fact] public void Observer_RecordsRawEventBeforeCorrelation() => Assert.True(Observer.IndexOf("commandEvents.Add", Observer.IndexOf("OnWillStart", StringComparison.Ordinal), StringComparison.Ordinal) < Observer.IndexOf("Correlate(\"WillStart\"", StringComparison.Ordinal));
|
||||
[Fact] public void Observer_RecordsEveryRejectionReason() => Assert.Contains("DOCUMENT_IDENTITY_MISMATCH", Observer);
|
||||
[Fact] public void Observer_DoesNotUseManagedDocumentReferenceAsAcceptance() => Assert.DoesNotContain("ReferenceEquals(sender, fixture)", Observer);
|
||||
[Fact] public void Observer_RecordsExactlyTwoSequentialRuns() => Assert.Contains("runs.Count < 2", Observer);
|
||||
[Fact] public void Finalize_ReportsMissingPlanSeparatelyFromMissingRuns() => Assert.True(Observer.IndexOf("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA", StringComparison.Ordinal) < Observer.IndexOf("AUTOMATION_PLAN_IDS_DIFFER", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user