Skip to content

Commit 3b0a8e9

Browse files
authored
Merge pull request #322 from csf-dev/craigfowler/issue215
Resolve #215 - Record timings in reports
2 parents d464f30 + a251642 commit 3b0a8e9

11 files changed

Lines changed: 168 additions & 16 deletions

CSF.Screenplay/ReportModel/PerformableReport.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ public class PerformableReport : ReportableModelBase
3535
/// </remarks>
3636
public string PerformancePhase { get; set; }
3737

38+
/// <summary>
39+
/// Gets or sets the relative time at which this performable ended/finished.
40+
/// </summary>
41+
/// <remarks>
42+
/// <para>
43+
/// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
44+
/// report metadata, at <see cref="ReportMetadata.Timestamp"/>.
45+
/// </para>
46+
/// <para>
47+
/// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
48+
/// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
49+
/// </para>
50+
/// </remarks>
51+
/// <seealso cref="ReportMetadata.Timestamp"/>
52+
/// <seealso cref="PerformanceReport.Started"/>
53+
/// <seealso cref="ReportableModelBase.Started"/>
54+
public TimeSpan Ended { get; set; }
55+
3856
/// <summary>
3957
/// Gets or sets a string representation of the result which was emitted by the corresponding performable.
4058
/// </summary>

CSF.Screenplay/ReportModel/PerformanceReport.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,23 @@ public List<ReportableModelBase> Reportables
5252
get => reportables;
5353
set => reportables = value ?? throw new ArgumentNullException(nameof(value));
5454
}
55+
56+
/// <summary>
57+
/// Gets or sets the time at which this performance was begun.
58+
/// </summary>
59+
/// <remarks>
60+
/// <para>
61+
/// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
62+
/// report metadata, at <see cref="ReportMetadata.Timestamp"/>.
63+
/// </para>
64+
/// <para>
65+
/// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
66+
/// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
67+
/// </para>
68+
/// </remarks>
69+
/// <seealso cref="ReportMetadata.Timestamp"/>
70+
/// <seealso cref="ReportableModelBase.Started"/>
71+
/// <seealso cref="PerformableReport.Ended"/>
72+
public TimeSpan Started { get; set; }
5573
}
5674
}

CSF.Screenplay/ReportModel/ReportMetadata.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ public class ReportMetadata
1010
const string reportFormatVersion = "2.0.0";
1111

1212
/// <summary>
13-
/// Gets or sets the UTC timestamp at which the report was generated.
13+
/// Gets or sets the date &amp; time at which the Screenplay began.
1414
/// </summary>
15-
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
15+
/// <remarks>
16+
/// <para>
17+
/// Other time-related values within the Screenplay report are expressed relative to this time.
18+
/// </para>
19+
/// </remarks>
20+
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
1621

1722
/// <summary>
1823
/// Gets or sets a version number for the format of report that has been produced.

CSF.Screenplay/ReportModel/ReportableModelBase.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Text.Json.Serialization;
23

34
namespace CSF.Screenplay.ReportModel
@@ -32,5 +33,28 @@ public abstract class ReportableModelBase
3233
/// </para>
3334
/// </remarks>
3435
public string ActorName { get; set; }
36+
37+
/// <summary>
38+
/// Gets or sets the relative time at which this reportable event occurred.
39+
/// </summary>
40+
/// <remarks>
41+
/// <para>
42+
/// For many types of reportable items (derived from this type), only the start time is recorded, because it is expected that the
43+
/// activity upon which is being reported takes a trivial amount of time.
44+
/// For <see cref="PerformableReport"/> instances, an end time is also recorded, as these are expected to take an appreciable amount of time.
45+
/// </para>
46+
/// <para>
47+
/// This property is expressed as an amount of time since the Screenplay began. The beginning of the Screenplay is recorded in the
48+
/// report metadata, at <see cref="ReportMetadata.Timestamp"/>.
49+
/// </para>
50+
/// <para>
51+
/// Recall that it is quite normal for performances and thus reportable actions to occur in parallel.
52+
/// Do not be alarmed if it appears that unrelated performances are interleaved with regard to their timings.
53+
/// </para>
54+
/// </remarks>
55+
/// <seealso cref="ReportMetadata.Timestamp"/>
56+
/// <seealso cref="PerformanceReport.Started"/>
57+
/// <seealso cref="PerformableReport.Ended"/>
58+
public TimeSpan Started { get; set; }
3559
}
3660
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
3+
namespace CSF.Screenplay.Reporting
4+
{
5+
6+
/// <summary>
7+
/// An object which acts as a stopwatch, intended for used in providing timing data for reports.
8+
/// </summary>
9+
public interface IMeasuresTime : IDisposable
10+
{
11+
/// <summary>
12+
/// Begins the timer, recording/tracking time.
13+
/// </summary>
14+
/// <returns>The current date &amp; time, at the point when timing began.</returns>
15+
/// <exception cref="InvalidOperationException">If this method is used more than once upon the same object instance.</exception>
16+
DateTimeOffset BeginTiming();
17+
18+
/// <summary>
19+
/// Gets the amount of time (wall clock time) which has elapsed since <see cref="BeginTiming"/> was executed.
20+
/// </summary>
21+
/// <returns>A timespan which is the time elapsed since timing began.</returns>
22+
/// <exception cref="InvalidOperationException">If this method is used before <see cref="BeginTiming"/> has been executed.</exception>
23+
TimeSpan GetCurrentTime();
24+
}
25+
}

