Skip to content

Commit 2e4bd0f

Browse files
author
matt W10
committed
feat(baseline): fix out directory for the disk baseline provider
The disk baseline provider always wrote the baseline to a hard-coded "StrykerOutput" folder under the project path. Storing the baseline inside the test project's directory is problematic: without a non-obvious workaround, Stryker flags it as a change to the test project and ignores the baseline. This PR changes disk baseline provider so --output is honored if specified. In this case, both a 'baseline' folder and .gitignore file will be put inside the specified output directory as siblings to the 'reports' folder. If --output is not specified, StrykerOutput will be created as before, but .gitignore will be generated inside StrykerOutput instead of inside the 'reports' folder, which fixes the problem of the baseline being incorrectly flagged as a change to the test project.
1 parent f31e6b3 commit 2e4bd0f

10 files changed

Lines changed: 207 additions & 7 deletions

File tree

src/Stryker.Abstractions/Options/IStrykerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public interface IStrykerOptions
1515
string S3Endpoint { get; init; }
1616
string S3Region { get; init; }
1717
BaselineProvider BaselineProvider { get; init; }
18+
string BaselineOutputPath { get; init; }
1819
bool BreakOnInitialTestFailure { get; set; }
1920
int Concurrency { get; init; }
2021
string Configuration { get; init; }

src/Stryker.CLI/Stryker.CLI.UnitTest/Logging/InputBuilderTests.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public void ShouldAddGitIgnore()
2727
var gitIgnoreFile =
2828
fileSystemMock.AllFiles.Single(x => x.EndsWith(Path.Combine(".gitignore")));
2929
gitIgnoreFile.ShouldNotBeNull();
30-
DateTime.TryParse(Directory.GetParent(gitIgnoreFile)!.Name.Split(".")[0], out _).ShouldBeTrue();
30+
// the gitignore lives at the stable output root, not the per-run timestamped folder
31+
Directory.GetParent(gitIgnoreFile)!.Name.ShouldBe("StrykerOutput");
3132
var fileContents = fileSystemMock.GetFile(gitIgnoreFile).Contents;
3233
Encoding.Default.GetString(fileContents).ShouldBe("*");
3334
}
@@ -67,4 +68,36 @@ public void ShouldAddGitIgnoreWithRelativePath()
6768
var fileContents = fileSystemMock.GetFile(gitIgnoreFile).Contents;
6869
Encoding.Default.GetString(fileContents).ShouldBe("*");
6970
}
71+
72+
[TestMethod]
73+
public void ShouldSetBaselineOutputToStableRoot()
74+
{
75+
var fileSystemMock = new MockFileSystem();
76+
var basePath = Directory.GetCurrentDirectory();
77+
var target = new LoggingInitializer();
78+
79+
var inputs = new StrykerInputs();
80+
inputs.BasePathInput.SuppliedInput = basePath;
81+
target.SetupLogOptions(inputs, fileSystemMock);
82+
83+
// the baseline follows the stable output root, not the per-run timestamped output path
84+
inputs.BaselineOutputInput.SuppliedInput.ShouldBe(Path.Combine(basePath, "StrykerOutput"));
85+
inputs.OutputPathInput.SuppliedInput.ShouldStartWith(Path.Combine(basePath, "StrykerOutput") + Path.DirectorySeparatorChar);
86+
}
87+
88+
[TestMethod]
89+
public void ShouldSetBaselineOutputToSuppliedOutputPath()
90+
{
91+
var fileSystemMock = new MockFileSystem();
92+
var basePath = Directory.GetCurrentDirectory();
93+
var target = new LoggingInitializer();
94+
95+
var inputs = new StrykerInputs();
96+
inputs.BasePathInput.SuppliedInput = basePath;
97+
inputs.OutputPathInput.SuppliedInput = "output";
98+
target.SetupLogOptions(inputs, fileSystemMock);
99+
100+
// an explicit output path has no timestamp subfolder, so it is the root itself
101+
inputs.BaselineOutputInput.SuppliedInput.ShouldBe(Path.Combine(basePath, "output"));
102+
}
70103
}

