Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add a test suite under Tests - #103

Merged
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite
Jul 25, 2026
Merged

Add a test suite under Tests#103
pleriche merged 5 commits into
pleriche:masterfrom
TetzkatLipHoka:test-suite

Conversation

@TetzkatLipHoka

Copy link
Copy Markdown
Contributor

Nine console test programs plus a script that builds and runs them with every Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas changes - this is purely additive, and if you never run it, it costs nothing.

The motivation is #102: the corruption scan silently stopped seeing small debug blocks, and it stayed unnoticed because there was nothing that would have run the same check across all three size classes after a change. FastMM5Test_ScanCoverage is exactly that check, and it fails on the affected commits and passes on either side of them.

How it works

No test framework. Each program is a plain console application that prints one line per check and exits with 0 when everything passed, otherwise with the number of failed checks. RunTests.ps1 builds and runs them all and exits with the number of failed runs, so it drops straight into a CI step:

pwsh -File Tests\RunTests.ps1 # every configured compiler, Win32 and Win64
pwsh -File Tests\RunTests.ps1 -Only D13.1 # a single compiler
pwsh -File Tests\RunTests.ps1 -Quick # shorter stress runs

The $Compilers table at the top of the script is the only thing that needs adjusting for a different machine.

What is covered

ProgramWhat it covers
FastMM5Test_DebugModeEntering and leaving debug mode, allocate/write/realloc/free in both modes, allocated bytes returning to the starting value.
FastMM5Test_SizeClassesEvery size class from 1 byte to 2 MB plus reallocations across the boundaries, each verified with a fill pattern.
FastMM5Test_UsagePerSizeClassChurn per size class: nothing may stay allocated, and the committed address space must not keep growing from phase to phase.
FastMM5Test_ModeTransitionThe Begin/End contract from #85, including the case that a failed Begin still has to be balanced.
FastMM5Test_DoubleFree#73: the second free is rejected and leaves the pending free list intact (no self cycle). A walker thread sleeping on the block under test forces the pending free path deterministically.
FastMM5Test_ScanCoverage#102: corrupted header checksum, overrun into the footer, and write into a freed block - across small, medium and large.
FastMM5Test_ScanRaceThe other direction: threads churning small debug blocks while a scanner runs must not produce a false positive or a crash.
FastMM5Test_ScanHeaderBoundsCorrupts UserSize / StackTraceEntryCount, i.e. the fields that decide where the scan reads; each case must give a clean report rather than an A/V.
FastMM5Test_MultiThreadStressMultithreaded stress with optional cross thread frees through a lock free mailbox; content integrity plus closing balance.

FastMM_TestUtils.pas holds the assertions and the exit code convention. It also clears FastMM_MessageBoxEvents and FastMM_LogToFileEvents for the duration of a run, since several tests corrupt blocks on purpose and a modal dialog would hang an unattended run.

Testing

36 of 36 runs pass: Delphi 10 Seattle and Delphi 13.1, each Win32 and Win64.

Two notes

The Tests/README.md records two things that cost me time and would otherwise be rediscovered by whoever writes the next test: corrupting a freed small block is not observable (raising the report allocates the exception object, which is handed exactly that block, so the process dies before any handler runs - use medium or large blocks there), and a corruption test that happens to use a large block proves nothing about the small block path, which is how #102 stayed hidden.

The sources carry {$if CompilerVersion >= ...} guards in a few places so the same files also build on older compilers in a fork. They are inert on XE3 and later; happy to strip them if you would rather not carry them.

Nine console test programs plus a script that builds and runs them with every
Delphi installation it finds, for Win32 and Win64. Nothing in FastMM5.pas
changes; this is purely additive.
Each test exits with 0 when all of its checks passed and with the number of
failures otherwise, so no test framework is needed and the runner (or a CI step)
only has to look at the exit code. RunTests.ps1 exits with the number of failed
runs.
Covered: the block size classes including reallocations across their
boundaries, debug mode basics, usage accounting per size class (both leaks and
unbounded address space growth), the mode transition contract from pleriche#85, double
free handling from pleriche#73, the corruption scan from pleriche#102 in both directions
(detection must work, false positives must not happen), corrupted size fields in
the debug header, and multithreaded stress with cross thread frees.
Verified with Delphi 10 Seattle and Delphi 13.1, Win32 and Win64: 36 of 36 runs
pass.
@pleriche

Copy link
Copy Markdown
Owner

Looks good.

One thing I would prefer though is if the list of compilers to use was external, e.g. in CompilerPaths.txt (added to .gitignore) so the script doesn't need to be edited. If there's no CompilerPaths.txt then it would be really awesome if the script could detect and use the latest compiler automatically. We use MSBuild for our build process with the latest installed compiler. It's a crude batch file, but it does the job:

rem Find the rsvars batch file for the most preferred Delphi version
rem 12 (Yukon)
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\23.0\bin\rsvars.bat"
rem 11 (Alexandria)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\22.0\bin\rsvars.bat"
)
rem 10.4 (Sydney)
if not exist %SetVarsBatchFile% (
set SetVarsBatchFile="%EmbarcaderoRoot:"=%\Studio\21.0\bin\rsvars.bat"
)
rem Initialize the msbuild environment
call %SetVarsBatchFile%