CSF.Screenplay/Reporting/JsonScreenplayReporter.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ namespace CSF.Screenplay.Reporting
4141
public sealed class JsonScreenplayReporter : IReporter
4242
{
4343
readonly ScreenplayReportBuilder builder;
44+
readonly IMeasuresTime reportTimer;
4445
readonly Utf8JsonWriter jsonWriter;
4546
readonly object syncRoot = new object();
4647
IHasPerformanceEvents subscribed;
@@ -134,7 +135,7 @@ void OnPerformanceBegun(object sender, PerformanceEventArgs e)
134135
void OnScreenplayStarted(object sender, EventArgs e)
135136
{
136137
jsonWriter.WriteStartObject();
137-
var metadata = new ReportMetadata();
138+
var metadata = new ReportMetadata() { Timestamp = reportTimer.BeginTiming() };
138139
jsonWriter.WritePropertyName(nameof(ScreenplayReport.Metadata));
139140
JsonSerializer.Serialize(jsonWriter, metadata);
140141
jsonWriter.WriteStartArray(nameof(ScreenplayReport.Performances));
@@ -171,15 +172,19 @@ static string GetPhaseString(PerformancePhase phase)
171172
/// Initializes a new instance of <see cref="JsonScreenplayReporter"/> for a specified file path.
172173
/// </summary>
173174
/// <param name="writeStream">The stream to which the JSON report shall be written.</param>
174-
/// <param name="builder">The Screenplay report builder</param>
175+
/// <param name="builderFactory">A factory for a Screenplay report builder</param>
176+
/// <param name="reportTimer">A timing service for reports</param>
175177
/// <exception cref="ArgumentNullException">If <paramref name="writeStream"/> is <see langword="null" />.</exception>
176-
public JsonScreenplayReporter(Stream writeStream, ScreenplayReportBuilder builder)
178+
public JsonScreenplayReporter(Stream writeStream, Func<IMeasuresTime,ScreenplayReportBuilder> builderFactory, IMeasuresTime reportTimer)
177179
{
178180
if (writeStream is null)
179181
throw new ArgumentNullException(nameof(writeStream));
182+
if(builderFactory is null)
183+
throw new ArgumentNullException(nameof(builderFactory));
180184

181185
jsonWriter = new Utf8JsonWriter(writeStream, new JsonWriterOptions { Indented = false });
182-
this.builder = builder ?? throw new ArgumentNullException(nameof(builder));
186+
this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
187+
builder = builderFactory(reportTimer);
183188
}
184189
}
185190
}

CSF.Screenplay/Reporting/PerformanceReportBuilder.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class PerformanceReportBuilder
2424
readonly IGetsValueFormatter valueFormatterProvider;
2525
readonly IFormatsReportFragment formatter;
2626
readonly IGetsContentType contentTypeProvider;
27+
readonly IMeasuresTime reportTimer;
2728

2829
/// <summary>
2930
/// 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)
8485
{
8586
ActorName = actor.Name,
8687
Report = string.Format(ReportStrings.ActorCreatedFormat, actor.Name),
88+
Started = reportTimer.GetCurrentTime(),
8789
});
8890
}
8991

@@ -102,6 +104,7 @@ public void ActorGainedAbility(Actor actor, object ability)
102104
{
103105
ActorName = actor.Name,
104106
Report = reportText,
107+
Started = reportTimer.GetCurrentTime(),
105108
});
106109
}
107110