src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,17 @@ public void SetupLogOptions(IStrykerInputs inputs, IFileSystem fileSystem = null
3636

3737
private string CreateOutputPath(IStrykerInputs inputs, IFileSystem fileSystem)
3838
{
39-
var outputPath = inputs.OutputPathInput.SuppliedInput ?? Path.Combine("StrykerOutput", DateTime.Now.ToString("yyyy-MM-dd.HH-mm-ss"));
39+
// The stable output root. When no output path is supplied the per-run output lives in a
40+
// timestamped subfolder of this root; an explicitly supplied output path is the root itself.
41+
// The root is where the disk baseline is stored (so it can be found on the next run) and
42+
// where the gitignore is placed (so the baseline and all run outputs are excluded from git).
43+
var outputRoot = inputs.OutputPathInput.SuppliedInput ?? "StrykerOutput";
44+
var outputPath = inputs.OutputPathInput.SuppliedInput ?? Path.Combine(outputRoot, DateTime.Now.ToString("yyyy-MM-dd.HH-mm-ss"));
45+
46+
if (!Path.IsPathRooted(outputRoot))
47+
{
48+
outputRoot = Path.Combine(inputs.BasePathInput.SuppliedInput, outputRoot);
49+
}
4050

4151
if (!Path.IsPathRooted(outputPath))
4252
{
@@ -46,8 +56,11 @@ private string CreateOutputPath(IStrykerInputs inputs, IFileSystem fileSystem)
4656
// outputpath should always be created
4757
fileSystem.Directory.CreateDirectory(FilePathUtils.NormalizePathSeparators(outputPath));
4858

49-
// add gitignore if it didn't exist yet
50-
var gitignorePath = FilePathUtils.NormalizePathSeparators(Path.Combine(outputPath, ".gitignore"));
59+
// store the baseline under the stable output root so it follows --output and persists across runs
60+
inputs.BaselineOutputInput.SuppliedInput = outputRoot;
61+
62+
// add gitignore to the output root if it didn't exist yet
63+
var gitignorePath = FilePathUtils.NormalizePathSeparators(Path.Combine(outputRoot, ".gitignore"));
5164
if (!fileSystem.File.Exists(gitignorePath))
5265
{
5366
try
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Stryker.Configuration.Options.Inputs;
2+
3+
public class BaselineOutputInput : Input<string>
4+
{
5+
protected override string Description => "The directory the disk baseline provider stores and loads the baseline report from. This is derived from the output path (the stable output root) rather than supplied directly, so the baseline follows --output, is covered by the gitignore, and persists across runs since it lives outside the per-run timestamped folder.";
6+
7+
public override string Default => "StrykerOutput";
8+
9+
public string Validate()
10+
{
11+
if (string.IsNullOrWhiteSpace(SuppliedInput))
12+
{
13+
return Default;
14+
}
15+
return SuppliedInput;
16+
}
17+
}

src/Stryker.Configuration/Options/StrykerInputs.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public interface IStrykerInputs
1414
S3RegionInput S3RegionInput { get; init; }
1515
AzureFileStorageUrlInput AzureFileStorageUrlInput { get; init; }
1616
BaselineProviderInput BaselineProviderInput { get; init; }
17+
BaselineOutputInput BaselineOutputInput { get; init; }
1718
BasePathInput BasePathInput { get; init; }
1819
ConcurrencyInput ConcurrencyInput { get; init; }
1920
ConfigurationInput ConfigurationInput { get; init; }
@@ -90,6 +91,7 @@ public StrykerInputs(IFileSystem fileSystem = null)
9091
public WithBaselineInput WithBaselineInput { get; init; } = new();
9192
public ReportersInput ReportersInput { get; init; } = new();
9293
public BaselineProviderInput BaselineProviderInput { get; init; } = new();
94+
public BaselineOutputInput BaselineOutputInput { get; init; } = new();
9395
public AzureFileStorageUrlInput AzureFileStorageUrlInput { get; init; } = new();
9496
public AzureFileStorageSasInput AzureFileStorageSasInput { get; init; } = new();
9597
public S3BucketNameInput S3BucketNameInput { get; init; } = new();
@@ -175,6 +177,7 @@ public IStrykerOptions ValidateAll()
175177
S3Region = S3RegionInput.Validate(baselineProvider, withBaseline),
176178
WithBaseline = withBaseline,
177179
BaselineProvider = baselineProvider,
180+
BaselineOutputPath = BaselineOutputInput.Validate(),
178181
FallbackVersion = FallbackVersionInput.Validate(withBaseline, projectVersion, sinceTarget),
179182
Since = sinceEnabled,
180183
SinceTarget = sinceTarget,

src/Stryker.Configuration/Options/StrykerOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ public string Configuration
158158
/// </summary>
159159
public BaselineProvider BaselineProvider { get; init; }
160160

161+
/// <summary>
162+
/// The directory the disk baseline provider stores and loads the baseline report from.
163+
/// A relative path is resolved against <see cref="ProjectPath"/>. Defaults to the stable
164+
/// StrykerOutput folder so baselines persist across runs.
165+
/// </summary>
166+
public string BaselineOutputPath { get; init; }
167+
161168
/// <summary>
162169
/// The url to connect to the Azure File Storage API
163170
/// </summary>

src/Stryker.Core/Stryker.Core.UnitTest/Baseline/Providers/DiskBaselineProviderTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,65 @@ public async Task ShouldWriteToDiskAsync()
3838
file.ShouldNotBeNull();
3939
}
4040

41+
[TestMethod]
42+
public async Task ShouldWriteToConfiguredBaselineOutputPathAsync()
43+
{
44+
var fileSystemMock = new MockFileSystem();
45+
var options = new StrykerOptions()
46+
{
47+
ProjectPath = @"C:/Users/JohnDoe/Project/TestFolder",
48+
BaselineOutputPath = "custom-baseline"
49+
};
50+
var sut = new DiskBaselineProvider(options, fileSystemMock);
51+
52+
await sut.Save(JsonReport.Build(options, ReportTestHelper.CreateProjectWith(), It.IsAny<TestProjectsInfo>()), "baseline/version");
53+
54+
var path = FilePathUtils.NormalizePathSeparators(@"C:/Users/JohnDoe/Project/TestFolder/custom-baseline/baseline/version/stryker-report.json");
55+
56+
var file = fileSystemMock.GetFile(path);
57+
file.ShouldNotBeNull();
58+
}
59+
60+
[TestMethod]
61+
public async Task ShouldWriteToAbsoluteBaselineOutputPathAsync()
62+
{
63+
var fileSystemMock = new MockFileSystem();
64+
var options = new StrykerOptions()
65+
{
66+
ProjectPath = @"C:/Users/JohnDoe/Project/TestFolder",
67+
BaselineOutputPath = @"D:/shared/baselines"
68+
};
69+
var sut = new DiskBaselineProvider(options, fileSystemMock);
70+
71+
await sut.Save(JsonReport.Build(options, ReportTestHelper.CreateProjectWith(), It.IsAny<TestProjectsInfo>()), "baseline/version");
72+
73+
var path = FilePathUtils.NormalizePathSeparators(@"D:/shared/baselines/baseline/version/stryker-report.json");
74+
75+
var file = fileSystemMock.GetFile(path);
76+
file.ShouldNotBeNull();
77+
}
78+
79+
[TestMethod]
80+
public async Task ShouldLoadReportFromConfiguredBaselineOutputPathAsync()
81+
{
82+
var fileSystemMock = new MockFileSystem();
83+
var options = new StrykerOptions()
84+
{
85+
ProjectPath = @"C:/Users/JohnDoe/Project/TestFolder",
86+
BaselineOutputPath = "custom-baseline"
87+
};
88+
var report = JsonReport.Build(options, ReportTestHelper.CreateProjectWith(), It.IsAny<ITestProjectsInfo>());
89+
90+
fileSystemMock.AddFile("C:/Users/JohnDoe/Project/TestFolder/custom-baseline/baseline/version/stryker-report.json", report.ToJson());
91+
92+
var target = new DiskBaselineProvider(options, fileSystemMock);
93+
94+
var result = await target.Load("baseline/version");
95+
96+
result.ShouldNotBeNull();
97+
result.ToJson().ShouldBe(report.ToJson());
98+
}
99+
41100
[TestMethod]
42101
public async Task ShouldHandleFileNotFoundExceptionOnLoadAsync()
43102
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Shouldly;
2+
using Stryker.Configuration.Options.Inputs;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
namespace Stryker.Core.UnitTest.Options.Inputs;
6+
7+
[TestClass]
8+
public class BaselineOutputInputTests : TestBase
9+
{
10+
[TestMethod]
11+
public void ShouldHaveHelpText()
12+
{
13+
var target = new BaselineOutputInput();
14+
target.HelpText.ShouldNotBeNullOrEmpty();
15+
}
16+
17+
[TestMethod]
18+
[DataRow(null)]
19+
[DataRow("")]
20+
[DataRow(" ")]
21+
public void ShouldReturnDefault_WhenNotSupplied(string input)
22+
{
23+
var target = new BaselineOutputInput { SuppliedInput = input };
24+
25+
var result = target.Validate();
26+
27+
result.ShouldBe("StrykerOutput");
28+
}
29+
30+
[TestMethod]
31+
public void ShouldReturnSuppliedValue_WhenSupplied()
32+
{
33+
var target = new BaselineOutputInput { SuppliedInput = "custom-baseline" };
34+
35+
var result = target.Validate();
36+
37+
result.ShouldBe("custom-baseline");
38+
}
39+
}

src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerInputsTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class StrykerInputsTests : TestBase
1919
AzureFileStorageSasInput = new AzureFileStorageSasInput(),
2020
AzureFileStorageUrlInput = new AzureFileStorageUrlInput(),
2121
BaselineProviderInput = new BaselineProviderInput(),
22+
BaselineOutputInput = new BaselineOutputInput(),
2223
BasePathInput = new BasePathInput() { SuppliedInput = Directory.GetCurrentDirectory() },
2324
ConcurrencyInput = new ConcurrencyInput(),
2425
DashboardApiKeyInput = new DashboardApiKeyInput(),
@@ -200,6 +201,24 @@ public void WithBaselineShouldNotThrow_2743() // https://github.com/stryker-muta
200201
Should.NotThrow(() => _target.ValidateAll());
201202
}
202203

204+
[TestMethod]
205+
public void BaselineOutputPathShouldDefaultToStrykerOutput()
206+
{
207+
var result = _target.ValidateAll();
208+
209+
result.BaselineOutputPath.ShouldBe("StrykerOutput");
210+
}
211+
212+
[TestMethod]
213+
public void ShouldSetBaselineOutputPathWhenSupplied()
214+
{
215+
_target.BaselineOutputInput.SuppliedInput = "custom-baseline";
216+
217+
var result = _target.ValidateAll();
218+
219+
result.BaselineOutputPath.ShouldBe("custom-baseline");
220+
}
221+
203222
[TestMethod]
204223
public void BaseLineOptionsShouldBeSetToDefaultWhenBaselineIsDisabled()
205224
{

src/Stryker.Core/Stryker.Core/Baseline/Providers/DiskBaselineProvider.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ public class DiskBaselineProvider : IBaselineProvider
1616
private readonly IStrykerOptions _options;
1717
private readonly IFileSystem _fileSystem;
1818
private readonly ILogger<DiskBaselineProvider> _logger;
19-
private const string _outputPath = "StrykerOutput";
19+
private const string _defaultOutputPath = "StrykerOutput";
20+
21+
/// <summary>
22+
/// The directory (relative to the project path, or absolute) the baseline is stored under.
23+
/// Honors the configured baseline output path, falling back to the stable StrykerOutput
24+
/// folder so baselines persist across runs when no output path was supplied.
25+
/// </summary>
26+
private string OutputPath => string.IsNullOrWhiteSpace(_options.BaselineOutputPath)
27+
? _defaultOutputPath
28+
: _options.BaselineOutputPath;
2029

2130
public DiskBaselineProvider(IStrykerOptions options, IFileSystem fileSystem = null)
2231
{
@@ -29,7 +38,7 @@ public DiskBaselineProvider(IStrykerOptions options, IFileSystem fileSystem = nu
2938
public async Task<IJsonReport> Load(string version)
3039
{
3140
var reportPath = FilePathUtils.NormalizePathSeparators(
32-
Path.Combine(_options.ProjectPath, _outputPath, version, "stryker-report.json"));
41+
Path.Combine(_options.ProjectPath, OutputPath, version, "stryker-report.json"));
3342

3443
if (_fileSystem.File.Exists(reportPath))
3544
{
@@ -45,7 +54,7 @@ public async Task<IJsonReport> Load(string version)
4554
public async Task Save(IJsonReport report, string version)
4655
{
4756
var reportDirectory = FilePathUtils.NormalizePathSeparators(
48-
Path.Combine(_options.ProjectPath, _outputPath, version));
57+
Path.Combine(_options.ProjectPath, OutputPath, version));
4958

5059
_fileSystem.Directory.CreateDirectory(reportDirectory);
5160

0 commit comments

Comments
 (0)