RunTests.ps1 no longer carries a table of paths that has to be edited. It reads
the installed versions from the registry (HKCU and HKLM, including the 32 bit
view) and from %EmbarcaderoRoot% if that is set, and uses the newest one by
default. -AllCompilers runs all of them, -ListCompilers just reports what was
found.
Where the list has to be pinned - other install locations, a specific set of
versions, or a machine where the registry cannot be read - a CompilerPaths.txt
next to the script takes over, with one installation root per line and an
optional "Name = Path". All of its entries are used, so it doubles as "run
against exactly these". It is in .gitignore, so a local setup never shows up as
a change.
The reported name of an installation is its IDE version, and next to it the
compiler version from the registry, which is the number the CompilerVersion
guards in the sources refer to.
Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens
and 13.1 - 72 of 72 runs pass - and with the automatic path, CompilerPaths.txt,
-Only, -AllCompilers and a deliberately broken entry.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Done - pushed, and the hard coded table is gone entirely.

Automatic by default. The installed versions are read from the registry (Embarcadero\BDS under both HKCU and HKLM, including the 32 bit view) and from %EmbarcaderoRoot%\Studio\* if that variable is set, and the newest one is used. No configuration, nothing to edit. -ListCompilers reports what it found:

Delphi installations found (* = selected):
* 37.0 (compiler version 37) C:\Delphi\13.1
23.0 (compiler version 29) C:\Delphi\12.3
22.0 (compiler version 28) C:\Delphi\11.3
21.0 (compiler version 27) C:\Delphi\10.4
...

The name is the IDE version and next to it the compiler version, taken from ProductVersion in the registry - which is exactly the number the CompilerVersion guards in the sources refer to, so it lines up with what one actually cares about when a test behaves differently on one version. -AllCompilers runs all of them, -Only 23.0 picks one.

CompilerPaths.txt takes over when it exists, with one installation root per line and an optional Name = Path:

Athens = C:\Program Files (x86)\Embarcadero\Studio\23.0
%ProgramFiles(x86)%\Embarcadero\Studio\22.0

Environment variables are expanded, # and ; start a comment, and an entry whose path has no bin\dcc32.exe is reported and skipped rather than failing the run. Since listing paths explicitly is a statement of intent, all entries in the file are used - so it doubles as "run against exactly these". It is in .gitignore.

One deliberate limitation worth mentioning: only the Embarcadero era versions are found automatically, because those are the ones registered under Embarcadero\BDS. Delphi 7 in my own tree sits under Borland\Delphi and does not show up - which is correct for this suite, since it targets XE3 and later like FastMM5 does, and CompilerPaths.txt covers it for a fork. The README says so.

Verified on Win32 and Win64 with Delphi 10 Seattle, 11.3 Alexandria, 12.3 Athens and 13.1: 72 of 72 runs pass. That is a good deal broader than the two versions the first push claimed, and it came for free once selecting compilers stopped being a manual step. I also exercised the paths themselves: automatic detection, CompilerPaths.txt, -Only with a name from the file, -Only with a name that does not exist (lists what is available, exit code 1), -AllCompilers, and an intentionally broken path in the file.

Nothing in the suite links an .obj, so this changes nothing for a current
Delphi, but it lets the same command line serve a compiler whose RTL declares
external routines from .obj files - which is what makes the script usable
unchanged in a fork that also targets older versions through CompilerPaths.txt.
Versions before the platform aware RTL layout keep their DCUs directly in
"lib" rather than in "lib\win32\release". Falling back to it means the same
script serves those too when they are listed in CompilerPaths.txt, and it is
inert for every version that has the per platform directories.
@TetzkatLipHoka

Copy link
Copy Markdown
ContributorAuthor

Two small follow-up commits, both of which I would understand you reverting - they are there because they let one script serve older compilers listed in CompilerPaths.txt, and they keep the file identical between here and a fork rather than permanently diverging:

  • -O<rtl> is now passed alongside -U<rtl>. Nothing in the suite links an .obj, so it does nothing for a current Delphi; it matters for a compiler whose RTL declares external routines from .obj files.
  • If lib\win32\release does not exist, the flat lib directory is used. Inert for every version that has the per platform layout.

With those two, and nothing else, the suite builds and passes under Delphi 7 as well, which is a useful canary for the CompilerVersion guards. Total now 45 of 45: D7 Win32, plus Seattle, 11.3, 12.3 and 13.1 - and Win64 is skipped rather than failed where there is no dcc64.

If you would rather the script stayed strictly XE3+, say so and I will drop both; the automatic detection and CompilerPaths.txt you asked for do not depend on them.

@pleriche

Copy link
Copy Markdown
Owner

Great work, thanks! I've merged the pull request.

@TetzkatLipHoka
TetzkatLipHoka deleted the test-suite branch July 25, 2026 20:54
TetzkatLipHoka added a commit to TetzkatLipHoka/FastMM5 that referenced this pull request Jul 26, 2026
The correctness test predates the suite that came in with pleriche#103, so it had
its own scaffolding and its own name. It is now FastMM5Test_FillPattern:
it uses FastMM_TestUtils, reports one check per size rather than one per
byte position, and names the first missed offset when a size fails.
RunTests.ps1 runs it with everything else - all 34,949 byte positions take
about a fifth of a second, so it needs no shortened quick variant.
fillbench.dpr and Measure.ps1 become FastMM5Bench_FillPattern and
MeasureFillPattern.ps1. They stay outside the suite on purpose, since they
report a time rather than a pass or a fail, and the Bench prefix says so.
The harness no longer hard codes the directory the two builds live in.
README-sse2.md records janrysavy's independent reproduction on Ryzen 9
7950X and Core i7-8750H, and his finding that a straightforward Win64
integration costs the small sizes 3.58% through register saves and code
movement alone, even though they never execute the vector path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TetzkatLipHoka@pleriche