@@ -120,6 +123,7 @@ public void ActorSpotlit(Actor actor)
120123
{
121124
ActorName = actor.Name,
122125
Report = string.Format(ReportStrings.ActorSpotlitFormat, actor.Name),
126+
Started = reportTimer.GetCurrentTime(),
123127
});
124128
}
125129

@@ -136,6 +140,7 @@ public void SpotlightTurnedOff()
136140
NewPerformableList.Add(new SpotlightTurnedOffReport
137141
{
138142
Report = ReportStrings.SpotlightTurnedOff,
143+
Started = reportTimer.GetCurrentTime(),
139144
});
140145
}
141146

@@ -172,6 +177,7 @@ public void BeginPerformable(object performable, Actor actor, string performance
172177
PerformableType = performable.GetType().FullName,
173178
ActorName = actor.Name,
174179
PerformancePhase = performancePhase,
180+
Started = reportTimer.GetCurrentTime(),
175181
};
176182

177183
NewPerformableList.Add(performableReport);
@@ -235,6 +241,7 @@ public void EndPerformable(object performable, Actor actor)
235241
CurrentPerformable.Report = performable is ICanReport reporter
236242
? reporter.GetReportFragment(actor, formatter).FormattedFragment
237243
: string.Format(ReportStrings.FallbackReportFormat, actor.Name, performable.GetType().FullName);
244+
CurrentPerformable.Ended = reportTimer.GetCurrentTime();
238245
performableStack.Pop();
239246
}
240247

@@ -254,6 +261,7 @@ public void RecordFailureForCurrentPerformable(Exception exception)
254261
{
255262
CurrentPerformable.Exception = exception.ToString();
256263
CurrentPerformable.ExceptionIsFromConsumedPerformable = exception is PerformableException;
264+
CurrentPerformable.Ended = reportTimer.GetCurrentTime();
257265
performableStack.Pop();
258266
}
259267

@@ -266,21 +274,26 @@ public void RecordFailureForCurrentPerformable(Exception exception)
266274
/// <param name="valueFormatterProvider">A value formatter factory</param>
267275
/// <param name="formatter">A report-fragment formatter</param>
268276
/// <param name="contentTypeProvider">A content type provider service</param>
277+
/// <param name="reportTimer">A report timer</param>
269278
/// <exception cref="ArgumentNullException">If any parameter is <see langword="null" />.</exception>
270279
public PerformanceReportBuilder(List<IdentifierAndNameModel> namingHierarchy,
271280
IGetsValueFormatter valueFormatterProvider,
272281
IFormatsReportFragment formatter,
273-
IGetsContentType contentTypeProvider)
282+
IGetsContentType contentTypeProvider,
283+
IMeasuresTime reportTimer)
274284
{
275285
if (namingHierarchy is null)
276286
throw new ArgumentNullException(nameof(namingHierarchy));
277287

278288
this.valueFormatterProvider = valueFormatterProvider ?? throw new ArgumentNullException(nameof(valueFormatterProvider));
279289
this.formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
280290
this.contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException(nameof(contentTypeProvider));
291+
this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
292+
281293
report = new PerformanceReport
282294
{
283295
NamingHierarchy = namingHierarchy.ToList(),
296+
Started = reportTimer.GetCurrentTime(),
284297
};
285298
}
286299
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Diagnostics;
3+
4+
namespace CSF.Screenplay.Reporting
5+
{
6+
/// <summary>
7+
/// Default implementation of <see cref="IMeasuresTime"/> which uses a <see cref="Stopwatch"/>.
8+
/// </summary>
9+
public sealed class ReportTimer : IMeasuresTime
10+
{
11+
readonly Stopwatch stopwatch = new Stopwatch();
12+
13+
/// <inheritdoc/>
14+
public DateTimeOffset BeginTiming()
15+
{
16+
if(stopwatch.IsRunning) throw new InvalidOperationException($"The {nameof(BeginTiming)} method may not be used more than once.");
17+
18+
stopwatch.Start();
19+
return DateTimeOffset.Now;
20+
}
21+
22+
/// <inheritdoc/>
23+
public void Dispose()
24+
{
25+
if(stopwatch.IsRunning)
26+
stopwatch.Stop();
27+
}
28+
29+
/// <inheritdoc/>
30+
public TimeSpan GetCurrentTime()
31+
{
32+
if(!stopwatch.IsRunning) throw new InvalidOperationException($"The {nameof(GetCurrentTime)} method may not be used before {nameof(BeginTiming)}.");
33+
return stopwatch.Elapsed;
34+
}
35+
}
36+
}

