Skip to content
5 changes: 2 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="CSharpier.MsBuild" Version="1.2.6" />
Expand All @@ -12,9 +11,9 @@
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.2.2" />
<PackageVersion Include="MSTest" Version="4.2.2" />
<PackageVersion Include="PolyShim" Version="2.11.0" />
<PackageVersion Include="PowerKit" Version="1.1.1" />
<PackageVersion Include="PowerKit" Version="1.2.0" />
<PackageVersion Include="RazorBlade" Version="1.0.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
</Project>
62 changes: 62 additions & 0 deletions GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,66 @@ public async Task I_can_try_to_use_the_logger_to_produce_a_summary_when_the_outp
testOutput.WriteLine("Summary output:");
testOutput.WriteLine(summaryOutput);
}

[Fact]
public async Task I_can_try_to_use_the_logger_to_produce_a_summary_when_the_output_file_is_nearly_full_and_content_contains_non_ascii_characters_and_get_a_truncated_summary_within_the_size_limit()
{
// Arrange
using var testResultsDir = TempDirectory.Create();
using var summaryFile = TempFile.Create();

const int prefillSize = 1024 * 1024 - 250;
File.WriteAllZeroes(summaryFile.Path, prefillSize);

using var commandWriter = new StringWriter();

// Use a file-backed StreamWriter so that the file path is exposed internally
await using var summaryFileStream = File.Open(
summaryFile.Path,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite
);

await using var summaryWriter = new StreamWriter(summaryFileStream);

var builder = await TestApplication.CreateBuilderAsync([
"--results-directory",
testResultsDir.Path,
"--report-github",
"--report-github-summary-include-passed",
]);

// Use non-ASCII characters in test names (each Chinese character is 3 bytes in UTF-8,
// so truncating by char count instead of byte count would exceed the size limit)
builder.RegisterFakeTests(
new TestNodeBuilder()
.SetDisplayName(new string('一', 100))
.SetOutcome(TestOutcome.Failed)
.Build(),
new TestNodeBuilder()
.SetDisplayName(new string('二', 100))
.SetOutcome(TestOutcome.Failed)
.Build()
);

builder.AddGitHubActionsReporting(commandWriter, summaryWriter);

// Act
var app = await builder.BuildAsync();
await app.RunAsync();

await summaryWriter.FlushAsync();

// Assert
var commandOutput = commandWriter.ToString();
var summaryFileSize = new FileInfo(summaryFile.Path).Length;

commandOutput.Should().ContainAll("::warning", "truncated");
summaryFileSize.Should().BeLessThanOrEqualTo(1024 * 1024);

testOutput.WriteLine($"Summary file size: {summaryFileSize}");
testOutput.WriteLine("Command output:");
testOutput.WriteLine(commandOutput);
}
}
61 changes: 61 additions & 0 deletions GitHubActionsTestLogger.Tests/VsTestSummarySpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -432,4 +432,65 @@ public async Task I_can_try_to_use_the_logger_to_produce_a_summary_when_the_outp
testOutput.WriteLine("Summary output:");
testOutput.WriteLine(summaryOutput);
}

