Skip to content

Commit b98e308

Browse files
authored
Plugin options system improvements (#411)
* Plugin options system improvements * Docstrings
1 parent 20a5db4 commit b98e308

File tree

7 files changed

+102
-24
lines changed

7 files changed

+102
-24
lines changed

src/Microsoft.Performance.SDK.Runtime.Tests/Options/PluginOptionsSystemTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
using Microsoft.Performance.SDK.Options;
99
using Microsoft.Performance.SDK.Processing;
1010
using Microsoft.Performance.SDK.Runtime.Options;
11+
using Microsoft.Performance.SDK.Runtime.Options.Serialization;
1112
using Microsoft.Performance.SDK.Runtime.Options.Serialization.DTO;
13+
using Microsoft.Performance.SDK.Runtime.Options.Serialization.Loading;
1214
using Microsoft.Performance.SDK.Tests.Options;
1315
using Microsoft.Performance.Testing;
1416
using Microsoft.Performance.Testing.SDK;
@@ -111,6 +113,18 @@ await sut.Saver.TrySaveAsync(new PluginOptionsDto()
111113
Assert.AreEqual(expectedValue, newDto.BooleanOptions.First(o => o.Guid == optionGuid).Value);
112114
}
113115

116+
[TestMethod]
117+
public async Task TrySaveCurrentRegistry_LoaderFails_FailsToSave()
118+
{
119+
var sut = new PluginOptionsSystem(
120+
new NullPluginOptionsLoader(),
121+
new InMemoryPluginOptionsDtoRepository(),
122+
new PluginOptionsRegistry(Logger.Null));
123+
124+
sut.RegisterOptionsFrom(new StubProcessingSource(TestPluginOption.FieldOption("foo")));
125+
Assert.IsFalse(await sut.TrySaveCurrentRegistry());
126+
}
127+
114128
private PluginOptionsSystem CreateSut(params IProcessingSource[] processingSources)
115129
{
116130
var sut = PluginOptionsSystem.CreateUnsaved((_) => new NullLogger());

src/Microsoft.Performance.SDK.Runtime.Tests/Options/StreamPluginOptionsLoaderTests.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace Microsoft.Performance.SDK.Runtime.Tests.Options;
1515

1616
/// <summary>
17-
/// Tests for the <see cref="StreamPluginOptionsLoader"/>.
17+
/// Tests for the <see cref="StreamPluginOptionsLoader{T}"/>.
1818
/// </summary>
1919
[TestClass]
2020
[UnitTest]
@@ -53,7 +53,7 @@ public async Task LoadedDto_ContainsCorrectValues()
5353

5454
/// <summary>
5555
/// Asserts that the <see cref="Stream"/> used to load the <see cref="PluginOptionsDto"/> is closed after loading
56-
/// if <see cref="StreamPluginOptionsLoader"/> is configured to do so.
56+
/// if <see cref="StreamPluginOptionsLoader{T}"/> is configured to do so.
5757
/// </summary>
5858
[TestMethod]
5959
public async Task CloseStreamOnRead_ClosesStream()
@@ -71,7 +71,7 @@ public async Task CloseStreamOnRead_ClosesStream()
7171

7272
/// <summary>
7373
/// Asserts that the <see cref="Stream"/> used to load the <see cref="PluginOptionsDto"/> is not closed after loading
74-
/// if <see cref="StreamPluginOptionsLoader"/> is not configured to do so.
74+
/// if <see cref="StreamPluginOptionsLoader{T}"/> is not configured to do so.
7575
/// </summary>
7676
[TestMethod]
7777
public async Task DoNotCloseStreamOnRead_DoesNotCloseStream()
@@ -87,20 +87,37 @@ public async Task DoNotCloseStreamOnRead_DoesNotCloseStream()
8787
Assert.IsTrue(stream.CanWrite);
8888
}
8989

90+
[TestMethod]
91+
public async Task EmptyStream_ReturnsEmptyDto()
92+
{
93+
using MemoryStream stream = new MemoryStream();
94+
var sut = new TestStreamPluginOptionsLoader(stream, false);
95+
96+
var loadedDto = await sut.TryLoadAsync();
97+
98+
Assert.IsNotNull(loadedDto);
99+
Assert.IsTrue(loadedDto.IsEmpty());
100+
}
101+
90102
private sealed class TestStreamPluginOptionsLoader
91-
: StreamPluginOptionsLoader
103+
: StreamPluginOptionsLoader<MemoryStream>
92104
{
93-
private readonly Stream stream;
105+
private readonly MemoryStream memoryStream;
94106

95-
public TestStreamPluginOptionsLoader(MemoryStream stream, bool closeStreamOnRead)
107+
public TestStreamPluginOptionsLoader(MemoryStream memoryStream, bool closeStreamOnRead)
96108
: base(closeStreamOnRead, Logger.Null)
97109
{
98-
this.stream = stream;
110+
this.memoryStream = memoryStream;
111+
}
112+
113+
protected override MemoryStream GetStream()
114+
{
115+
return this.memoryStream;
99116
}
100117

101-
protected override Stream GetStream()
118+
protected override bool HasContent(MemoryStream stream)
102119
{
103-
return this.stream;
120+
return stream.Length > 0;
104121
}
105122
}
106123
}

