Skip to content
62 changes: 62 additions & 0 deletions GitHubActionsTestLogger.Tests/MtpSummarySpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,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 @@ -431,4 +431,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);
}
}
24 changes: 22 additions & 2 deletions GitHubActionsTestLogger/GitHub/GitHubWorkflow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ await CreateAnnotationAsync(
column
);

private static string TruncateToUtf8ByteCount(string value, int byteLimit)
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
{
if (byteLimit <= 0)
return string.Empty;

if (Encoding.UTF8.GetByteCount(value) <= byteLimit)
return value;

var bytes = Encoding.UTF8.GetBytes(value);
var limit = byteLimit;

// If the byte at 'limit' is a UTF-8 continuation byte (10xxxxxx), back up to the start
// of the multi-byte sequence so we don't split a character in the middle.
while (limit > 0 && (bytes[limit] & 0xC0) == 0x80)
limit--;

return Encoding.UTF8.GetString(bytes, 0, limit);
}

private string TruncateSummary(string content)
{
// Try to extract the underlying file path from the summary writer to monitor file size
Expand Down Expand Up @@ -122,7 +141,8 @@ private string TruncateSummary(string content)
&& requiredSize <= availableSize
? content
// There is enough space to fit some of the content
: availableSize > 0 && requiredSize > availableSize ? content[..availableSize]
: availableSize > 0 && requiredSize > availableSize
? TruncateToUtf8ByteCount(content, availableSize)
// There is no space at all
: string.Empty;
Comment thread
Tyrrrz marked this conversation as resolved.
}
Expand Down Expand Up @@ -155,7 +175,7 @@ If you have multiple summary providers in the same step (e.g. running multiple t
await summaryWriter.WriteLineAsync();
await summaryWriter.WriteLineAsync();

await summaryWriter.WriteLineAsync(content);
await summaryWriter.WriteLineAsync(truncated);
await summaryWriter.FlushAsync();
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
}
}
Expand Down