CSF.Screenplay/Reporting/ScreenplayReportBuilder.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ namespace CSF.Screenplay.Reporting
2828
public class ScreenplayReportBuilder
2929
{
3030
readonly ConcurrentDictionary<Guid, PerformanceReportBuilder> performanceReports = new ConcurrentDictionary<Guid, PerformanceReportBuilder>();
31-
readonly Func<List<IdentifierAndNameModel>, PerformanceReportBuilder> performanceBuilderFactory;
31+
readonly Func<List<IdentifierAndNameModel>,IMeasuresTime,PerformanceReportBuilder> performanceBuilderFactory;
32+
readonly IMeasuresTime reportTimer;
3233

3334
/// <summary>
3435
/// Begins building a report about a new performance.
@@ -52,7 +53,7 @@ public void BeginPerformance(Guid performanceIdentifier, IReadOnlyList<Identifie
5253
var mappedNamingHierarchy = namingHierarchy
5354
.Select(x => new IdentifierAndNameModel { Identifier = x.Identifier, Name = x.Name, WasIdentifierAutoGenerated = x.WasIdentifierAutoGenerated })
5455
.ToList();
55-
performanceReports.TryAdd(performanceIdentifier, performanceBuilderFactory(mappedNamingHierarchy));
56+
performanceReports.TryAdd(performanceIdentifier, performanceBuilderFactory(mappedNamingHierarchy, reportTimer));
5657
}
5758

5859
/// <summary>
@@ -95,10 +96,12 @@ public PerformanceReport EndPerformanceAndGetReport(Guid performanceIdentifier,
9596
/// Initialises a new instance of <see cref="ScreenplayReportBuilder"/>.
9697
/// </summary>
9798
/// <param name="performanceBuilderFactory">A factory function for performance report builders</param>
99+
/// <param name="reportTimer">A report timer</param>
98100
/// <exception cref="ArgumentNullException">If <paramref name="performanceBuilderFactory"/> is <see langword="null" />.</exception>
99-
public ScreenplayReportBuilder(Func<List<IdentifierAndNameModel>,PerformanceReportBuilder> performanceBuilderFactory)
101+
public ScreenplayReportBuilder(Func<List<IdentifierAndNameModel>,IMeasuresTime,PerformanceReportBuilder> performanceBuilderFactory, IMeasuresTime reportTimer)
100102
{
101103
this.performanceBuilderFactory = performanceBuilderFactory ?? throw new ArgumentNullException(nameof(performanceBuilderFactory));
104+
this.reportTimer = reportTimer ?? throw new ArgumentNullException(nameof(reportTimer));
102105
}
103106
}
104107
}

CSF.Screenplay/ScreenplayServiceCollectionExtensions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,20 @@ public static IServiceCollection AddScreenplay(this IServiceCollection services)
7272
.AddTransient<JsonScreenplayReporter>()
7373
.AddTransient<NoOpReporter>()
7474
.AddTransient<ITestsPathForWritePermissions, WritePermissionTester>()
75-
.AddTransient<Func<List<IdentifierAndNameModel>, PerformanceReportBuilder>>(s =>
75+
.AddTransient<Func<List<IdentifierAndNameModel>, IMeasuresTime, PerformanceReportBuilder>>(s =>
7676
{
77-
return idsAndNames => ActivatorUtilities.CreateInstance<PerformanceReportBuilder>(s, idsAndNames);
77+
return (idsAndNames, timer) => ActivatorUtilities.CreateInstance<PerformanceReportBuilder>(s, idsAndNames, timer);
7878
})
7979
.AddTransient<GetAssetFilePaths>()
8080
.AddTransient<ToStringFormatter>()
8181
.AddTransient<HumanizerFormatter>()
8282
.AddTransient<NameFormatter>()
83-
.AddTransient<FormattableFormatter>();
83+
.AddTransient<FormattableFormatter>()
84+
.AddTransient<IMeasuresTime, ReportTimer>()
85+
.AddTransient<Func<IMeasuresTime, ScreenplayReportBuilder>>(s =>
86+
{
87+
return timer => ActivatorUtilities.CreateInstance<ScreenplayReportBuilder>(s, timer);
88+
});
8489

8590
return services;
8691
}

0 commit comments

Comments
 (0)