This is an automated email from the ASF dual-hosted git repository.

leginee pushed a commit to branch bazel-migration
in repository https://gitbox.apache.org/repos/asf/openoffice.git

commit a03189dcea08fd90cad2a9843a32e97aeedbab58
Author: Peter Kovacs <[email protected]>
AuthorDate: Wed Aug 5 06:09:54 2026 +0200

    test: wire sal child-process suites + the OfficeConnection fixture
    
    Two gtest_test capabilities, and the last two blockers on the qa/ sweep.
    
    bin_layout — the sal child-process suites (osl/process, rtl/process,
    rtl/bootstrap) were long recorded as unwirable "without source changes",
    on the reading that they look their helper exe up at ../bin.  They don't:
    each derives the helper's directory as dir-of(own module) -> strip the
    last component -> + "bin", which under dmake's solver/bin was the
    IDENTITY, not a sibling lookup.  Naming the staging dir "bin" reproduces
    it exactly.  Helpers come from a new sal_qa_helper_exe macro (same
    compile environment as sal_qa_test, minus gtest) and are staged via
    gtest_test's `companions`.
    
    Unrelated second landmine in the same suites: both osl/process TUs use
    LPTSTR / GetEnvironmentStrings / _tcslen without including <windows.h>,
    and nothing they do include reaches it — bit-rot, since dmake gates every
    qa/ dir behind ENABLE_UNIT_TESTS=NO.  Supplied with /FIwindows.h, but the
    two want OPPOSITE flavours and swapping them compiles cleanly while
    silently walking an ANSI block as wide chars: parent = ANSI, child =
    /DUNICODE= /D_UNICODE= (empty, so its own #define is an identical
    redefinition rather than a C4005).
    
    office_connection — fixture (b), test::OfficeConnection: launch a real
    soffice with -accept=...;urp and resolve a remote context.  The launcher
    makes a throwaway user installation and exports arg-soffice / arg-user.
    There is NOTHING to port from dmake here: solenv's C++ APP1TEST rule runs
    the bare exe with only --gtest_output and never sets these; the complete
    recipe is the JAVA one (installationtest.mk::javatest), which this
    mirrors, with two forced divergences.  (1) The args go in the
    ENVIRONMENT, not as -env: command-line args — rtl::Bootstrap tries the
    command line first, but that half reads osl_getCommandArgCount(), only
    populated by SAL_IMPLEMENT_MAIN, and AOO's gtest suites all declare a
    bare main().  (2) arg-user is a NATIVE PATH, not Java's file:// URL,
    because the C++ side feeds it to getFileURLFromSystemPath().
    
    LANDMINE: a backslash is an ESCAPE CHARACTER in any rtl::Bootstrap value
    (every value goes through macro expansion; read() in sal's bootstrap.cxx
    turns \X into X), so a raw Windows path loses EVERY separator —
    "C:\Users\x" comes back "C:Usersx".  Doubled in the launcher.
    
    LANDMINE, inverting the usual runtime_dlls advice: with uno_install,
    co-locating a core UNO DLL BREAKS the bootstrap.  cppuhelper picks the
    dir to load bootstrap.uno.dll from via get_this_libpath() =
    getUrlFromAddress on ITSELF, and the exe's own dir beats PATH, so a
    co-located cppuhelper3MSC.dll makes it search the test dir.  List only
    test-only DLLs; the office closure is already on PATH via program/.
    
    The fixture ships with a MIGRATION-AUTHORED smoke test
    (main/test/qa/test_officeconnection.cxx).  Upstream's only C++ consumer
    is xmlsecurity/qa/certext, which cannot build here — it calls
    ne_ssl_cert_read() from neon, which AOO replaced with curl and removed
    from the tree — so without this the fixture would ship unexercised.
    
    Green: osl_process 7/8, rtl_Process 3/3, rtl_Bootstrap 25/30,
    //main/test:test_qa_officeconnection 3/3 (~15s).  Residual reds are test
    defects, not build: osl_process asserts an environment ORDER Windows
    doesn't use, and rtl_Bootstrap expects the default ini to be testshl2.ini
    because under the retired testshl2 harness the process literally was
    testshl2.exe.
    
    This closes the qa/ sweep: no already-migrated module still has an
    unwired C++ qa/ suite.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 CLAUDE.md                              |  76 +++++++++++++++---
 build/rules/gtest_test.bzl             | 116 +++++++++++++++++++++++++--
 main/sal/BUILD.bazel                   | 111 +++++++++++++++++++++-----
 main/sal/sal_qa.bzl                    |  49 +++++++++---
 main/test/BUILD.bazel                  |  78 ++++++++++++++++++
 main/test/qa/test_officeconnection.cxx | 141 +++++++++++++++++++++++++++++++++
 main/test/readme.md                    | 118 ++++++++++++++++++++++++---
 7 files changed, 629 insertions(+), 60 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index ba0552af62..ce1d4525b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -48,8 +48,13 @@ test          🔨  C++ unit-test infra runnable — NOW THE 
FRONT-LINE TASK: br
                    EMPTY test.log; and the CRT manifest must be EMBEDDED 
(RT_MANIFEST
                    id 1, now linked into every gtest_test exe) not just staged 
as an
                    external <exe>.manifest, or late DLL loads fall outside the
-                   activation context.  NEXT: sweep qa/ across the remaining 
migrated
-                   modules.  DONE since: 8 sal osl/socket suites + 
shell_qa_zip.
+                   activation context.  The qa/ SWEEP IS DONE: no 
ALREADY-MIGRATED module
+                   still has an unwired C++ qa/ suite (verified 2026-08-04 — 
every other
+                   qa/ dir is Java, or Perl/shell tooling as in 
basegfx+slideshow; the
+                   only C++ leftover is xmlsecurity/qa/certext, which needs 
fixture (b)).
+                   DONE since: 8 sal osl/socket suites + shell_qa_zip + the 3 
sal
+                   child-process suites (osl_process 7/8, rtl_Process 3/3,
+                   rtl_Bootstrap 25/30).
                    "Needs a CppUnit external dep" was largely a MYTH — NOTHING 
checked
                    so far actually uses cppunit.  osl/socket and shell/qa are 
plain
                    GoogleTest (now wired); writerfilter/qa/cppunittests is 
misnamed —
@@ -76,10 +81,42 @@ test          🔨  C++ unit-test infra runnable — NOW THE 
FRONT-LINE TASK: br
                    //main/svl:svl_qa_test_URIHelper.  Caveat — it depends on 
the WHOLE
                    install, so it is slow and not a unit test; only use 
uno_install
                    where UNO is genuinely bootstrapped.  (b) 