[Fact]
public async Task I_can_try_to_use_the_logger_to_produce_a_summary_when_the_output_file_is_nearly_full_and_content_contains_non_ascii_characters_and_get_a_truncated_summary_within_the_size_limit()
{
// Arrange
using var summaryFile = TempFile.Create();

const int prefillSize = 1024 * 1024 - 250;
File.WriteAllZeroes(summaryFile.Path, prefillSize);

using var commandWriter = new StringWriter();

var events = new FakeTestLoggerEvents();
var logger = new VsTestLogger();

// Use a file-backed StreamWriter so that the file path is exposed internally
using var summaryFileStream = File.Open(
summaryFile.Path,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite
);

using var summaryWriter = new StreamWriter(summaryFileStream);

logger.Initialize(
events,
new Dictionary<string, string?> { ["summary-include-passed"] = "true" },
commandWriter,
summaryWriter
);

// Act
// Use non-ASCII characters in test names (each Chinese character is 3 bytes in UTF-8,
// so truncating by char count instead of byte count would exceed the size limit)
events.SimulateTestRun(
new TestResultBuilder()
.SetDisplayName(new string('一', 100))
.SetFullyQualifiedName("TestProject.SomeTests.Test1")
.SetOutcome(TestOutcome.Failed)
.Build(),
new TestResultBuilder()
.SetDisplayName(new string('二', 100))
.SetFullyQualifiedName("TestProject.SomeTests.Test2")
.SetOutcome(TestOutcome.Failed)
.Build()
);

await summaryWriter.FlushAsync();

// Assert
var commandOutput = commandWriter.ToString();
var summaryFileSize = new FileInfo(summaryFile.Path).Length;

commandOutput.Should().ContainAll("::warning", "truncated");
summaryFileSize.Should().BeLessThanOrEqualTo(1024 * 1024);

testOutput.WriteLine($"Summary file size: {summaryFileSize}");
testOutput.WriteLine("Command output:");
testOutput.WriteLine(commandOutput);
}
}
47 changes: 18 additions & 29 deletions GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,15 @@ private string TruncateSummary(string content)
return content;

var existingSize = File.Exists(filePath) ? new FileInfo(filePath).Length : 0L;

// Calculate required size for the summary content
var contentSize = Encoding.UTF8.GetByteCount(content);
var newLineSize = Encoding.UTF8.GetByteCount(Environment.NewLine);
var requiredSize = contentSize + newLineSize * 3;

if (existingSize + requiredSize > GitHubEnvironment.SummaryFileSizeLimit)
if (existingSize + contentSize > GitHubEnvironment.SummaryFileSizeLimit)
{
var availableSize = (int)
Math.Min(
GitHubEnvironment.SummaryFileSizeLimit - existingSize - newLineSize * 3L,
int.MaxValue
);

return
// There is enough space to fit the whole content
availableSize > 0
&& requiredSize <= availableSize
? content
// There is enough space to fit some of the content
: availableSize > 0 && requiredSize > availableSize ? content[..availableSize]
// There is no space at all
Math.Min(GitHubEnvironment.SummaryFileSizeLimit - existingSize, int.MaxValue);

return availableSize > 0
? content.TruncateBytes(availableSize, Encoding.UTF8)
: string.Empty;
Comment thread
Tyrrrz marked this conversation as resolved.
}

Expand All @@ -132,9 +119,19 @@ private string TruncateSummary(string content)

public async Task CreateSummaryAsync(string content)
{
// If the summary file already contains HTML content, we need to first add two newlines
// in order to switch GitHub's parser from HTML mode back to markdown mode.
// It's safe to do it unconditionally because, if the file is empty, these newlines
// will simply be ignored.
// https://github.com/Tyrrrz/GitHubActionsTestLogger/issues/22
// The newlines are included in the content before truncation so that the byte budget
// is always accurate and the file never exceeds the size limit.
var actualContent =
Environment.NewLine + Environment.NewLine + content + Environment.NewLine;

// Truncate summary to fit into GitHub's step summary size limit
var truncated = TruncateSummary(content);
if (truncated.Length < content.Length)
var truncatedContent = TruncateSummary(actualContent);
if (truncatedContent.Length < actualContent.Length)
{
await CreateWarningAnnotationAsync(
"Test summary truncated",
Expand All @@ -147,15 +144,7 @@ If you have multiple summary providers in the same step (e.g. running multiple t
);
}

// If the summary file already contains HTML content, we need to first add two newlines
// in order to switch GitHub's parser from HTML mode back to markdown mode.
// It's safe to do it unconditionally because, if the file is empty, these newlines
// will simply be ignored.
// https://github.com/Tyrrrz/GitHubActionsTestLogger/issues/22
await summaryWriter.WriteLineAsync();
await summaryWriter.WriteLineAsync();

await summaryWriter.WriteLineAsync(content);
await summaryWriter.WriteAsync(truncatedContent);
await summaryWriter.FlushAsync();
}
}
Expand Down