src/Microsoft.Performance.SDK.Runtime/Options/PluginOptionsSystem.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ public async Task<bool> TryLoadAsync()
154154
/// the <see cref="Saver"/>. Previously saved options, as loaded by the <see cref="Loader"/> during this method call,
155155
/// will still be included in the saved options. If an option in the <see cref="Registry"/> was previously saved,
156156
/// its value will be updated after the save.
157+
/// If <see cref="Loader"/> fails to load options, the save will fail and this method will have
158+
/// no effect.
157159
/// </summary>
158160
/// <returns>
159161
/// <c>true</c> if the <see cref="Registry"/> was saved; <c>false</c> otherwise.
@@ -166,6 +168,12 @@ public async Task<bool> TrySaveCurrentRegistry()
166168
{
167169
var newDto = optionsRegistryToDtoConverter.ConvertToDto(this.Registry);
168170
var oldDto = await this.Loader.TryLoadAsync();
171+
172+
if (oldDto == null)
173+
{
174+
return false;
175+
}
176+
169177
return await this.Saver.TrySaveAsync(oldDto.UpdateTo(newDto));
170178
}
171179
}

src/Microsoft.Performance.SDK.Runtime/Options/Serialization/DTO/PluginOptionsDto.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public sealed class PluginOptionsDto
2727
/// </summary>
2828
public IReadOnlyCollection<FieldArrayPluginOptionDto> FieldArrayOptions { get; init; } = new List<FieldArrayPluginOptionDto>();
2929

30+
public bool IsEmpty()
31+
{
32+
return !this.BooleanOptions.Any() &&
33+
!this.FieldOptions.Any() &&
34+
!this.FieldArrayOptions.Any();
35+
}
36+
3037
/// <summary>
3138
/// Merges the values from the given <see cref="PluginOptionsDto"/> into this one, returning the new instance.
3239
/// DTOs in <paramref name="newDto"/> that have the same <see cref="PluginOptionDto.Guid"/> as a DTO in this

src/Microsoft.Performance.SDK.Runtime/Options/Serialization/Loading/FilePluginOptionsLoader.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ namespace Microsoft.Performance.SDK.Runtime.Options.Serialization.Loading;
1212
/// A <see cref="IPluginOptionsLoader"/> that can load a <see cref="PluginOptionsDto"/> instance from a file.
1313
/// </summary>
1414
public sealed class FilePluginOptionsLoader
15-
: StreamPluginOptionsLoader
15+
: StreamPluginOptionsLoader<FileStream>
1616
{
17-
private readonly string filePath;
18-
1917
/// <summary>
2018
/// Initializes a new instance of the <see cref="FilePluginOptionsLoader"/> class.
2119
/// </summary>
@@ -37,17 +35,28 @@ public FilePluginOptionsLoader(string filePath, ILogger logger)
3735
Guard.NotNullOrWhiteSpace(filePath, nameof(filePath));
3836
Guard.NotNull(logger, nameof(logger));
3937

40-
this.filePath = filePath;
38+
this.FilePath = filePath;
4139
}
4240

41+
/// <summary>
42+
/// Gets the path to the file from which to load options.
43+
/// </summary>
44+
public string FilePath { get; }
45+
4346
private protected override string GetDeserializeErrorMessage(Exception exception)
4447
{
45-
return $"Failed to load plugin options from '{this.filePath}': {exception.Message}.";
48+
return $"Failed to load plugin options from '{this.FilePath}': {exception.Message}.";
49+
}
50+
51+
/// <inheritdoc />
52+
protected override FileStream GetStream()
53+
{
54+
return File.Open(this.FilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read);
4655
}
4756