test::OfficeConnection
-                   (launch soffice -accept=…;urp, resolve over URP) is STILL 
UNWIRED
-                   and is the remaining front line — test.dll is built and its 
args
-                   come from rtl::Bootstrap (arg-soffice=path:…, arg-user=…), 
so what
-                   is missing is the process lifecycle, not the plumbing.
+                   (launch soffice -accept=…;urp, resolve over URP) is DONE 
2026-08-05:
+                   gtest_test office_connection=True (+ uno_install) makes a 
throwaway
+                   user installation and exports arg-soffice/arg-user.  GREEN:
+                   //main/test:test_qa_officeconnection (3/3 runs, ~15s — real 
office
+                   boot → URP resolve → remote Desktop → clean terminate).
+                   NOTHING TO PORT from dmake: solenv's C++ APP1TEST rule 
(_tg_app.mk)
+                   runs the bare exe with only --gtest_output and never sets 
these args
+                   at all; the complete recipe is the JAVA one
+                   (installationtest.mk::javatest), which is what was 
mirrored.  Two
+                   forced divergences from it: (1) the args go in the 
ENVIRONMENT, not
+                   as -env: command-line args — rtl::Bootstrap tries the 
command line
+                   first, but that half reads osl_getCommandArgCount(), only 
populated
+                   by SAL_IMPLEMENT_MAIN, and AOO's gtest suites all declare a 
bare
+                   main() (same root cause as 
//build/testsupport:sal_process_init);
+                   (2) arg-user is a NATIVE PATH, not Java's file:// URL, 
because the
+                   C++ side feeds it to getFileURLFromSystemPath().
+                   LANDMINE: a BACKSLASH IS AN ESCAPE CHARACTER in any 
rtl::Bootstrap
+                   value (every value goes through macro expansion; read() in 
sal
+                   bootstrap.cxx turns \X into X), so a raw Windows path loses 
EVERY
+                   separator — "C:\Users\x" comes back "C:Usersx" and
+                   getFileURLFromSystemPath fails with 21.  Double them.
+                   LANDMINE (INVERTS the usual runtime_dlls advice): with 
uno_install,
+                   co-locating a core UNO DLL BREAKS the bootstrap.  
cppuhelper picks
+                   the dir to load bootstrap.uno.dll from via 
get_this_libpath() =
+                   getUrlFromAddress on ITSELF, and the exe's own dir beats 
PATH — so a
+                   co-located cppuhelper3MSC.dll makes it search the TEST dir 
("loading
+                   component library failed: …/<test>.run/bootstrap.uno.dll"). 
 List
+                   only test-only DLLs; the office closure is already on PATH 
via
+                   program/ (svl_qa_test_URIHelper lists none, which is why it 
passed).
+                   CONSUMER NOTE: upstream's ONLY C++ user of the fixture,
+                   xmlsecurity/qa/certext, CANNOT BUILD — it needs neon
+                   (ne_ssl_cert_read) and AOO replaced neon with curl (no 
main/neon, no
+                   ext module, no neon in configure.in, ucb webdav is 
curl-based; only a
+                   stale NEON3RDLIB lingers in solenv/inc/libs.mk).  Hence the
+                   migration-authored smoke test, without which the fixture 
would ship
+                   unexercised.
                    LANDMINE for any new launcher: bazel test's CWD is NEITHER 
the exe
                    dir NOR the execroot — locate everything from %~dp0 (see
                    _windows_relpath in gtest_test.bzl).
@@ -107,10 +144,29 @@ test          🔨  C++ unit-test infra runnable — NOW THE 
FRONT-LINE TASK: br
                       env, not just uno_install=//main/staging:install.
                     • cppuhelper/qa/propertysetmixin — UNO component DLL + own
                       types.idl + a per-test services.rdb 
(packcomponents.xslt).
-                    • xmlsecurity/qa/certext — uses test::OfficeConnection ⇒ 
fixture (b).
-                    • sal child-process suites (osl/process, rtl/bootstrap,
-                      rtl/process) — helper exe via 
getExecutablePath()+"/../bin";
-                      gtest_test's `companions` hook exists for this.
+                    • xmlsecurity/qa/certext — BLOCKED, and NOT on fixture (b) 
as
+                      recorded here before: it #includes <neon/ne_ssl.h> and 
calls
+                      ne_ssl_cert_read(), but AOO deleted neon in favour of 
curl.  Would
+                      need a neon external wrap (or a source change) — do not 
treat it as
+                      a fixture-(b) consumer.
+                   DONE 2026-08-04 — sal child-process suites (osl/process, 
rtl/process,
+                   rtl/bootstrap).  The recorded blocker ("helper exe via
+                   getExecutablePath()+/../bin, needs source changes") was a 
MISREADING:
+                   the idiom is dir-of(own module) → strip last component → 
+"bin", which
+                   under dmake's solver/bin is the IDENTITY, not a sibling 
lookup.  New
+                   gtest_test `bin_layout` just names the staging dir "bin" 
and it
+                   resolves; helpers via sal_qa_helper_exe + `companions`.  
Second
+                   landmine, unrelated: BOTH osl/process TUs use LPTSTR/
+                   GetEnvironmentStrings/_tcslen without including <windows.h> 
(nothing
+                   they include reaches it — precompiled_sal.hxx is empty), 
i.e. bit-rot,
+                   since dmake gates qa/ behind ENABLE_UNIT_TESTS=NO.  Fixed 
with
+                   /FIwindows.h, but the two need OPPOSITE flavours and 
swapping them
+                   compiles cleanly while walking an ANSI block as wide chars: 
parent =
+                   ANSI, child = /DUNICODE= /D_UNICODE= (empty, so its own 
#define is an
+                   identical redefinition, not C4005).  Residual reds are test 
defects,
+                   not build: osl_process asserts an env ORDER Windows doesn't 
use;
+                   rtl_Bootstrap expects the default ini to be testshl2.ini 
because the
+                   testshl2-era process was literally testshl2.exe.
 testtools     ⬜  (bridgetest — pure-C++ UNO bridge round-trip; cli/pyuno/java 
variants
                    need rules_java — see Java bucket)
 qadevOOo      🔨  OOoRunner.jar built (//main/qadevOOo:OOoRunner — qadevOOo QA
diff --git a/build/rules/gtest_test.bzl b/build/rules/gtest_test.bzl
index 1424e1b272..9b100ec977 100644
--- a/build/rules/gtest_test.bzl
+++ b/build/rules/gtest_test.bzl
@@ -60,7 +60,16 @@ def _windows_relpath(from_dir, to_dir):
     return "..\\" * (len(f) - common) + "\\".join(t[common:])
 
 def _staged_gtest_test_impl(ctx):
-    d = ctx.label.name + ".run"
+    # The staging dir is normally "<name>.run".  bin_layout makes it
+    # "<name>.run/bin" instead — the ONE thing the child-process suites need.
+    # They locate their helper exe with the testshl2-era idiom
+    #     dir-of(own module) → strip last path component → + "bin"
+    # (osl_process.cxx::getExecutablePath, rtl_Process/rtl_Bootstrap::
+    # getModulePath).  Under dmake both parent and child lived in solver/bin, 
so
+    # that round-trip was the IDENTITY; it only looks like an "../bin" lookup.
+    # Naming the staging dir "bin" reproduces the identity exactly, so the
+    # helpers staged beside the exe are found with no source change.
+    d = ctx.label.name + ".run" + ("/bin" if ctx.attr.bin_layout else "")
     staged = []
     seen = {}
 
@@ -144,6 +153,11 @@ def _staged_gtest_test_impl(ctx):
             ]:
                 stage(f)
 
+    if ctx.attr.office_connection and not uno_program_dir:
+        fail("office_connection requires uno_install — test::OfficeConnection 
" +
+             "bootstraps an in-process context to build the URL resolver, and 
" +
+             "the soffice it launches comes from the staged install.")
+
     executable = staged_exe
     if ctx.attr.run_in_staged_dir or uno_program_dir:
         launcher_dir = staged_exe.dirname  # the .bat sits beside the staged 
exe
@@ -160,9 +174,68 @@ def _staged_gtest_test_impl(ctx):
                 # live in program/; the exe's own directory still wins for 
what it
                 # imports directly, so its staged copies are unaffected.
                 'set "PATH=%_PROG%;%PATH%"',
-                'cd /d "%_PROG%" || exit /b 1',
-                '"%_EXE%" %*',
             ]
+
+            # Working directory.  program/ by default (what the UNO bootstrap
+            # tests have always used), but the staged dir when the test also
+            # opens a fixture by bare relative name — the two demands collide,
+            # and PATH already covers the only reason program/ was the cwd.
+            if ctx.attr.run_in_staged_dir:
+                lines += ['cd /d "%~dp0" || exit /b 1']
+            else:
+                lines += ['cd /d "%_PROG%" || exit /b 1']
+
+            if ctx.attr.office_connection:
+                # test::OfficeConnection reads its two arguments through
+                # rtl::Bootstrap under an "arg-" prefix (test/getargument.cxx).
+                # There is no dmake recipe to copy: solenv's C++ APP1TEST rule
+                # runs the bare exe with only --gtest_output, so it never sets
+                # these at all; the complete recipe is the JAVA one
+                # (installationtest.mk::javatest), which this mirrors.
+                #   * arg-soffice — "path:<exe>" makes OfficeConnection LAUNCH 
an
+                #     office (with -accept=pipe,name=…;urp); "connect:<desc>"
+                #     would attach to an already-running one.
+                #   * arg-user — a fresh user installation, wiped before AND 
after
+                #     as javatest does, so no state leaks between runs.  NOTE 
it
+                #     is a NATIVE PATH, not the file:// URL the Java side 
passes:
+                #     the C++ side feeds it to toAbsoluteFileUrl(), i.e.
+                #     osl::FileBase::getFileURLFromSystemPath().
+                #
+                # Passed as ENVIRONMENT VARIABLES, not as "-env:arg-…" command
+                # line arguments, which is what the dmake/Java side implies.
+                # rtl::Bootstrap resolves a key by trying the command line 
first
+                # and the environment second (bootstrap.cxx, Bootstrap_Impl::
+                # getValue), but the command-line half reads
+                # osl_getCommandArgCount(), which is only ever populated by
+                # sal_detail_initialize() — i.e. by SAL_IMPLEMENT_MAIN.  AOO's
+                # GoogleTest suites declare a plain main() instead and skip 
that,
+                # so -env: arguments are INVISIBLE to them.  Same root cause as
+                # //build/testsupport:sal_process_init (no WSAStartup for the
+                # socket suites); the environment route needs no shim TU and
+                # cannot be broken by a future suite's choice of main().
+                lines += [
+                    'set "_USER=%TEST_TMPDIR%\\oootest_user"',
+                    'if "%TEST_TMPDIR%"=="" set "_USER=%~dp0oootest_user"',
+                    # TEST_TMPDIR arrives with forward slashes; osl's
+                    # getFileURLFromSystemPath wants native separators.
+                    'set "_USER=%_USER:/=\\%"',
+                    'if exist "%_USER%" rmdir /s /q "%_USER%"',
+                    'mkdir "%_USER%" || exit /b 1',
+                    'set "_SOFFICE=%_PROG%\\soffice.exe"',
+                    # A BACKSLASH IS AN ESCAPE CHARACTER in a bootstrap value:
+                    # rtl::Bootstrap runs every value it returns through macro
+                    # expansion, where read() (sal/rtl/source/bootstrap.cxx)
+                    # turns "\X" into "X" and "\uXXXX" into a code point.  So a
+                    # raw Windows path loses EVERY separator on the way out —
+                    # "C:\Users\x" comes back as "C:Usersx" and
+                    # getFileURLFromSystemPath then fails with 21.  Double them
+                    # so one survives each unescape.  (The dmake/Java side 
never
+                    # hit this: it passes a file:// URL, which has no
+                    # backslashes.)
+                    'set "arg-soffice=path:%_SOFFICE:\\=\\\\%"',
+                    'set "arg-user=%_USER:\\=\\\\%"',
+                ]
+            lines += ['"%_EXE%" %*']
         else:
             # Co-locating a data file with the exe is not enough for a test 
that
             # opens it by bare relative name: the working directory is the
@@ -172,7 +245,12 @@ def _staged_gtest_test_impl(ctx):
                 'cd /d "%~dp0" || exit /b 1',
                 '"%~dp0' + staged_exe.basename + '" %*',
             ]
-        lines += ["exit /b %ERRORLEVEL%", ""]
+
+        # Capture the exit code BEFORE any cleanup — rmdir would clobber it.
+        lines += ['set "_RC=%ERRORLEVEL%"']
+        if ctx.attr.office_connection:
+            lines += ['rmdir /s /q "%_USER%" 2>nul']
+        lines += ["exit /b %_RC%", ""]
 
         launcher = ctx.actions.declare_file(d + "/" + ctx.label.name + 
"_run.bat")
         ctx.actions.write(output = launcher, content = "\r\n".join(lines), 
is_executable = True)
@@ -194,6 +272,8 @@ _staged_gtest_test = rule(
         "companions": attr.label_list(cfg = "target"),
         "app_manifest": attr.label(allow_single_file = True, default = 
_APP_MANIFEST),
         "run_in_staged_dir": attr.bool(default = False),
+        "bin_layout": attr.bool(default = False),
+        "office_connection": attr.bool(default = False),
         "uno_install": attr.label(allow_files = True),
     },
 )
