// Copyright Epic Games, Inc. All Rights Reserved. #nullable enable using System; using System.Linq; using System.Collections; using System.Collections.Generic; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using EpicGames.Core; using static Gauntlet.HordeReport.AutomatedTestSessionData; using AutomatedTestSessionData = Gauntlet.HordeReport.AutomatedTestSessionData; using AutomationTool; using Logging = Microsoft.Extensions.Logging; namespace Gauntlet { /// /// Test execution tracking objects /// namespace TestTracking { /// /// Handler for associated phase data /// public interface IPhaseDataHandler { /// /// Called when test phase is added to report /// /// public void OnAddToReport(TestPhase ReportedPhase); } /// /// Object that represent a test phase /// public class Phase { public string Name; public double ElapseTimeInSeconds; public DateTime? StartedAt; public TestPhaseOutcome Outcome; private ConcurrentQueue Events; /// /// Opaque object that can be associated with the phase. /// The object class can implement its own handler by implementing IPhaseDataHandler interface. /// OnAddToReport method will be called when the phase is attached to the report allowing /// the opaque data to make modifications to the reported data. /// public object? Data; public Phase(string InName, DateTime? InDateTime = null, object? InData = null) { Name = InName; ElapseTimeInSeconds = 0; StartedAt = InDateTime; Outcome = TestPhaseOutcome.Unknown; Events = new ConcurrentQueue(); Data = InData; } /// /// Mark the phase as started /// /// /// public void Start(object? InData = null) { if (StartedAt != null) { throw new AutomationException($"Phase '{Name}' already started."); } StartedAt = DateTime.UtcNow; if (InData != null) Data = InData; } /// /// Mark the phase as finished /// /// The optional outcome. By default it will evaluted based on tracked events /// public double End(TestPhaseOutcome? InOutcome = null) { DateTime StartedTime = StartedAt ?? DateTime.UtcNow; ElapseTimeInSeconds = (DateTime.UtcNow - StartedTime).TotalSeconds; if (InOutcome != null) { Outcome = InOutcome.Value; } else { Outcome = Events.Any(E => E.Level == Logging.LogLevel.Error || E.Level == Logging.LogLevel.Critical) ? TestPhaseOutcome.Failed : TestPhaseOutcome.Success; } return ElapseTimeInSeconds; } /// /// Add an event to the phase tracker /// /// The log level of the event /// /// public void AddEvent(Logging.LogLevel Level, string Message, params object[] Args) { string? Format = null; Dictionary? Properties = null; if (Args.Any()) { Format = Message; Properties = new Dictionary(); MessageTemplate.ParsePropertyValues(Format, Args, Properties); Message = MessageTemplate.Render(Format, Properties); } EventId EventId = Level == Logging.LogLevel.Critical ? KnownLogEvents.Gauntlet_FatalEvent : KnownLogEvents.Gauntlet_TestEvent; LogEvent Event = new LogEvent(DateTime.UtcNow, Level, EventId, Message, Format, Properties, null); Events.Enqueue(Event); } /// /// Get the associated data /// /// /// public T? GetData() { if (Data is T Value) { return Value; } return default(T); } /// /// Add the information tracked by the phase to the test report /// /// /// public TestPhase AddToReport(AutomatedTestSessionData Report) { if (Outcome == TestPhaseOutcome.Unknown) { if (StartedAt == null) { Outcome = TestPhaseOutcome.NotRun; } else { End(TestPhaseOutcome.Interrupted); } } TestPhase ReportedPhase = Report.AddPhase(Name); if (StartedAt != null) { ReportedPhase.SetTiming(StartedAt.Value, (float)ElapseTimeInSeconds); } ReportedPhase.SetOutcome(Outcome); TestEventStream Stream = ReportedPhase.GetStream(); foreach (LogEvent Event in Events) { Stream.AddEvent(Event); } if (Data is IPhaseDataHandler DataHandler) { DataHandler.OnAddToReport(ReportedPhase); } return ReportedPhase; } } /// /// Object to track queued phases and enumerate through /// public class PhaseQueue : IEnumerable { public PhaseQueue() { _manifest = new(); _queuedPhases = new(); _last = null; _current = null; } private ConcurrentDictionary _manifest; private Queue _queuedPhases; private Phase? _last; private Phase? _current; /// /// The last added phase to the queue /// public Phase? Last => _last; /// /// The current running phase. Use Start to identify the running phase /// public Phase? Current => _current; /// /// Start a phase, if not queued it will be added to the queue /// /// The name of the phase /// Extra object to associate with the phase /// /// public Phase Start(string Name, object? Data = null) { bool bAlreadyAdded = false; Phase StartedPhase = _manifest.AddOrUpdate(Name, (Name) => new Phase(Name, DateTime.UtcNow, Data), (Name, ExistingPhase) => { bAlreadyAdded = true; ExistingPhase.Start(Data); return ExistingPhase; } ); if (!bAlreadyAdded) { _queuedPhases.Enqueue(StartedPhase); _last = StartedPhase; } _current = StartedPhase; return StartedPhase; } /// /// Add a new phase. Raise an exception if it already exists. /// /// The name of the phase /// Extra object to associate with the phase /// /// public Phase Add(string Name, object? Data = null) { Phase NewPhase = new Phase(Name, null, Data); if (!_manifest.TryAdd(Name, NewPhase)) { throw new AutomationException($"Phase '{Name}' already added. Make sure to use unique phase name."); } _queuedPhases.Enqueue(NewPhase); _last = NewPhase; return NewPhase; } /// /// Get the corresponding phase /// /// /// public Phase? Get(string Name) { Phase? FetchedPhase = null; _manifest.TryGetValue(Name, out FetchedPhase); return FetchedPhase; } /// /// Set current phase /// /// /// public bool SetCurrent(string Name) { Phase? FetchedPhase = null; _manifest.TryGetValue(Name, out FetchedPhase); _current = FetchedPhase; return FetchedPhase != null; } IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator)GetEnumerator(); } /// public IEnumerator GetEnumerator() { return _queuedPhases.GetEnumerator(); } /// /// Clear the queue /// public void Clear() { _queuedPhases.Clear(); _manifest.Clear(); _last = null; _current = null; } } } }