4857
/// <inheritdoc />
49-
protected override Stream GetStream()
58+
protected override bool HasContent(FileStream stream)
5059
{
51-
return File.Open(this.filePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read);
60+
return stream.Length > 0;
5261
}
5362
}

src/Microsoft.Performance.SDK.Runtime/Options/Serialization/Loading/StreamPluginOptionsLoader.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ namespace Microsoft.Performance.SDK.Runtime.Options.Serialization.Loading;
1313
/// <summary>
1414
/// Base class for <see cref="IPluginOptionsLoader"/> classes which load from a stream.
1515
/// </summary>
16-
public abstract class StreamPluginOptionsLoader
16+
/// <typeparam name="T">
17+
/// The type of the stream from which to load.
18+
/// </typeparam>
19+
public abstract class StreamPluginOptionsLoader<T>
1720
: IPluginOptionsLoader
21+
where T : Stream
1822
{
1923
private readonly bool closeStreamOnRead;
2024
private readonly ILogger logger;
2125

2226
/// <summary>
23-
/// Initializes a new instance of the <see cref="StreamPluginOptionsLoader"/> class.
27+
/// Initializes a new instance of the <see cref="StreamPluginOptionsLoader{T}"/> class.
2428
/// </summary>
2529
/// <param name="closeStreamOnRead">
2630
/// Whether to dispose of the stream returned by <see cref="GetStream"/> at the end of a call to
@@ -45,6 +49,11 @@ public async Task<PluginOptionsDto> TryLoadAsync()
4549
var stream = GetStream();
4650
try
4751
{
52+
if (!HasContent(stream))
53+
{
54+
return new PluginOptionsDto();
55+
}
56+
4857
return await JsonSerializer.DeserializeAsync<PluginOptionsDto>(stream, jsonSerializerOptions);
4958
}
5059
catch (Exception e)
@@ -72,5 +81,16 @@ private protected virtual string GetDeserializeErrorMessage(Exception exception)
7281
/// <returns>
7382
/// The <see cref="Stream"/> from which to load the <see cref="PluginOptionsDto"/>.
7483
/// </returns>
75-
protected abstract Stream GetStream();
84+
protected abstract T GetStream();
85+
86+
/// <summary>
87+
/// Determines whether <paramref name="stream"/> has content.
88+
/// </summary>
89+
/// <param name="stream">
90+
/// The <see cref="Stream"/> of type <typeparamref name="T"/> to check.
91+
/// </param>
92+
/// <returns>
93+
/// Whether <paramref name="stream"/> has content.
94+
/// </returns>
95+
protected abstract bool HasContent(T stream);
7696
}

src/Microsoft.Performance.SDK.Runtime/Options/Serialization/Saving/FilePluginOptionsSaver.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ namespace Microsoft.Performance.SDK.Runtime.Options.Serialization.Saving;
1414
public sealed class FilePluginOptionsSaver
1515
: StreamPluginOptionsSaver
1616
{
17-
private readonly string filePath;
18-
1917
/// <summary>
2018
/// Initializes a new instance of the <see cref="FilePluginOptionsSaver"/> class.
2119
/// </summary>
@@ -37,17 +35,22 @@ public FilePluginOptionsSaver(string filePath, ILogger logger)
3735
Guard.NotNullOrWhiteSpace(filePath, nameof(filePath));
3836
Guard.NotNull(logger, nameof(logger));
3937

40-
this.filePath = filePath;
38+
this.FilePath = filePath;
4139
}
4240

41+
/// <summary>
42+
/// Gets the path to the file to which options will be saved.
43+
/// </summary>
44+
public string FilePath { get; }
45+
4346
private protected override string GetSerializeErrorMessage(Exception exception)
4447
{
45-
return $"Failed to save plugin options to '{this.filePath}': {exception.Message}.";
48+
return $"Failed to save plugin options to '{this.FilePath}': {exception.Message}.";
4649
}
4750

4851
/// <inheritdoc />
4952
protected override Stream GetStream()
5053
{
51-
return File.Open(this.filePath, FileMode.Create, FileAccess.Write, FileShare.None);
54+
return File.Open(this.FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
5255
}
5356
}

0 commit comments

Comments
 (0)