@@ -215,9 +295,11 @@ def gtest_test(
         data_files = [],
         uno_install = None,
         companions = [],
+        bin_layout = False,
+        office_connection = False,
         additional_linker_inputs = [],
         linkopts = [],
-        size = "small",
+        size = None,
         **kwargs):
     """A GoogleTest suite that actually runs under `bazel test` on Windows/MD.
 
@@ -237,7 +319,27 @@ def gtest_test(
         sfx.dll or sw.dll.
     It does make the test depend on the entire install, but a slow test beats 
no
     test; build time is not a reason to skip wiring something.
+
+    companions: helper exes the test spawns by name (a child process). Staged
+    beside the exe with their own external CRT manifest.
+
+    bin_layout: stage into "<name>.run/bin" instead of "<name>.run", for suites
+    that derive the helper's directory as <parent-of-own-dir>/bin (see the
+    staging rule). Implies run_in_staged_dir so the child's working directory
+    matches too.
+
+    office_connection: the test uses test::OfficeConnection, i.e. it LAUNCHES a
+    real soffice and talks to it over URP (fixture (b)). Requires uno_install.
+    Supplies arg-soffice / arg-user and a per-run user installation.
+    Defaults size to "medium" (300s): an office boot is far past the "small"
+    60s budget — ~15s observed here, and every run builds a FRESH user
+    installation so it is always a cold start. The headroom also bounds the
+    failure mode: OfficeConnection::setUp() retries the resolve in an UNBOUNDED
+    loop, so if the office never comes up the test timeout is the only thing
+    that ends it. Do not drop this to "small".
     """
