From 576f60d80792d85578593039e2b57d6cbb74caa0 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Wed, 3 Jun 2026 00:40:00 +0100 Subject: [PATCH] fix(stdlib): TStringList.Text/LoadFromFile preserve verbatim lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../test/pascal/cp.test.e2e.tstringlist.pas | 42 +++++++++++++++++++ stdlib/src/main/pascal/classes.pas | 22 ++++------ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/compiler/src/test/pascal/cp.test.e2e.tstringlist.pas b/compiler/src/test/pascal/cp.test.e2e.tstringlist.pas index 84b9687..4a2ef8e 100644 --- a/compiler/src/test/pascal/cp.test.e2e.tstringlist.pas +++ b/compiler/src/test/pascal/cp.test.e2e.tstringlist.pas @@ -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(''); 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; diff --git a/stdlib/src/main/pascal/classes.pas b/stdlib/src/main/pascal/classes.pas index 0d5c2b7..eadfccc 100644 --- a/stdlib/src/main/pascal/classes.pas +++ b/stdlib/src/main/pascal/classes.pas @@ -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;