fix(stdlib): TStringList.Text/LoadFromFile preserve verbatim lines

SplitIntoList — the sole backend for TStringList.SetText and LoadFromFile —
trimmed leading and trailing spaces from each split segment. That is correct
for CSV-style splitting but wrong for line-splitting: a TStringList must store
each line verbatim, including indentation.

Removed the trimming so each segment is added as Copy(S, Start, Len) untouched.
SplitIntoList has exactly one caller (SetText), so no trimming-dependent code
is affected.

This also fixes a latent test-infrastructure bug: the threaded test runner
parses each subprocess suite's stdout via TStringList.Text and identifies
failure-detail lines by their two-space indentation. With indentation stripped,
those lines never matched, AddFailure was never called, and the merged result
reported zero failures — so the plain summary printed OK even when [Threaded]
suites failed. With lines preserved, the summary now correctly reports failures.

Regression test: TestRun_TextSet_PreservesLeadingWhitespace asserts on raw
stdout so the spaces inside [] are checked without round-tripping through Text.

Self-hosting fixpoint verified clean.
This commit is contained in:
Graeme Geldenhuys 2026-06-03 00:40:00 +01:00
parent 99b7761bf4
commit 576f60d807
2 changed files with 49 additions and 15 deletions

View file

@ -39,6 +39,7 @@ type
procedure TestRun_TextGet;
procedure TestRun_TextGet_ManyLines;
procedure TestRun_TextSet;
procedure TestRun_TextSet_PreservesLeadingWhitespace;
procedure TestRun_GrowBeyondInitialCapacity;
procedure TestRun_Sorted_Add_OrderedOutput;
procedure TestRun_Sorted_Find;
@ -278,6 +279,31 @@ const
end.
''';
{ Text/LoadFromFile must preserve each line verbatim, including leading and
trailing whitespace — standard TStringList semantics. Regression for the
SplitIntoList trimming bug that silently dropped indentation (which in turn
masked failures in the threaded test-runner's subprocess output parser).
The program prints each line wrapped in [] so the harness can see the
surrounding spaces without itself round-tripping through Text. }
SrcTextSetPreservesWhitespace =
'''
program P;
uses Classes;
var L: TStringList; I: Integer;
begin
L := TStringList.Create;
L.Text := 'a' + #10 + ' indented' + #10 + 'trailing ';
WriteLn(L.Count);
I := 0;
while I < L.Count do
begin
WriteLn('[' + L.Get(I) + ']');
I := I + 1
end;
L.Free
end.
''';
SrcGrow =
'''
program P;
@ -694,6 +720,22 @@ begin
end
end;
procedure TE2ETStringListTests.TestRun_TextSet_PreservesLeadingWhitespace;
var
Output: string;
RCode: Integer;
begin
if not ToolchainAvailable then begin Fail('<toolchain-missing>'); Exit end;
AssertTrue('compile+run',
CompileAndRunWithRTL(SrcTextSetPreservesWhitespace, Output, RCode));
AssertEquals('exit 0', 0, RCode);
{ Assert on the raw stdout so the leading/trailing spaces inside [] are
checked directly, without round-tripping through Text. }
AssertEquals('verbatim lines preserved',
'3' + #10 + '[a]' + #10 + '[ indented]' + #10 + '[trailing ]' + #10,
Output);
end;
procedure TE2ETStringListTests.TestRun_GrowBeyondInitialCapacity;
var
Output: string;

View file

@ -273,9 +273,12 @@ procedure SplitIntoList(const S: string; ASep: Integer; AList: TStringList);
var
I: Integer;
Start: Integer;
SLo: Integer;
SHi: Integer;
begin
{ Line-splitter for TStringList.Text / LoadFromFile. Each segment between
separators is added verbatim — leading and trailing whitespace is part of
the line and must be preserved (standard TStringList semantics). Trimming
here previously stripped indentation, which silently corrupted any
whitespace-significant text round-tripped through Text. }
AList.Clear;
Start := 0;
I := 0;
@ -283,24 +286,13 @@ begin
begin
if OrdAt(S, I) = ASep then
begin
{ Trim surrounding spaces }
SLo := Start;
SHi := I - 1;
while (SLo <= SHi) and (OrdAt(S, SLo) = 32) do SLo := SLo + 1;
while (SHi >= SLo) and (OrdAt(S, SHi) = 32) do SHi := SHi - 1;
AList.Add(Copy(S, SLo, SHi - SLo + 1));
AList.Add(Copy(S, Start, I - Start));
Start := I + 1;
end;
I := I + 1;
end;
if Start < Length(S) then
begin
SLo := Start;
SHi := Length(S) - 1;
while (SLo <= SHi) and (OrdAt(S, SLo) = 32) do SLo := SLo + 1;
while (SHi >= SLo) and (OrdAt(S, SHi) = 32) do SHi := SHi - 1;
AList.Add(Copy(S, SLo, SHi - SLo + 1));
end;
AList.Add(Copy(S, Start, Length(S) - Start));
end;