+    if size == None:
+        size = "medium" if office_connection else "small"
     cc_binary(
         name = name + "_bin",
         srcs = srcs,
@@ -263,7 +365,9 @@ def gtest_test(
         binary = ":" + name + "_bin",
         runtime = runtime_dlls + data_files + [_CRT],
         companions = companions,
-        run_in_staged_dir = bool(data_files),
+        run_in_staged_dir = bool(data_files) or bin_layout,
+        bin_layout = bin_layout,
+        office_connection = office_connection,
         uno_install = uno_install,
         size = size,
     )
diff --git a/main/sal/BUILD.bazel b/main/sal/BUILD.bazel
index ee152cc1f3..8f6c5e60f9 100644
--- a/main/sal/BUILD.bazel
+++ b/main/sal/BUILD.bazel
@@ -2,7 +2,7 @@ package(default_visibility = ["//visibility:public"])
 
 # we need to load cc_binary and cc_library to be able to use them here
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
-load("//main/sal:sal_qa.bzl", "sal_qa_test")
+load("//main/sal:sal_qa.bzl", "sal_qa_helper_exe", "sal_qa_test")
 
 # Global defines for all cc_library rules in sal (Windows MSVC)
 _COPTS = [
@@ -200,19 +200,16 @@ filegroup(
 # only real gate is the test source's helper includes — all handled by the
 # macro's /Imain/sal/qa/inc + per-dir include.
 #
-# DEFERRED (not wired):
-#   * cppunit suites — osl/socket/*, rtl_strings/* → Phase 4 (needs CppUnit 
dep)
-#   * child-process tests — osl/process, rtl/bootstrap, rtl/process (parent
-#     execs a helper exe that would need separate staging)
-#   * resource/IPC tests — osl/file, osl/module, osl/pipe (need temp dirs, a
-#     loadable module, or a process pair)
-#   * systools/test_comtools — Win32 COM smart-pointers (needs ole32 + CoInit)
-#   * osl/security — Win32 SID/registry APIs (needs <windows.h> + advapi32.lib)
+# DEFERRED (not wired): osl/security only — see the note at its commented-out
+# sal_qa_test below.  Everything else once listed here is now wired: the
+# osl/socket suites (plain GoogleTest all along, + sal_process_init), the
+# resource/IPC tests (osl/file, osl/module, osl/pipe), systools/test_comtools,
+# and the child-process spawners (osl/process, rtl/process, rtl/bootstrap —
+# see the bin_layout note below).
 #
 # KNOWN UPSTREAM FAILURES (wired + run, but fail on their own merits — source 
is
-# out of scope so not fixed here): testHelperFunctions has 64-bit-shift logic
-# asserts that fail on this platform.  These remain buildable/runnable
-# individually.
+# out of scope so not fixed here): see the annotated list next to the 
test_suite
+# at the bottom of this file.  All of them remain buildable/runnable 
individually.
 #
 # FIXED: rtl_str / rtl_ustr / rtl_string previously had *_000 cases that passed
 # NULL into C string functions which dereference it (e.g. rtl_str_compare(NULL,
@@ -268,6 +265,74 @@ sal_qa_test(name = "osl_old_test_file", srcs = 
["qa/osl/file/osl_old_test_file.c
 # osl_Thread calls Win32 Sleep() (kernel32, default-linked): force-include 
<windows.h>.
 sal_qa_test(name = "osl_Thread", srcs = ["qa/osl/process/osl_Thread.cxx"], 
subdir = "osl/process", copts = ["/FIwindows.h"])
 
+# ── child-process suites ─────────────────────────────────────────────────
+# Long listed here as unwirable "without source changes", on the reading that
+# they look their helper exe up at ../bin.  They don't.  Each derives the
+# helper's directory as
+#     dir-of(own module) → strip the last path component → append "bin"
+# (osl_process.cxx::getExecutablePath, rtl_Process.cxx / rtl_Bootstrap.cxx::
+# getModulePath).  Under dmake parent and child both lived in solver/bin, so
+# that round-trip resolved back to the SAME directory — it is the identity, not
+# a sibling lookup.  gtest_test's `bin_layout` reproduces it by naming the
+# staging dir "bin"; the helpers then sit right where the suites look, with the
+# source untouched.  (bin_layout also runs the test with its working directory
+# in the staged dir, which the environment-dump cases need.)
+#
+# Both TUs of the osl/process pair walk the environment block with LPTSTR /
+# GetEnvironmentStrings() / _tcslen() but neither includes <windows.h>, and
+# nothing they do include reaches it (sal's precompiled_sal.hxx is empty and no
+# rtl header pulls it in) — dead code, since dmake gates every qa/ dir behind
+# ENABLE_UNIT_TESTS, which defaults to NO.  Source is out of scope, so supply 
it
+# with /FIwindows.h — but the two want OPPOSITE flavours, and getting that
+# backwards compiles cleanly while silently walking an ANSI block as wide 
chars:
+#   * osl_process (parent) defines neither UNICODE nor _UNICODE, and does
+#     std::string(p) / _tcslen(p) on the LPTSTR → it wants the ANSI mapping.
+#   * osl_process_child defines UNICODE + _UNICODE itself and does
+#     reinterpret_cast<wchar_t*> + WideCharToMultiByte → it wants the wide one,
+#     so UNICODE must already be defined when windows.h arrives.  Defined as
+#     EMPTY (/DUNICODE=) so the source's own `#define UNICODE` is an identical
+#     redefinition rather than a C4005.
+sal_qa_helper_exe(
+    name = "osl_process_child",
+    srcs = ["qa/osl/process/osl_process_child.cxx"],
+    subdir = "osl/process",
+    copts = ["/DUNICODE=", "/D_UNICODE=", "/FIwindows.h"],
+)
+sal_qa_test(
+    name = "osl_process",
+    srcs = ["qa/osl/process/osl_process.cxx"],
+    subdir = "osl/process",
+    copts = ["/FIwindows.h"],
+    companions = [":osl_process_child"],
+    bin_layout = True,
+)
+
+sal_qa_helper_exe(name = "child_process", srcs = 
["qa/rtl/process/child_process.cxx"], subdir = "rtl/process")
+sal_qa_helper_exe(name = "child_process_id", srcs = 
["qa/rtl/process/child_process_id.cxx"], subdir = "rtl/process")
+sal_qa_test(
+    name = "rtl_Process",
+    srcs = ["qa/rtl/process/rtl_Process.cxx"],
+    subdir = "rtl/process",
+    companions = [
+        ":child_process",
+        ":child_process_id",
+    ],
+    bin_layout = True,
+)
+
+# rtl_Bootstrap additionally writes its own bootstrap_process.ini next to the
+# exe at run time (create_bootstrap_processrc) and reads 
testshl2.ini/pseudo.ini
+# from the exe's directory via osl_getExecutableFile — all satisfied by the 
same
+# staged dir, no extra data files.
+sal_qa_helper_exe(name = "bootstrap_process", srcs = 
["qa/rtl/bootstrap/bootstrap_process.cxx"], subdir = "rtl/bootstrap")
+sal_qa_test(
+    name = "rtl_Bootstrap",
+    srcs = ["qa/rtl/bootstrap/rtl_Bootstrap.cxx"],
+    subdir = "rtl/bootstrap",
+    companions = [":bootstrap_process"],
+    bin_layout = True,
+)
+
 # osl/socket — 8 apps from qa/osl/socket/makefile.mk.  These were long listed 
as
 # "blocked on a CppUnit external dep"; that was wrong — every one of them is
 # already a plain GoogleTest suite (`#include "gtest/gtest.h"`, TEST_F) with no
@@ -349,21 +414,22 @@ sal_qa_test(name = "sal_ut_bytesequence", srcs = 
["qa/ByteSequence/ByteSequence.
 # test — passing and failing alike (failures are information, not hidden).  
Most
 # pass; the known failures fail on their OWN merits, NOT the build (source is 
out
 # of scope), and are kept in so they stay visible:
-#   testHelperFunctions — test_t_abs64 does (2147483648 << 1) in 32-bit → 0
 #   rtl_logfile — writes/reads c:/temp and asserts on it → env/permission bound
 #   rtl_textcvt — conversion/mime/codepage table expectations (sal conversion
 #       works in the running app → test-data drift)
 #   osl_File / osl_old_test_file / tcwf / osl_Module / sal_ut_osl_pipe — 
exercise
 #       the filesystem / module loader / pipes; may depend on the run 
environment
+#   osl_process — osl_execProc_merged_child_environment compares the merged 
vars
+#       against a hand-written ORDER, but Windows returns the environment block
+#       sorted case-insensitively (PAT, Patha, PATHb ≠ PAT, PATHb, Patha)
+#   rtl_Bootstrap — 4 of 5 read the DEFAULT bootstrap context and expect
+#       testshl2.ini; the default ini is <exename>.ini, and under the retired
+#       testshl2 harness the process was literally testshl2.exe
 #
-# NOT YET WIRED (hard blockers, not cosmetic):
-#   * cppunit suites — osl/socket/*, rtl_strings/* → need a CppUnit external 
dep
-#     (Phase 4); they will not compile until it is wrapped.
-#   * child-process spawners — osl/process:osl_process, 
rtl/process:rtl_Process,
-#     rtl/bootstrap:rtl_Bootstrap → resolve their helper exe via
-#     getExecutablePath()→"../bin" (the dmake solver/bin layout), which flat
-#     Bazel staging cannot satisfy without source changes.  The gtest_test rule
-#     has a `companions` hook ready for when that layout is reproduced.
+# NOT YET WIRED: osl_Security only — it does not COMPILE (testshl2 header, see
+# above), which is a different thing from an excluded-because-failing test.
+# The child-process spawners (osl_process, rtl_Process, rtl_Bootstrap) used to
+# be listed here; they are wired now via bin_layout (see above).
 test_suite(
     name = "sal_tests",
     tests = [
@@ -399,6 +465,9 @@ test_suite(
         ":tcwf",
         ":osl_old_test_file",
         ":osl_Thread",
+        ":osl_process",
+        ":rtl_Process",
+        ":rtl_Bootstrap",
         ":qa_rtl_strings",
         ":osl_SocketOld",
         ":osl_Socket_tests",
diff --git a/main/sal/sal_qa.bzl b/main/sal/sal_qa.bzl
index cc16b962dd..10fed987f7 100644
--- a/main/sal/sal_qa.bzl
+++ b/main/sal/sal_qa.bzl
@@ -10,6 +10,7 @@ only.  They differ just in their source(s) and a per-dir 
include for a local
   * sal_headers + stlport + boost; sal_implib link; sal3.dll at run time.
 """
 
+load("@rules_cc//cc:defs.bzl", "cc_binary")
 load("//build/rules:gtest_test.bzl", "gtest_test")
 
 _DEFINES = [
@@ -20,26 +21,50 @@ _DEFINES = [
     "snprintf=_snprintf",
 ]
 
+_COMMON_COPTS = [
+    "/Imain/sal/inc/pch",
+    "/Imain/sal/qa/inc",
+    "/Imain/soltools/winunistd",
+    "/Zc:wchar_t-",
+]
+
+_COMMON_DEPS = [
+    "//main/sal:sal_headers",
+    "//main/stlport:stlport",
+    "@boost.legacy//:boost.legacy",
+]
+
 def sal_qa_test(name, srcs, subdir, copts = [], deps = [], linkopts = [], 
companions = [], **kwargs):
     gtest_test(
         name = name,
         srcs = srcs,
         companions = companions,
-        copts = [
-            "/Imain/sal/inc/pch",
-            "/Imain/sal/qa/inc",
-            "/Imain/sal/qa/" + subdir,
-            "/Imain/soltools/winunistd",
-            "/Zc:wchar_t-",
-        ] + copts,
+        copts = _COMMON_COPTS + ["/Imain/sal/qa/" + subdir] + copts,
         defines = _DEFINES,
-        deps = [
-            "//main/sal:sal_headers",
-            "//main/stlport:stlport",
-            "@boost.legacy//:boost.legacy",
-        ] + deps,
+        deps = _COMMON_DEPS + deps,
         additional_linker_inputs = ["//main/sal:sal_implib"],
         linkopts = ["$(execpath //main/sal:sal_implib)"] + linkopts,
         runtime_dlls = ["//main/sal:sal3"],
         **kwargs
     )
+
+def sal_qa_helper_exe(name, srcs, subdir, copts = [], deps = [], linkopts = 
[]):
+    """A child process a sal/qa suite spawns by name (osl_process_child, …).
+
+    Same compile environment as sal_qa_test, minus gtest: these are not test
+    harnesses but plain exes that the parent launches and then inspects the 
exit
+    code of, the arguments echoed by, or a dumped environment file written by.
+    Pass them to sal_qa_test's `companions` so they are staged beside the exe.
+    """
+    cc_binary(
+        name = name,
+        srcs = srcs,
+        copts = _COMMON_COPTS + ["/Imain/sal/qa/" + subdir] + copts,
+        defines = _DEFINES,
+        deps = _COMMON_DEPS + deps,
+        additional_linker_inputs = ["//main/sal:sal_implib"],
+        # /MANIFEST:NO — the staging rule supplies an external <exe>.manifest
+        # for the VC90 CRT, exactly as it does for the test exe itself.
+        linkopts = ["$(execpath //main/sal:sal_implib)", "/MANIFEST:NO"] + 
linkopts,
+        testonly = True,
+    )
diff --git a/main/test/BUILD.bazel b/main/test/BUILD.bazel
index 0eb2079979..a91f345a8e 100644
--- a/main/test/BUILD.bazel
+++ b/main/test/BUILD.bazel
@@ -1,6 +1,7 @@
 package(default_visibility = ["//visibility:public"])
 
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
+load("//build/rules:gtest_test.bzl", "gtest_test")
 
 # main/test — libtest, AOO's C++ unit-test support library (test.dll).
 #
@@ -87,3 +88,80 @@ filegroup(
     output_group = "interface_library",
     visibility = ["//visibility:public"],
 )
+
+# ── smoke test for the OfficeConnection fixture itself ────────────────
+# MIGRATION-AUTHORED (qa/test_officeconnection.cxx) — see the long comment at
+# the top of that file.  Upstream's only C++ user of test::OfficeConnection is
+# xmlsecurity/qa/certext, which cannot build here (it needs neon, which AOO
+# replaced with curl and removed from the tree), so without this the fixture
+# would ship unexercised.
+#
+# office_connection = True is what makes this fixture (b) rather than (a): the
+# launcher creates a throwaway user installation and passes -env:arg-soffice /
+# -env:arg-user, which test::OfficeConnection reads through rtl::Bootstrap.
+# It LAUNCHES A FULL OFFICE, so it is slow by construction (14-18s, and always 
a
+# cold start — the user installation is recreated per run).
+#
+# EXPECTED: `bazel test` warns that the execution time is "outside of range for
+# MODERATE tests. Consider setting timeout=short or size=small".  Do NOT take
+# that advice.  size=small caps the timeout at 60s, barely 3x the observed time
+# on an idle machine (18.1s of that range was measured under parallel load), 
and
+# the failure mode here is an UNBOUNDED retry loop in
+# OfficeConnection::setUp() — the timeout is the only thing that ends a run
+# where the office never accepts.  The warning is the cost of that headroom.
+gtest_test(
+    name = "test_qa_officeconnection",
+    srcs = ["qa/test_officeconnection.cxx"],
+    copts = [
+        "/Imain/test/inc",
+        "/Imain/test/inc/pch",
+        "/Zc:wchar_t-",
+    ],
+    # NOT OOO_DLLIMPLEMENTATION_TEST — that is for building test.dll itself;
+    # a consumer wants the dllimport side of OOO_DLLPUBLIC_TEST.
+    defines = [
+        "WNT",
+        "GUI",
+        "WIN32",
+        "snprintf=_snprintf",
+    ],
+    deps = [
+        ":test_headers",
+        "//main/sal:sal_headers",
+        "//main/cppu:cppu_headers",
+        "//main/cppuhelper:cppuhelper_headers",
+        "//main/udkapi:udkapi_idl_headers",
+        "//main/offapi:offapi_idl_headers",
+        "//main/stlport:stlport",
+        "@boost.legacy//:boost.legacy",
+    ],
+    additional_linker_inputs = [
+        "//main/sal:sal_implib",
+        "//main/cppu:cppu3_implib",
+        "//main/cppuhelper:cppuhelper_implib",
+        ":test_implib",
+    ],
+    linkopts = [
+        "$(execpath //main/sal:sal_implib)",
+        "$(execpath //main/cppu:cppu3_implib)",
+        "$(execpath //main/cppuhelper:cppuhelper_implib)",
+        "$(execpath :test_implib)",
+    ],
+    # ONLY test.dll.  It is a test-only library, so unlike everything else here
+    # it is genuinely not part of the staged install.
+    #
+    # Do NOT add sal3/cppu3/cppuhelper here to "be safe".  With uno_install the
+    # usual advice inverts: co-locating a core UNO DLL BREAKS the bootstrap.
+    # cppuhelper resolves the directory it loads bootstrap.uno.dll and friends
+    # from via get_this_libpath() — Module::getUrlFromAddress() on ITSELF
+    # (cppuhelper/source/bootstrap.cxx) — and the loader searches the exe's own
+    # directory before PATH.  A co-located cppuhelper3MSC.dll therefore makes
+    # that path the TEST staging dir instead of program/, and the bootstrap 
dies
+    # with "loading component library failed: 
.../<test>.run/bootstrap.uno.dll".
+    # Everything in the office closure is already on PATH via program/; leave 
it
+    # there.  //main/svl:svl_qa_test_URIHelper passes for the same reason — it
+    # lists no runtime_dlls at all.
+    runtime_dlls = [":test"],
+    uno_install = "//main/staging:install",
+    office_connection = True,
+)
diff --git a/main/test/qa/test_officeconnection.cxx 
b/main/test/qa/test_officeconnection.cxx
new file mode 100644
index 0000000000..4867221e4f
--- /dev/null
+++ b/main/test/qa/test_officeconnection.cxx
@@ -0,0 +1,141 @@
+/**************************************************************
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *************************************************************/
+
+// MIGRATION-AUTHORED, not an upstream test.
+//
+// test::OfficeConnection (libtest) is the "running office" test fixture: it
+// launches a real soffice with -accept=pipe,name=...;urp and resolves a remote
+// component context over URP.  Upstream's ONLY C++ consumer of it is
+// xmlsecurity/qa/certext, which cannot be built here at all — it includes
+// <neon/ne_ssl.h> and calls ne_ssl_cert_read(), but AOO replaced neon with 
curl
+// (main/ucb/source/ucp/webdav is curl-based, configure.in has no neon option 
and
+// no neon source is in the tree; only a stale NEON3RDLIB lingers in
+// solenv/inc/libs.mk).
+//
+// So without this file the fixture would be unexercised infrastructure — the
+// kind that rots silently and is discovered to be broken by whoever first 
needs
+// it.  This exercises the whole round trip end to end: launch, URP resolve,
+// remote service creation, clean termination.  Same precedent as
+// //build/testsupport:sal_process_init.cxx.
+
+#include "sal/config.h"
+
+#include "com/sun/star/lang/XMultiComponentFactory.hpp"
+#include "com/sun/star/uno/Exception.hpp"
+#include "com/sun/star/uno/Reference.hxx"
+#include "com/sun/star/uno/XComponentContext.hpp"
+#include "com/sun/star/uno/XInterface.hpp"
+#include "rtl/bootstrap.hxx"
+#include "rtl/string.hxx"
+#include "rtl/ustring.hxx"
+
+#include <preextstl.h>
+#include "gtest/gtest.h"
+#include <postextstl.h>
+
+#include "test/officeconnection.hxx"
+
+namespace {
+
+namespace css = com::sun::star;
+
+// One office for the whole test case, not one per TEST_F: setUp() launches a
+// process and tearDown() terminates it, so a per-test fixture would boot and
+// shut down a full soffice for every assertion.
+class OfficeConnectionTest: public ::testing::Test {
+protected:
+    static test::OfficeConnection * connection_;
+
+    // OfficeConnection::setUp() can throw a UNO exception (toAbsoluteFileUrl
+    // raises RuntimeException; the resolve loop only swallows
+    // NoConnectException).  UNO exceptions do NOT derive from std::exception,
+    // so gtest reports them as a bare "Unknown C++ exception" with no message 
—
+    // useless for diagnosis.  Catch and surface the text, plus the two
+    // arguments the fixture ran with, since a wrong arg-soffice/arg-user is by
+    // far the most likely cause.
+    static void SetUpTestCase() {
+        connection_ = new test::OfficeConnection;
+        try {
+            connection_->setUp();
+        } catch (const css::uno::Exception & e) {
+            FAIL() << "OfficeConnection::setUp() threw "
+                   << rtl::OUStringToOString(
+                          e.Message, RTL_TEXTENCODING_UTF8).getStr()
+                   << " [arg-soffice=" << reportArgument("soffice")
+                   << "] [arg-user=" << reportArgument("user") << "]";
+        }
+    }
+
+    static rtl::OString reportArgument(char const * name) {
+        rtl::OUString value;
+        if (!rtl::Bootstrap::get(
+                rtl::OUString::createFromAscii("arg-") +
+                    rtl::OUString::createFromAscii(name),
+                value))
+        {
+            return rtl::OString(RTL_CONSTASCII_STRINGPARAM("<unset>"));
+        }
+        return rtl::OUStringToOString(value, RTL_TEXTENCODING_UTF8);
+    }
+
+    static void TearDownTestCase() {
+        connection_->tearDown();
+        delete connection_;
+        connection_ = 0;
+    }
+};
+
+test::OfficeConnection * OfficeConnectionTest::connection_ = 0;
+
+// The URP resolve succeeded and handed back a usable remote context.
+TEST_F(OfficeConnectionTest, resolvesComponentContext) {
+    ASSERT_TRUE(connection_ != 0);
+    ASSERT_TRUE(connection_->getComponentContext().is());
+}
+
+// The context is genuinely the OFFICE's, not a local one: ask it for its
+// service manager and instantiate a service that only a running office
+// provides.  This is what distinguishes fixture (b) from the in-process
+// bootstrap of fixture (a).
+TEST_F(OfficeConnectionTest, createsRemoteService) {
+    css::uno::Reference< css::uno::XComponentContext > context(
+        connection_->getComponentContext());
+    ASSERT_TRUE(context.is());
+
+    css::uno::Reference< css::lang::XMultiComponentFactory > manager(
+        context->getServiceManager());
+    ASSERT_TRUE(manager.is());
+
+    css::uno::Reference< css::uno::XInterface > desktop(
+        manager->createInstanceWithContext(
+            rtl::OUString(
+                RTL_CONSTASCII_USTRINGPARAM("com.sun.star.frame.Desktop")),
+            context));
+    ASSERT_TRUE(desktop.is());
+}
+
+}
+
+int main(int argc, char **argv)
+{
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/main/test/readme.md b/main/test/readme.md
index 92ff1fda42..1f34fd0682 100644
--- a/main/test/readme.md
+++ b/main/test/readme.md
@@ -57,6 +57,8 @@ holdouts).  This brings the test layer onto Bazel so suites 
run under
    - `//main/desktop:desktop_qa_dp_version` — extension version ordering
    - `//main/sal:osl_Socket_tests`, `:osl_StreamSocket`, `:osl_DatagramSocket`,
      `:osl_AcceptorSocket` (4 of the 8 socket suites; see the socket note 
below)
+   - `//main/sal:rtl_Process` (3), `:osl_process` (7/8), `:rtl_Bootstrap`
+     (25/30) — the child-process suites; see "the `bin` layout" below
 
    **Tests with private IDL types** (cppu/qa has a `types.idl` defining
    Enum1/Struct1/Interface1/… used only by the tests): reuse the `idl_library`
@@ -102,10 +104,39 @@ holdouts).  This brings the test layer onto Bazel so 
suites run under
 
    b. **Running-office connection** — `test::OfficeConnection` (libtest) starts
       a real soffice with `-accept=pipe,name=…;urp` and resolves a remote
-      context over URP. **Still unwired.** `test.dll` is built and its 
arguments
-      come from `rtl::Bootstrap` (`arg-soffice=path:<soffice.exe>`,
-      `arg-user=<user installation>`), so what is missing is the process
-      lifecycle, not the plumbing.
+      context over URP. Pass `office_connection = True` **together with**
+      `uno_install` (the fixture still bootstraps an in-process context to 
build
+      the URL resolver). The launcher then creates a throwaway user 
installation
+      and passes the two arguments the fixture reads through `rtl::Bootstrap`:
+      `-env:arg-soffice=path:<soffice.exe>` and `-env:arg-user=<dir>`.
+
+      **There is no dmake recipe to port for this.** solenv's C++ run rule
+      (`_tg_app.mk`, `APP1TEST`) invokes the bare exe with nothing but
+      `--gtest_output=`, so it never sets these at all — an upstream gap, not
+      something the migration dropped. The complete recipe is the **Java** one,
+      `installationtest.mk::javatest`, and the launcher mirrors it (including
+      wiping the user installation before *and* after). One deliberate
+      divergence: Java passes `arg-user` as a `file://` URL, but the C++ side
+      feeds it to `toAbsoluteFileUrl()` →
+      `osl::FileBase::getFileURLFromSystemPath()`, so it must be a **native
+      path**.
+
+      **Upstream's only C++ consumer cannot be built**:
+      `xmlsecurity/qa/certext` includes `<neon/ne_ssl.h>` and calls
+      `ne_ssl_cert_read()`, but AOO replaced neon with **curl** — WebDAV
+      (`ucb/source/ucp/webdav`) is curl-based, `configure.in` has no neon
+      option, and no neon source exists in the tree (only a stale `NEON3RDLIB`
+      in `solenv/inc/libs.mk`). That is a dead dependency, not a migration gap,
+      and it is why the fixture is exercised by a migration-authored smoke test
+      (`//main/test:test_qa_officeconnection`) instead — otherwise it would 
ship
+      unexercised and rot.
+
+      Note `OfficeConnection::setUp()` retries the resolve in an **unbounded**
+      loop, so a soffice that never accepts hangs until the Bazel test timeout.
+      That is why `office_connection` defaults `size` to `large`.
+
+      Green: `//main/test:test_qa_officeconnection` (~13 s — a full office 
boot,
+      URP resolve, remote service creation and clean terminate).
 
 ## Gotchas (learned the hard way)
 
@@ -176,6 +207,52 @@ holdouts).  This brings the test layer onto Bazel so 
suites run under
   [gtest_test.bzl](../../build/rules/gtest_test.bzl) does this, and it is why
   `uno_install` works from any package depth.
 
+- **A backslash is an ESCAPE character in an `rtl::Bootstrap` value.** Every
+  value `Bootstrap::get()` returns goes through macro expansion, where `read()`
+  ([sal/rtl/source/bootstrap.cxx](../sal/rtl/source/bootstrap.cxx)) turns `\X`
+  into `X` and `\uXXXX` into a code point. So a raw Windows path handed to a
+  bootstrap variable comes back with **every separator gone** —
+  `C:\Users\x` → `C:Usersx` — and `getFileURLFromSystemPath` then fails with
+  21. Double the backslashes so one survives each unescape (the `gtest_test`
+  launcher does this with cmd's `%VAR:\=\\%`). The dmake/Java side never hit
+  this because it passes a `file://` URL, which has no backslashes.
+
+- **With `uno_install`, co-locating a core UNO DLL BREAKS the bootstrap.** This
+  inverts the usual "list every transitive DLL in `runtime_dlls`" advice, so it
+  is easy to walk into while trying to be careful. cppuhelper finds the
+  directory to load `bootstrap.uno.dll` (and the other stoc bootstrap
+  components) from via `get_this_libpath()` — `Module::getUrlFromAddress()` on
+  *itself*, in 
[cppuhelper/source/bootstrap.cxx](../cppuhelper/source/bootstrap.cxx).
+  The loader searches the exe's own directory before `PATH`, so a co-located
+  `cppuhelper3MSC.dll` makes that path the *test staging dir* and the bootstrap
+  dies with `loading component library failed: …/<test>.run/bootstrap.uno.dll`.
+  Everything in the office closure is already reachable through `program/` on
+  `PATH` — leave it there. Both `//main/svl:svl_qa_test_URIHelper` and
+  `//main/test:test_qa_officeconnection` list nothing but their own test-only
+  DLL for exactly this reason.
+
+  (`bootstrap.uno.dll` is also one of the hardcoded-name cases CLAUDE.md warns
+  about for the `.uno` infix divergence — but it is staged under the upstream
+  name, so the infix is *not* what bites here. The directory is.)
+
+- **"`../bin`" was a misreading — it is the identity.** The child-process
+  suites were long recorded as unwirable without source changes because they
+  "resolve their helper exe via `getExecutablePath()`+`/../bin`". They don't.
+  The idiom (`osl_process.cxx::getExecutablePath`, `rtl_Process.cxx` /
+  `rtl_Bootstrap.cxx::getModulePath`) is
+
+  ```
+  dir-of(own module) → strip the last path component → append "bin"
+  ```
+
+  and under dmake both parent and child lived in `solver/bin`, so that
+  round-trip resolves back to the *same* directory. Naming the staging dir
+  `bin` reproduces it exactly — that is all `gtest_test`'s `bin_layout` does
+  (it also implies `run_in_staged_dir`). No source change, no `companions`
+  layout gymnastics. Worth remembering as a class: a path expression that looks
+  like it reaches out of the staging dir may just be a no-op round-trip that
+  the old layout satisfied trivially.
+
 - **Staging a data file beside the exe is not enough.** Co-located DLLs resolve
   regardless (the loader searches the exe's own path), which makes it easy to
   assume relative file opens will too — they don't. A test that opens a 
fixture by bare relative name
@@ -203,11 +280,11 @@ holdouts).  This brings the test layer onto Bazel so 
suites run under
 
 ## The sal suite is deliberately NOT a green gate
 
-`//main/sal:sal_tests` runs **every** migrated self-contained sal/qa test — 45
+`//main/sal:sal_tests` runs **every** migrated self-contained sal/qa test — 48
 targets, passing and failing alike, on the principle that failures are
 information, not something to hide (the rationale lives next to the
 `test_suite` in [main/sal/BUILD.bazel](../sal/BUILD.bazel)). So expect it to be
-red. As of 2026-08-02, 34 pass and these 11 fail, each on its **own merits** —
+red. As of 2026-08-04, 35 pass and these 13 fail, each on its **own merits** —
 none is a build or loader problem, and the source is out of scope:
 
 | Target | Failing | Why |
@@ -219,6 +296,8 @@ none is a build or loader problem, and the source is out of 
scope:
 | `osl_Thread` | 1 | `resume_001` is a timing race; flaky, not deterministic |
 | `rtl_OUString2` | 1 | `convertFromString` expects `\x80` to fail UTF-8 
validation — test-data drift, same class as `rtl_textcvt` |
 | `qa_rtl_strings` | 1 | `Convert.convertToString` — same text-conversion 
drift |
+| `osl_process` | 1 | `osl_execProc_merged_child_environment` asserts the 
merged vars come back in the order the test listed them (`std::equal` over a 
vector), but Windows returns the environment block sorted case-insensitively — 
`PAT`, `Patha`, `PATHb` vs the expected `PAT`, `PATHb`, `Patha`. Ordering bug 
in the test |
+| `rtl_Bootstrap` | 5 | 4 of them (`getFrom_004_1`, `setIniFilename_002`, 
`testOverride`, `testNonexisting`) read the **default** bootstrap context and 
expect `testshl2.ini` — the default ini is `<exename>.ini`, and under the 
retired testshl2 harness the process *was* `testshl2.exe` (the log shows ours 
resolving `rtl_Bootstrap.ini`). `expandMacrosFrom_002_2` is ini cross-reference 
precedence |
 | `osl_SocketOld` | 10 | see socket note below |
 | `osl_SocketAddr` | 3 | see socket note below |
 | `osl_Socket2` | 7 | see socket note below |
@@ -283,8 +362,25 @@ already wired (`rtl_OString2`, `rtl_str`, `rtl_string`, 
`rtl_OUString2`,
 `rtl_ustr`). So it is **dead weight, not a blocker**; there is nothing to gain
 by migrating it.
 
-Still unwired: child-process tests (`osl/process`, `rtl/bootstrap`,
-`rtl/process`) → they resolve their helper exe via 
`getExecutablePath()`+`"/../bin"`
-(the dmake `solver/bin` layout), which flat Bazel staging can't satisfy without
-source changes — `gtest_test` has a `companions` hook ready for when that 
layout
-is reproduced.
+The child-process tests (`osl/process`, `rtl/process`, `rtl/bootstrap`) were
+listed here as unwired for exactly that reason; they are wired now via
+`bin_layout` + `companions` (see the gotcha above). Their helper exes are built
+by `sal_qa_helper_exe` in [sal_qa.bzl](../sal/sal_qa.bzl) — the same compile
+environment as `sal_qa_test` minus gtest.
+
+Both TUs of the `osl/process` pair walk the environment block with `LPTSTR` /
+`GetEnvironmentStrings()` / `_tcslen()` but **neither includes `<windows.h>`**,
+and nothing they include reaches it (`precompiled_sal.hxx` is empty; no rtl
+header pulls it in). That is bit-rot, not a migration gap — dmake gates every
+`qa/` dir behind `ENABLE_UNIT_TESTS`, which defaults to NO, so these had not
+compiled in years. Supplied with `/FIwindows.h`, but the two want **opposite**
+flavours and getting it backwards compiles cleanly while silently walking an
+ANSI block as wide chars: the parent defines neither `UNICODE` nor `_UNICODE`
+and does `std::string(p)` → ANSI; the child defines both itself and does
+`reinterpret_cast<wchar_t*>` → needs `UNICODE` defined *before* `windows.h`
+arrives, hence `/DUNICODE=` (empty, so the source's own `#define UNICODE` is an
+identical redefinition rather than a C4005). `/DNOMINMAX` is already global in
+the toolchain, so force-including `windows.h` ahead of `<algorithm>` is safe.
+
+Still unwired: nothing in sal/qa except `osl_Security`, which does not compile
+(testshl2 header — see the gotcha above).

Reply via email to