Skip to content

Commit b75bd20

Browse files
authored
feat: relative attachments in loggers (#207)
* refactor: add ToAttachment extensions for MTP * refactor: add relative attachment path for extensions * feat: relative attachment paths with UseRelativeAttachmentPath logger configuration
1 parent 7cedd06 commit b75bd20

File tree

16 files changed

+328
-27
lines changed

16 files changed

+328
-27
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageVersion Include="Stylecop.Analyzers" Version="1.2.0-beta.556" />
1212

1313
<PackageVersion Include="MSTest.TestFramework" Version="3.10.5" />
14-
<PackageVersion Include="MSTest.TestAdapter" Version="3.10.1" />
14+
<PackageVersion Include="MSTest.TestAdapter" Version="3.10.5" />
1515
<PackageVersion Include="xunit" Version="2.9.3" />
1616
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
1717

src/JUnit.Xml.Package/README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,6 @@ The `--report-spekt-junit` option can also accept configuration arguments:
5151
> dotnet test -- --report-spekt-junit "key1=value1;key2=value2"
5252
```
5353

54-
All common options to the logger are documented [in the wiki][config-wiki]. E.g.
55-
token expansion for `{assembly}` or `{framework}` in result file. If you are writing multiple
56-
files to the same directory or testing multiple frameworks, these options can prevent
57-
test logs from over-writing each other.
58-
59-
[config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration
60-
6154
### Customizing Junit XML Contents
6255

6356
There are several options to customize how the junit xml is populated. These options exist to
@@ -75,6 +68,24 @@ After the logger name, command line arguments are provided as key/value pairs wi
7568
> dotnet test --test-adapter-path:. --logger:"junit;key1=value1;key2=value2"
7669
```
7770

71+
#### Available options
72+
73+
| Option name | Purpose | Documentation |
74+
| --------------------------- | -------------------------------------------------------------------------- | ----------------- |
75+
| LogFileName\* | Customize test result file name with `{assembly}` or `{framework}` tokens | See [config-wiki] |
76+
| LogFilePath\* | Test result file full path | See [config-wiki] |
77+
| UseRelativeAttachmentPath\* | Use attachment paths relative to test result file. Boolean. Default: false | See [config-wiki] |
78+
| MethodFormat | Alter test case name | See below |
79+
| FailureBodyFormat | Control test result output | See below |
80+
| StoreConsoleOutput | Show or hide console output in the test report. Boolean. Default: true | See below |
81+
82+
\*All common options to the logger are documented [in the wiki][config-wiki]. E.g.
83+
token expansion for `{assembly}` or `{framework}` in result file. If you are writing multiple
84+
files to the same directory or testing multiple frameworks, these options can prevent
85+
test logs from over-writing each other.
86+
87+
[config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration
88+
7889
#### MethodFormat
7990

8091
This option alters the `testcase name` attribute. By default, this contains only the method. Class, will add the class to the name. Full, will add the assembly/namespace/class to the method.

src/NUnit.Xml.Package/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ The `--report-spekt-nunit` option can also accept configuration arguments:
5151

5252
### Configuration
5353

54-
Supported configuration options include:
54+
#### Available options
5555

56-
- `LogFilePath`: Sets the output file path (supports token expansion with `{assembly}` and `{framework}`)
56+
| Option name | Purpose | Documentation |
57+
| --------------------------- | -------------------------------------------------------------------------- | ----------------- |
58+
| LogFileName\* | Customize test result file name with `{assembly}` or `{framework}` tokens | See [config-wiki] |
59+
| LogFilePath\* | Test result file full path | See [config-wiki] |
60+
| UseRelativeAttachmentPath\* | Use attachment paths relative to test result file. Boolean. Default: false | See [config-wiki] |
5761

58-
All common options to the logger is documented [in the wiki][config-wiki]. E.g.
62+
\*All common options to the logger is documented [in the wiki][config-wiki]. E.g.
5963
token expansion for `{assembly}` or `{framework}` in result file.
6064

6165
[config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration

src/TestLogger/Core/LoggerConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class LoggerConfiguration
1717
public const string LogFilePathKey = "LogFilePath";
1818
public const string LogFileNameKey = "LogFileName";
1919
public const string ParserKey = "Parser";
20+
public const string UseRelativeAttachmentPathKey = "UseRelativeAttachmentPath";
2021
private const string AssemblyToken = "{assembly}";
2122
private const string FrameworkToken = "{framework}";
2223

@@ -51,6 +52,11 @@ public LoggerConfiguration(Dictionary<string, string> config)
5152

5253
public string LogFilePath => this.Values[LogFilePathKey];
5354

55+
public bool UseRelativeAttachmentPaths
56+
=> this.Values.TryGetValue(UseRelativeAttachmentPathKey, out var useRelativePathsValue) &&
57+
bool.TryParse(useRelativePathsValue, out var useRelativePaths) &&
58+
useRelativePaths;
59+
5460
public Dictionary<string, string> Values { get; }
5561

5662
public string GetFormattedLogFilePath(TestRunConfiguration runConfiguration)

src/TestLogger/Core/TestRunCompleteWorkflow.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ namespace Spekt.TestLogger.Core
1515
public static class TestRunCompleteWorkflow
1616
{
1717
public static void Complete(this ITestRun testRun, TestRunCompleteEventArgs completeEvent)
18-
=> Complete(testRun, completeEvent.AttachmentSets.SelectMany(x => x.ToAttachments()).ToList());
18+
{
19+
var logFilePath = testRun.LoggerConfiguration
20+
.GetFormattedLogFilePath(testRun.RunConfiguration);
21+
var resultsDirectory = Path.GetDirectoryName(logFilePath);
22+
var attachments = completeEvent.AttachmentSets.SelectMany(x => x.ToAttachments(baseDirectory: resultsDirectory, makeRelativePaths: testRun.LoggerConfiguration.UseRelativeAttachmentPaths)).ToList();
23+
24+
Complete(testRun, attachments);
25+
}
1926

2027
public static void Complete(this ITestRun testRun, IReadOnlyCollection<TestAttachmentInfo> testAttachmentInfos)
2128
{

src/TestLogger/Core/TestRunResultWorkflow.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Spekt.TestLogger.Core
55
{
66
using System;
77
using System.Collections.Generic;
8+
using System.IO;
89
using System.Linq;
910
using System.Reflection;
1011
using Microsoft.Testing.Platform.Extensions.Messages;
@@ -35,7 +36,7 @@ public static void Result(this ITestRun testRun, TestNodeUpdateMessage testNodeU
3536

3637
Func<string, string> sanitize = testRun.Serializer.InputSanitizer.Sanitize;
3738

38-
var attachments = testNodeUpdateMessage.TestNode.Properties.OfType<FileArtifactProperty>().Select(p => new TestAttachmentInfo(p.FileInfo.FullName, p.Description)).ToList();
39+
var attachments = testNodeUpdateMessage.TestNode.Properties.OfType<FileArtifactProperty>().ToAttachments(baseDirectory: GetTestResultDirectory(testRun), makeRelativePaths: testRun.LoggerConfiguration.UseRelativeAttachmentPaths).ToList();
3940

4041
var (errorMessage, errorStackTrace) = state switch
4142
{
@@ -154,8 +155,10 @@ string x when x.Equals("Legacy", StringComparison.OrdinalIgnoreCase) => legacyPa
154155
_ => parser.Parse(fqn),
155156
};
156157

157-
Func<string, string> sanitize = testRun.Serializer.InputSanitizer.Sanitize;
158+
// Prepare attachments with relative paths if configured
159+
var attachments = result.Attachments.SelectMany(x => x.ToAttachments(baseDirectory: GetTestResultDirectory(testRun), makeRelativePaths: testRun.LoggerConfiguration.UseRelativeAttachmentPaths)).ToList();
158160

161+
Func<string, string> sanitize = testRun.Serializer.InputSanitizer.Sanitize;
159162
testRun.Store.Add(new TestResultInfo(
160163
sanitize(parsedName.Namespace),
161164
sanitize(parsedName.Type),
@@ -173,10 +176,17 @@ string x when x.Equals("Legacy", StringComparison.OrdinalIgnoreCase) => legacyPa
173176
sanitize(result.ErrorMessage),
174177
sanitize(result.ErrorStackTrace),
175178
result.Messages.Select(x => new TestResultMessage(sanitize(x.Category), sanitize(x.Text))).ToList(),
176-
result.Attachments.SelectMany(x => x.ToAttachments()).ToList(),
179+
attachments,
177180
result.TestCase.Traits.Select(x => new Trait(sanitize(x.Name), sanitize(x.Value))).ToList(),
178181
result.TestCase.ExecutorUri?.ToString(),
179182
result.TestCase));
180183
}
184+
185+
private static string GetTestResultDirectory(ITestRun testRun)
186+
{
187+
var logFilePath = testRun.LoggerConfiguration
188+
.GetFormattedLogFilePath(testRun.RunConfiguration);
189+
return Path.GetDirectoryName(logFilePath);
190+
}
181191
}
182192
}

src/TestLogger/TestReporter.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Spekt.TestReporter
55
{
66
using System;
77
using System.Collections.Generic;
8+
using System.IO;
89
using System.Reflection;
910
using System.Runtime.Versioning;
1011
using System.Threading;
@@ -18,6 +19,7 @@ namespace Spekt.TestReporter
1819
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
1920
using Spekt.TestLogger.Core;
2021
using Spekt.TestLogger.Platform;
22+
using Spekt.TestLogger.Utilities;
2123

2224
/// <summary>
2325
/// Base test reporter implementation for Microsoft.Testing.Platform.
@@ -56,7 +58,10 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo
5658
switch (value)
5759
{
5860
case SessionFileArtifact sessionFileArtifact:
59-
this.testAttachmentInfos.Add(new TestAttachmentInfo(sessionFileArtifact.FileInfo.FullName, sessionFileArtifact.Description));
61+
// Capture session file artifacts (e.g., logs, screenshots) at test run level
62+
var baseDirectory = Path.GetDirectoryName(this.testRun.LoggerConfiguration.GetFormattedLogFilePath(this.testRun.RunConfiguration));
63+
var makeRelativePath = this.testRun.LoggerConfiguration.UseRelativeAttachmentPaths;
64+
this.testAttachmentInfos.Add(sessionFileArtifact.ToAttachment(baseDirectory, makeRelativePath));
6065
break;
6166

6267
case TestNodeUpdateMessage testNodeUpdateMessage:
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Spekt Contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Spekt.TestLogger.Utilities
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Linq;
10+
using Microsoft.Testing.Platform.Extensions.Messages;
11+
using Spekt.TestLogger.Core;
12+
13+
public static class ArtifactExtensions
14+
{
15+
public static TestAttachmentInfo ToAttachment(this SessionFileArtifact artifact, string baseDirectory, bool makeRelativePath)
16+
{
17+
var filePath = artifact.FileInfo.FullName;
18+
if (makeRelativePath && !string.IsNullOrEmpty(baseDirectory))
19+
{
20+
filePath = MakeRelativePath(baseDirectory, filePath);
21+
}
22+
23+
return new TestAttachmentInfo(filePath, artifact.Description);
24+
}
25+
26+
public static IEnumerable<TestAttachmentInfo> ToAttachments(this IEnumerable<FileArtifactProperty> artifacts, string baseDirectory, bool makeRelativePaths)
27+
{
28+
if (makeRelativePaths && !string.IsNullOrEmpty(baseDirectory))
29+
{
30+
return artifacts.Select(a =>
31+
{
32+
var relativePath = MakeRelativePath(baseDirectory, a.FileInfo.FullName);
33+
return new TestAttachmentInfo(relativePath, a.Description);
34+
});
35+
}
36+
37+
return artifacts.Select(a => new TestAttachmentInfo(a.FileInfo.FullName, a.Description));
38+
}
39+
40+
/// <summary>
41+
/// Makes a target path relative to a base directory path.
42+
/// </summary>
43+
/// <param name="baseDirectoryPath">Base directory path.</param>
44+
/// <param name="targetPath">Target file path.</param>
45+
/// <returns>File path relative to base directory path.</returns>
46+
public static string MakeRelativePath(string baseDirectoryPath, string targetPath)
47+
{
48+
// Example: basePath: C:\a\b\c, targetPath: C:\a\d\e.txt, Output: ..\..\d\e.txt
49+
// Assumes baseDirectoryPath is a directory path in any OS.
50+
if (!baseDirectoryPath.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
51+
!baseDirectoryPath.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
52+
{
53+
baseDirectoryPath += Path.DirectorySeparatorChar;
54+
}
55+
56+
// Target path can be relative, or on a different drive, just return it as is.
57+
if (!Path.IsPathRooted(targetPath) ||
58+
!string.Equals(Path.GetPathRoot(baseDirectoryPath), Path.GetPathRoot(targetPath), StringComparison.OrdinalIgnoreCase))
59+
{
60+
return targetPath;
61+
}
62+
63+
var baseUri = new Uri(baseDirectoryPath);
64+
var targetUri = new Uri(targetPath);
65+
66+
var relativeUri = baseUri.MakeRelativeUri(targetUri);
67+
var relativePath = Uri.UnescapeDataString(relativeUri.ToString());
68+
69+
// Convert URI slashes to platform-specific directory separators.
70+
return relativePath.Replace('/', Path.DirectorySeparatorChar);
71+
}
72+
}
73+
}

src/TestLogger/Utilities/AttachmentSetExtensions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ namespace Spekt.TestLogger.Utilities
1111

1212
public static class AttachmentSetExtensions
1313
{
14-
public static IEnumerable<TestAttachmentInfo> ToAttachments(this AttachmentSet attachmentSet)
14+
public static IEnumerable<TestAttachmentInfo> ToAttachments(this AttachmentSet attachmentSet, string baseDirectory, bool makeRelativePaths)
1515
{
16+
if (makeRelativePaths && !string.IsNullOrEmpty(baseDirectory))
17+
{
18+
return attachmentSet.Attachments.Select(a =>
19+
{
20+
var attachmentPath = GetPathFromUri(a.Uri);
21+
var relativePath = ArtifactExtensions.MakeRelativePath(baseDirectory, attachmentPath);
22+
return new TestAttachmentInfo(relativePath, a.Description);
23+
});
24+
}
25+
1626
return attachmentSet.Attachments.Select(a => new
1727
TestAttachmentInfo(GetPathFromUri(a.Uri), a.Description));
1828
}

src/Xunit.Xml.Package/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ The `--report-spekt-xunit` option can also accept configuration arguments:
5151

5252
### Configuration
5353

54-
All common options to the logger is documented [in the wiki][config-wiki].
54+
#### Available options
55+
56+
| Option name | Purpose | Documentation |
57+
| --------------------------- | -------------------------------------------------------------------------- | ----------------- |
58+
| LogFileName\* | Customize test result file name with `{assembly}` or `{framework}` tokens | See [config-wiki] |
59+
| LogFilePath\* | Test result file full path | See [config-wiki] |
60+
| UseRelativeAttachmentPath\* | Use attachment paths relative to test result file. Boolean. Default: false | See [config-wiki] |
61+
62+
\*All common options to the logger is documented [in the wiki][config-wiki].
5563

5664
[config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration
5765

0 commit comments

Comments
 (0)