diff --git a/CSF.Screenplay/ReportModel/PerformableReport.cs b/CSF.Screenplay/ReportModel/PerformableReport.cs
index c60411c3..6091a65e 100644
--- a/CSF.Screenplay/ReportModel/PerformableReport.cs
+++ b/CSF.Screenplay/ReportModel/PerformableReport.cs
@@ -35,6 +35,24 @@ public class PerformableReport : ReportableModelBase
///
public string PerformancePhase { get; set; }
+ ///
+ /// Gets or sets the relative time at which this performable ended/finished.
+ ///
+ ///
+ ///
+ /// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
+ /// report metadata, at .
+ ///
+ ///
+ /// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
+ /// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TimeSpan Ended { get; set; }
+
///
/// Gets or sets a string representation of the result which was emitted by the corresponding performable.
///
diff --git a/CSF.Screenplay/ReportModel/PerformanceReport.cs b/CSF.Screenplay/ReportModel/PerformanceReport.cs
index 90525c39..203a0614 100644
--- a/CSF.Screenplay/ReportModel/PerformanceReport.cs
+++ b/CSF.Screenplay/ReportModel/PerformanceReport.cs
@@ -52,5 +52,23 @@ public List Reportables
get => reportables;
set => reportables = value ?? throw new ArgumentNullException(nameof(value));
}
+
+ ///
+ /// Gets or sets the time at which this performance was begun.
+ ///
+ ///
+ ///
+ /// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
+ /// report metadata, at .
+ ///
+ ///
+ /// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
+ /// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TimeSpan Started { get; set; }
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay/ReportModel/ReportMetadata.cs b/CSF.Screenplay/ReportModel/ReportMetadata.cs
index f41b7ad2..eebbe0ea 100644
--- a/CSF.Screenplay/ReportModel/ReportMetadata.cs
+++ b/CSF.Screenplay/ReportModel/ReportMetadata.cs
@@ -10,9 +10,14 @@ public class ReportMetadata
const string reportFormatVersion = "2.0.0";
///
- /// Gets or sets the UTC timestamp at which the report was generated.
+ /// Gets or sets the date & time at which the Screenplay began.
///
- public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+ ///
+ ///
+ /// Other time-related values within the Screenplay report are expressed relative to this time.
+ ///
+ ///
+ public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
///
/// Gets or sets a version number for the format of report that has been produced.
diff --git a/CSF.Screenplay/ReportModel/ReportableModelBase.cs b/CSF.Screenplay/ReportModel/ReportableModelBase.cs
index 4526b55e..c1bb234c 100644
--- a/CSF.Screenplay/ReportModel/ReportableModelBase.cs
+++ b/CSF.Screenplay/ReportModel/ReportableModelBase.cs
@@ -1,3 +1,4 @@
+using System;
using System.Text.Json.Serialization;
namespace CSF.Screenplay.ReportModel
@@ -32,5 +33,28 @@ public abstract class ReportableModelBase
///
///
public string ActorName { get; set; }
+
+ ///
+ /// Gets or sets the relative time at which this reportable event occurred.
+ ///
+ ///
+ ///
+ /// For many types of reportable items (derived from this type), only the start time is recorded, because it is expected that the
+ /// activity upon which is being reported takes a trivial amount of time.
+ /// For instances, an end time is also recorded, as these are expected to take an appreciable amount of time.
+ ///
+ ///
+ /// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
+ /// report metadata, at .
+ ///
+ ///
+ /// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
+ /// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TimeSpan Started { get; set; }
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/IProvidesReportTiming.cs b/CSF.Screenplay/Reporting/IProvidesReportTiming.cs
new file mode 100644
index 00000000..e95fcb91
--- /dev/null
+++ b/CSF.Screenplay/Reporting/IProvidesReportTiming.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace CSF.Screenplay.Reporting
+{
+
+///
+/// An object which acts as a stopwatch, intended for used in providing timing data for reports.
+///
+public interface IMeasuresTime : IDisposable
+{
+ ///
+ /// Begins the timer, recording/tracking time.
+ ///
+ /// The current date & time, at the point when timing began.
+ /// If this method is used more than once upon the same object instance.
+ DateTimeOffset BeginTiming();
+
+ ///
+ /// Gets the amount of time (wall clock time) which has elapsed since was executed.
+ ///
+ /// A timespan which is the time elapsed since timing began.
+ /// If this method is used before has been executed.
+ TimeSpan GetCurrentTime();
+}
+}
diff --git a/CSF.Screenplay/Reporting/JsonScreenplayReporter.cs b/CSF.Screenplay/Reporting/JsonScreenplayReporter.cs
index 89bfb953..0580da48 100644
--- a/CSF.Screenplay/Reporting/JsonScreenplayReporter.cs
+++ b/CSF.Screenplay/Reporting/JsonScreenplayReporter.cs
@@ -41,6 +41,7 @@ namespace CSF.Screenplay.Reporting
public sealed class JsonScreenplayReporter : IReporter
{
readonly ScreenplayReportBuilder builder;
+ readonly IMeasuresTime reportTimer;
readonly Utf8JsonWriter jsonWriter;
readonly object syncRoot = new object();
IHasPerformanceEvents subscribed;
@@ -134,7 +135,7 @@ void OnPerformanceBegun(object sender, PerformanceEventArgs e)
void OnScreenplayStarted(object sender, EventArgs e)
{
jsonWriter.WriteStartObject();
- var metadata = new ReportMetadata();
+ var metadata = new ReportMetadata() { Timestamp = reportTimer.BeginTiming() };
jsonWriter.WritePropertyName(nameof(ScreenplayReport.Metadata));
JsonSerializer.Serialize(jsonWriter, metadata);
jsonWriter.WriteStartArray(nameof(ScreenplayReport.Performances));
@@ -171,15 +172,19 @@ static string GetPhaseString(PerformancePhase phase)
/// Initializes a new instance of for a specified file path.
///
/// The stream to which the JSON report shall be written.
- /// The Screenplay report builder
+ /// A factory for a Screenplay report builder
+ /// A timing service for reports
/// If is .
- public JsonScreenplayReporter(Stream writeStream, ScreenplayReportBuilder builder)
+ public JsonScreenplayReporter(Stream writeStream, Func builderFactory, IMeasuresTime reportTimer)
{
if (writeStream is null)
throw new ArgumentNullException(nameof(writeStream));
+ if(builderFactory is null)
+ throw new ArgumentNullException(nameof(builderFactory));
jsonWriter = new Utf8JsonWriter(writeStream, new JsonWriterOptions { Indented = false });
- this.builder = builder ?? throw new ArgumentNullException(nameof(builder));
+ this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
+ builder = builderFactory(reportTimer);
}
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
index 44b9398b..7ebe38d7 100644
--- a/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
+++ b/CSF.Screenplay/Reporting/PerformanceReportBuilder.cs
@@ -24,6 +24,7 @@ public class PerformanceReportBuilder
readonly IGetsValueFormatter valueFormatterProvider;
readonly IFormatsReportFragment formatter;
readonly IGetsContentType contentTypeProvider;
+ readonly IMeasuresTime reportTimer;
///
/// Gets a value indicating whether or not this builder has a 'current' performable that it is building.
@@ -84,6 +85,7 @@ public void ActorCreated(Actor actor)
{
ActorName = actor.Name,
Report = string.Format(ReportStrings.ActorCreatedFormat, actor.Name),
+ Started = reportTimer.GetCurrentTime(),
});
}
@@ -102,6 +104,7 @@ public void ActorGainedAbility(Actor actor, object ability)
{
ActorName = actor.Name,
Report = reportText,
+ Started = reportTimer.GetCurrentTime(),
});
}
@@ -120,6 +123,7 @@ public void ActorSpotlit(Actor actor)
{
ActorName = actor.Name,
Report = string.Format(ReportStrings.ActorSpotlitFormat, actor.Name),
+ Started = reportTimer.GetCurrentTime(),
});
}
@@ -136,6 +140,7 @@ public void SpotlightTurnedOff()
NewPerformableList.Add(new SpotlightTurnedOffReport
{
Report = ReportStrings.SpotlightTurnedOff,
+ Started = reportTimer.GetCurrentTime(),
});
}
@@ -172,6 +177,7 @@ public void BeginPerformable(object performable, Actor actor, string performance
PerformableType = performable.GetType().FullName,
ActorName = actor.Name,
PerformancePhase = performancePhase,
+ Started = reportTimer.GetCurrentTime(),
};
NewPerformableList.Add(performableReport);
@@ -235,6 +241,7 @@ public void EndPerformable(object performable, Actor actor)
CurrentPerformable.Report = performable is ICanReport reporter
? reporter.GetReportFragment(actor, formatter).FormattedFragment
: string.Format(ReportStrings.FallbackReportFormat, actor.Name, performable.GetType().FullName);
+ CurrentPerformable.Ended = reportTimer.GetCurrentTime();
performableStack.Pop();
}
@@ -254,6 +261,7 @@ public void RecordFailureForCurrentPerformable(Exception exception)
{
CurrentPerformable.Exception = exception.ToString();
CurrentPerformable.ExceptionIsFromConsumedPerformable = exception is PerformableException;
+ CurrentPerformable.Ended = reportTimer.GetCurrentTime();
performableStack.Pop();
}
@@ -266,11 +274,13 @@ public void RecordFailureForCurrentPerformable(Exception exception)
/// A value formatter factory
/// A report-fragment formatter
/// A content type provider service
+ /// A report timer
/// If any parameter is .
public PerformanceReportBuilder(List namingHierarchy,
IGetsValueFormatter valueFormatterProvider,
IFormatsReportFragment formatter,
- IGetsContentType contentTypeProvider)
+ IGetsContentType contentTypeProvider,
+ IMeasuresTime reportTimer)
{
if (namingHierarchy is null)
throw new ArgumentNullException(nameof(namingHierarchy));
@@ -278,9 +288,12 @@ public PerformanceReportBuilder(List namingHierarchy,
this.valueFormatterProvider = valueFormatterProvider ?? throw new ArgumentNullException(nameof(valueFormatterProvider));
this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
this.contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException(nameof(contentTypeProvider));
+ this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
+
report = new PerformanceReport
{
NamingHierarchy = namingHierarchy.ToList(),
+ Started = reportTimer.GetCurrentTime(),
};
}
}
diff --git a/CSF.Screenplay/Reporting/ReportTimer.cs b/CSF.Screenplay/Reporting/ReportTimer.cs
new file mode 100644
index 00000000..bcb81529
--- /dev/null
+++ b/CSF.Screenplay/Reporting/ReportTimer.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Diagnostics;
+
+namespace CSF.Screenplay.Reporting
+{
+///
+/// Default implementation of which uses a .
+///
+public sealed class ReportTimer : IMeasuresTime
+{
+ readonly Stopwatch stopwatch = new Stopwatch();
+
+ ///
+ public DateTimeOffset BeginTiming()
+ {
+ if(stopwatch.IsRunning) throw new InvalidOperationException($"The {nameof(BeginTiming)} method may not be used more than once.");
+
+ stopwatch.Start();
+ return DateTimeOffset.Now;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if(stopwatch.IsRunning)
+ stopwatch.Stop();
+ }
+
+ ///
+ public TimeSpan GetCurrentTime()
+ {
+ if(!stopwatch.IsRunning) throw new InvalidOperationException($"The {nameof(GetCurrentTime)} method may not be used before {nameof(BeginTiming)}.");
+ return stopwatch.Elapsed;
+ }
+}
+}
diff --git a/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs b/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs
index 3bff8710..c19fe23c 100644
--- a/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs
+++ b/CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs
@@ -28,7 +28,8 @@ namespace CSF.Screenplay.Reporting
public class ScreenplayReportBuilder
{
readonly ConcurrentDictionary performanceReports = new ConcurrentDictionary();
- readonly Func, PerformanceReportBuilder> performanceBuilderFactory;
+ readonly Func,IMeasuresTime,PerformanceReportBuilder> performanceBuilderFactory;
+ readonly IMeasuresTime reportTimer;
///
/// Begins building a report about a new performance.
@@ -52,7 +53,7 @@ public void BeginPerformance(Guid performanceIdentifier, IReadOnlyList new IdentifierAndNameModel { Identifier = x.Identifier, Name = x.Name, WasIdentifierAutoGenerated = x.WasIdentifierAutoGenerated })
.ToList();
- performanceReports.TryAdd(performanceIdentifier, performanceBuilderFactory(mappedNamingHierarchy));
+ performanceReports.TryAdd(performanceIdentifier, performanceBuilderFactory(mappedNamingHierarchy, reportTimer));
}
///
@@ -95,10 +96,12 @@ public PerformanceReport EndPerformanceAndGetReport(Guid performanceIdentifier,
/// Initialises a new instance of .
///
/// A factory function for performance report builders
+ /// A report timer
/// If is .
- public ScreenplayReportBuilder(Func,PerformanceReportBuilder> performanceBuilderFactory)
+ public ScreenplayReportBuilder(Func,IMeasuresTime,PerformanceReportBuilder> performanceBuilderFactory, IMeasuresTime reportTimer)
{
this.performanceBuilderFactory = performanceBuilderFactory ?? throw new ArgumentNullException(nameof(performanceBuilderFactory));
+ this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
}
}
}
\ No newline at end of file
diff --git a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
index 66b32186..5df4e8b0 100644
--- a/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
+++ b/CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs
@@ -72,15 +72,20 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services)
.AddTransient()
.AddTransient()
.AddTransient()
- .AddTransient, PerformanceReportBuilder>>(s =>
+ .AddTransient, IMeasuresTime, PerformanceReportBuilder>>(s =>
{
- return idsAndNames => ActivatorUtilities.CreateInstance(s, idsAndNames);
+ return (idsAndNames, timer) => ActivatorUtilities.CreateInstance(s, idsAndNames, timer);
})
.AddTransient()
.AddTransient()
.AddTransient()
.AddTransient()
- .AddTransient();
+ .AddTransient()
+ .AddTransient()
+ .AddTransient>(s =>
+ {
+ return timer => ActivatorUtilities.CreateInstance(s, timer);
+ });
return services;
}
diff --git a/Tests/CSF.Screenplay.Tests/Reporting/ScreenplayReportSerializerTests.cs b/Tests/CSF.Screenplay.Tests/Reporting/ScreenplayReportSerializerTests.cs
index a4692291..83101f65 100644
--- a/Tests/CSF.Screenplay.Tests/Reporting/ScreenplayReportSerializerTests.cs
+++ b/Tests/CSF.Screenplay.Tests/Reporting/ScreenplayReportSerializerTests.cs
@@ -60,7 +60,7 @@ public async Task DeserializeAsyncShouldReturnAScreenplayReport(ScreenplayReport
using var scope = Assert.EnterMultipleScope();
Assert.That(result, Is.Not.Null, "The deserialized report should not be null.");
- Assert.That(result.Metadata.Timestamp, Is.EqualTo(new DateTime(2021, 1, 1, 0, 0, 0, DateTimeKind.Utc)), "The timestamp should be correct.");
+ Assert.That(result.Metadata.Timestamp, Is.EqualTo(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero)), "The timestamp should be correct.");
Assert.That(result.Metadata.ReportFormatVersion, Is.EqualTo("2.0.0"), "The report format version should be correct.");
Assert.That(result.Performances, Has.Count.EqualTo(1), "There should be one performance.");
@@ -94,10 +94,10 @@ public void SerializeAsyncShouldThrowIfReportIsNull(ScreenplayReportSerializer s
[Test, AutoMoqData]
public async Task SerializeAsyncShouldSerializeToAStream(ScreenplayReportSerializer sut)
{
- var report = new ScreenplayReport() { Metadata = new () { ReportFormatVersion = "foo bar" } };
+ var report = new ScreenplayReport() { Metadata = new () { ReportFormatVersion = "foo bar", Timestamp = DateTimeOffset.UtcNow } };
var stream = await sut.SerializeAsync(report);
using var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync();
- Assert.That(content, Does.Match(@"^\{""Metadata"":\{""Timestamp"":""[\d-]{10}T[\d:.]{10,16}Z"",""ReportFormatVersion"":""foo bar""\},""Performances"":\[\]\}$"));
+ Assert.That(content, Does.Match(@"^\{""Metadata"":\{""Timestamp"":""[\d-]{10}T[\d:.]{10,16}\+00:00"",""ReportFormatVersion"":""foo bar""\},""Performances"":\[\]\}$"));
}
}
\ No newline at end of file