// 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;
}
}
}
}