diff --git a/Directory.Packages.props b/Directory.Packages.props
index 772edb4..6dbed63 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,7 +2,6 @@
true
-
@@ -12,9 +11,9 @@
-
+
-
+
\ No newline at end of file
diff --git a/GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs b/GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs
index ff5dcfc..90dd1fd 100644
--- a/GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs
+++ b/GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs
@@ -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);
+ }
}
diff --git a/GitHubActionsTestLogger.Tests/VsTestSummarySpecs.cs b/GitHubActionsTestLogger.Tests/VsTestSummarySpecs.cs
index b34035a..4911875 100644
--- a/GitHubActionsTestLogger.Tests/VsTestSummarySpecs.cs
+++ b/GitHubActionsTestLogger.Tests/VsTestSummarySpecs.cs
@@ -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 { ["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);
+ }
}
diff --git a/GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs b/GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs
index c22aa11..3d4a112 100644
--- a/GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs
+++ b/GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs
@@ -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;
}
@@ -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",
@@ -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();
}
}