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 260b8d088b9acdd4c27795084a0bbae911b72308
Author: Peter Kovacs <[email protected]>
AuthorDate: Thu Aug 6 08:04:21 2026 +0200

    test(java): the Java/UNO test fixture — uno_junit_test, 4 suites green
    
    The qa/ directories hold a large body of JAVA suites — qa/complex/* 
(hand-written
    JUnit) and qa/unoapi (the qadevOOo UNOAPI runner) — that drive a real 
office over
    UNO.  They are the only coverage for anything that exists solely as a UNO 
service,
    and none of them could run here.  New rule //build/rules:junit_test.bzl
    (uno_junit_test) ports solenv/inc/installationtest.mk::javatest: a 
java_library
    plus a launcher .bat running org.junit.runner.JUnitCore against a staged 
office.
    
    It is the JAVA HALF of the same fixture gtest_test(office_connection=True)
    already provides, and the three differences all bite: the arguments are -D 
SYSTEM
    PROPERTIES rather than environment variables (System.getProperty vs
    rtl::Bootstrap), arg.user is a file:/// URL rather than a native path (Java 
feeds
    it to -env:UserInstallation=, C++ to getFileURLFromSystemPath), and the 
transport
    is a NAMED PIPE rather than a socket — so there is no port to reserve, no
    exclusive tag, and the suites run concurrently.
    
    GREEN:
      //main/qadevOOo:qa_complex_junitskeleton  upstream's worked example, and 
so the
                                                whole path: connect, service 
factory,
                                                fixture document by relative 
path,
                                                LOAD it into the office, close, 
ask
                                                the office for its temp dir (3, 
~14s)
      //main/svl:qa_complex_passwordcontainer   
com.sun.star.task.PasswordContainer
                                                with and without a master 
password,
                                                persistent and session-only, 
through
                                                the test's own 
XInteractionHandler —
                                                a UNO-only service with no 
possible
                                                C++ equivalent               
(3, ~13s)
      //main/svtools:qa_unoapi                  first UNOAPI suite ever run 
here: 26
                                                interface/property checks on
                                                svtools.AccessibleTabBar        
(~25s)
      //main/qadevOOo:qa_unoapi                 the runner testing itself, i.e. 
the
                                                guard on the framework the other
                                                unoapi suites are built from    
(~16s)
    
    JUnit 4.10 via http_file (@junit_jar -> //build/third_party/junit), 
upstream's
    OOO_JUNIT_JAR, never in ext_sources.  4.10 deliberately: it EMBEDS 
hamcrest-core,
    so there is no second jar to keep in sync (every upstream recipe forks on
    HAMCREST_CORE_JAR).
    
    THREE PRODUCT GAPS surfaced, each invisible until a foreign process loaded 
our
    DLLs:
    
      1. jpipe.dll + jpipx.dll were never staged.  Nothing in the office links 
or
         loads them — the CLIENT JVM does — so their absence broke nothing 
visible.
         Any external Java program talking to a running office over a pipe needs
         them; upstream ships both in the URE lib dir.
    
      2. A /MD DLL loaded by a process we do NOT build needs its OWN EMBEDDED
         MANIFEST.  This tree links /MANIFEST:NO throughout, which is invisible
         inside soffice.exe (its manifest covers the whole process) and fatal 
for a
         DLL a stock java.exe loads: nothing supplies an activation context,
         MSVCR90.dll cannot be resolved, and System.loadLibrary reports only the
         uninformative "Can't find dependent libraries" — a loose msvcr90.dll on
         PATH would just trade that for R6034.  New
         //main/external/msvcp90:vc90_dll_manifest_res is the same manifest at
         RT_MANIFEST id 2 (the DLL slot; id 1 is the EXE slot), linked into 
both pipe
         DLLs.  Upstream gets this free: solenv embeds a manifest in every DLL. 
 Any
         future DLL a foreign host loads needs it.
    
      3. OOoRunner.jar carried no object descriptions.  helper/APIDescGetter 
reads a
         description either from a -objdsc DIRECTORY or, absent that argument, 
from
         the classpath resource /objdsc/<module> (it has a JarURLConnection 
branch
         for exactly that).  The qa/unoapi Test.java adapters pass no -objdsc, 
and
         upstream's ant jar target does not include *.csv, so against that jar 
every
         unoapi suite dies at its first object with "couldn't find module".
         Bundling them is a deliberate divergence and is what makes the whole 
unoapi
         category runnable.
    
    OFFICE SHUTDOWN DEADLOCK, and the general escape from it.  
qadevOOo/qa/unoapi ran
    its assertions green in 3s and then hung to the 300s timeout.  Diagnosed, 
not
    guessed: the JVM's main thread sits in Process.waitFor(), i.e. 
XDesktop.terminate()
    already returned and the office did not exit; the office is alive, idle, 
has no
    visible window and no longer answers a fresh UNO connection; a non-invasive
    cdb -pv stack dump shows its MAIN thread inside a WinProc dispatch that 
entered a
    NESTED VCL message wait (Application::Execute -> DispatchMessageW -> 
GetMessageW),
    holding the SolarMutex, while two URP threads block in vos::OMutex::acquire 
— an
    incoming binaryurp request into fwk's LockHelper, and an sw proxy release 
coming
    back through msci_uno.  A product deadlock between the solar mutex and 
in-flight
    bridge calls, not a fixture defect: the suites holding no stale proxies at
    teardown terminate cleanly.
    
    fixture_starts_office makes the LAUNCHER own the office (start detached, 
wait for
    the accept pipe, kill it after) and the test attach with connect: instead of
    path:, which leaves OfficeConnection.tearDown() a no-op — process is null, 
so it
    neither terminates nor waits.  Same division of labour as server_args in
    gtest_test.  It is opt-in and costs tearDown's check that the office exits 
0, so
    each use records why.  Two mechanics: the readiness wait is ONE PowerShell
    process because cmd cannot list the pipe namespace at all (dir on the pipe 
device
    answers "invalid parameter" — it is a device path, not a directory) and osl 
pipes
    appear as OSL_PIPE_<SID>_<name>; and the kill matches the unique pipe name 
on the
    command line, never taskkill /im soffice.exe, which would take down a 
developer's
    session and any concurrent suite.
    
    The test JVM is arch-selected like jre_home_env (new jre_java_exe /
    jre_home_native in //build:jre.bzl).  The office is a separate process, but 
the
    JVM loads jpipe.dll itself, so a 32-bit build needs a 32-bit JVM; JAVA_HOME 
is
    pinned to the same JDK because OfficeConnection always passes
    -env:UNO_JAVA_JFW_ENV_JREHOME=true, which is what the office's own jvmfwk 
reads.
    
    The queue is larger than the frontier recorded: 18 qa/unoapi + 24 qa/complex
    directories.  Two gates on the rest — 11 of the 18 unoapi adapters pass 
-tdoc, so
    qadevOOo/testdocs must be staged (data_tree stages one file per entry; a
    directory-preserving variant is the next rule work), and several suites need
    per-module fixtures.  See main/test/readme.md.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 CLAUDE.md                                        | 123 +++++++-
 MODULE.bazel                                     |  17 ++
 build/jre.bzl                                    |  75 ++++-
 build/rules/gtest_test.bzl                       |   9 +
 build/rules/junit_test.bzl                       | 355 +++++++++++++++++++++++
 build/third_party/junit/BUILD.bazel              |  17 ++
 main/external/msvcp90/BUILD.bazel                |  28 ++
 main/external/msvcp90/vc90_dll_manifest.rc       |  25 ++
 main/external/msvcp90/vc90_dll_manifest_amd64.rc |   6 +
 main/jurt/BUILD.bazel                            |  24 +-
 main/qadevOOo/BUILD.bazel                        | 108 ++++++-
 main/staging/BUILD.bazel                         |  12 +
 main/svl/BUILD.bazel                             |  27 ++
 main/svtools/BUILD.bazel                         |  44 +++
 main/test/BUILD.bazel                            |  29 ++
 main/test/readme.md                              | 136 ++++++++-
 16 files changed, 1008 insertions(+), 27 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9e4190650d..ad0c198cff 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -67,8 +67,9 @@ test          🔨  C++ unit-test infra runnable — NOW THE 
FRONT-LINE TASK: br
                    ⇒ every osl socket call is WSANOTINITIALISED) and 
data_files (a
                    staged fixture is NOT enough — bazel test's CWD is the 
execroot, so
                    relative opens need the cd-first .bat launcher).  NEXT: 
what is
-                   genuinely left is not standalone — svl/qa/complex + 
svtools/qa/unoapi
-                   + sfx2 + writerfilter/qa/complex are Java/UNO; 
svl/qa/test_URIHelper,
+                   genuinely left is not standalone — the JAVA/UNO suites (see 
the
+                   uno_junit_test block at the end of this bucket, DONE 
2026-08-06,
+                   3 green); svl/qa/test_URIHelper,
                    configmgr/qa/unit and cppuhelper/qa/propertysetmixin 
bootstrap a UNO
                    component context.  KEY DISTINCTION (was conflated under
                    "OfficeConnection", making the work look bigger than it 
is): those
@@ -264,6 +265,104 @@ test          🔨  C++ unit-test infra runnable — NOW THE 
FRONT-LINE TASK: br
                    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.
+                   JAVA/UNO SUITES — FIXTURE DONE 2026-08-06, 3 GREEN.  New 
rule
+                   uno_junit_test (build/rules/junit_test.bzl) = the JAVA HALF 
of
+                   fixture (b), porting installationtest.mk::javatest: 
java_library +
+                   a launcher .bat that runs org.junit.runner.JUnitCore with
+                   -Dorg.openoffice.test.arg.soffice=path:<staged soffice> and
+                   .arg.user=file:///<scratch>.  SAME fixture as the C++ side, 
spelled
+                   differently, and BOTH differences bite: the args are SYSTEM
+                   PROPERTIES not env vars (Java reads System.getProperty, C++ 
reads
+                   rtl::Bootstrap), and arg.user is a file:/// URL not a 
native path
+                   (Java feeds it to -env:UserInstallation=, C++ to
+                   getFileURLFromSystemPath).  Transport differs too — Java 
connects
+                   over a NAMED PIPE (pipe,name=oootest<uuid>), so unlike 
bridgetest_urp
+                   there is no port to reserve and no tags=["exclusive"]; the 
three
+                   suites run concurrently.
+                   OFFICE SHUTDOWN DEADLOCK, and the general escape from it —
+                   uno_junit_test `fixture_starts_office`.  qadevOOo/qa/unoapi 
ran
+                   its assertions green in 3s and then HUNG to the 300s 
timeout.
+                   DIAGNOSED, not guessed: the JVM's main thread sits in
+                   Process.waitFor() (OfficeConnection:126), i.e. 
XDesktop.terminate()
+                   ALREADY RETURNED and the office did not exit; the office is 
alive,
+                   idle, has NO visible window and no longer answers a fresh 
UNO
+                   connection; a non-invasive `cdb -pv -p <pid> -c "~*k"` 
shows its
+                   MAIN thread inside a WinProc dispatch that entered a NESTED 
VCL
+                   message wait (Application::Execute → DispatchMessageW → …
+                   → GetMessageW) — so it holds the SolarMutex — while TWO URP 
threads
+                   block in vos::OMutex::acquire: an incoming binaryurp 
request into
+                   fwk's LockHelper, and an sw proxy release coming back 
through
+                   msci_uno.  So it is a solar-mutex-vs-in-flight-bridge-calls 
deadlock
+                   in the OFFICE's shutdown, not a fixture defect (the suites 
holding
+                   no stale proxies at teardown terminate cleanly); fixing it 
properly
+                   is a SOURCE change.  fixture_starts_office makes the 
LAUNCHER own
+                   the office (start detached, wait for the pipe, taskkill 
after) and
+                   the test attach with connect: instead of path:, which leaves
+                   tearDown a NO-OP (process==null ⇒ it neither terminates nor 
waits).
+                   Verdict = the JUnit result.  OPT-IN: it gives up tearDown's 
check
+                   that the office exits 0, so use it only where THAT is what 
is broken
+                   and say why.  Two mechanics: (1) the readiness wait is ONE 
PowerShell
+                   process — cmd CANNOT list the pipe namespace ("dir 
\\.\pipe\" =
+                   "invalid parameter", it is a device path) and osl pipes 
appear as
+                   \\.\pipe\OSL_PIPE_<SID>_<name> so match the NAME at the 
end; it is
+                   not optional because OfficeConnection's resolve loop has NO 
sleep
+                   when it did not start the process itself (it would spin a 
core);
+                   (2) the kill matches the UNIQUE PIPE NAME on the command 
line, never
+                   taskkill /im soffice.exe — that would kill a developer's 
session and
+                   any concurrent suite.  GREEN: //main/qadevOOo:
+                   qa_complex_junitskeleton (3, ~14s — upstream's own worked 
example,
+                   so it exercises the whole path: connect → 
XMultiServiceFactory →
+                   qadevOOo TestParameters → fixture doc by relative path → 
LOAD it into
+                   the office → close → office temp dir), //main/svl:
+                   qa_complex_passwordcontainer (3, ~13s — com.sun.star.task.
+                   PasswordContainer with/without master password, persistent +
+                   session-only, through the test's own XInteractionHandler; 
no C++
+                   equivalent is possible, it is a UNO-only service), 
//main/svtools:
+                   qa_unoapi (~25s — FIRST UNOAPI suite ever run here, 26 
interface/
+                   property checks on svtools.AccessibleTabBar), 
//main/qadevOOo:
+                   qa_unoapi (~16s — the runner testing ITSELF, i.e. the guard 
on the
+                   framework every other unoapi suite is built out of;
+                   fixture_starts_office).
+                   JUnit 4.10 via http_file (@junit_jar → 
//build/third_party/junit) —
+                   upstream's OOO_JUNIT_JAR, never in ext_sources.  4.10 
DELIBERATELY:
+                   it EMBEDS hamcrest-core, so there is no second jar (every 
upstream
+                   recipe forks on HAMCREST_CORE_JAR).
+                   THREE PRODUCT GAPS surfaced, all invisible until a FOREIGN 
process
+                   loaded our DLLs:
+                    (1) jpipe.dll + jpipx.dll were NEVER STAGED.  Nothing in 
the office
+                        links or loads them — the CLIENT JVM does — so their 
absence
+                        broke nothing visible.  Now in //main/staging.
+                    (2) a /MD DLL loaded by a process we do NOT build needs 
its OWN
+                        EMBEDDED MANIFEST.  We link /MANIFEST:NO everywhere, 
which is
+                        invisible inside soffice.exe (its manifest covers the 
process)
+                        and fatal for a DLL a stock java.exe loads: no 
activation
+                        context ⇒ MSVCR90.dll unresolvable ⇒ 
System.loadLibrary says only
+                        "Can't find dependent libraries" (and a loose 
msvcr90.dll on PATH
+                        would just trade that for R6034).  New
+                        //main/external/msvcp90:vc90_dll_manifest_res — same 
manifest at
+                        RT_MANIFEST id 2 (the DLL slot; id 1 is the EXE slot) 
— linked
+                        into both pipe DLLs.  Upstream gets this free: solenv 
embeds a
+                        manifest in every DLL.  ANY future DLL a foreign host 
loads needs
+                        this.
+                    (3) OOoRunner.jar carried NO object descriptions — see the 
qadevOOo
+                        bucket.  Fixing it is what makes the unoapi CATEGORY 
runnable.
+                   The test JVM is arch-selected like jre_home_env (new 
jre_java_exe /
+                   jre_home_native in //build:jre.bzl): the office is a 
separate process,
+                   but the JVM loads jpipe.dll ITSELF, so a 32-bit build needs 
a 32-bit
+                   JVM.  JAVA_HOME is pinned to the same JDK because 
OfficeConnection
+                   always passes -env:UNO_JAVA_JFW_ENV_JREHOME=true (the 
OFFICE's jvmfwk
+                   then reads it).
+                   THE QUEUE IS LARGE AND WAS UNDER-COUNTED HERE: 18 qa/unoapi 
+ 24
+                   qa/complex dirs, not the 4 modules this bucket used to 
name.  Two
+                   gates on the rest: (i) -tdoc — 11 of the 18 unoapi adapters 
pass a
+                   test-document root ⇒ qadevOOo/testdocs must be staged, and 
data_tree
+                   stages one FILE per entry, so a directory-preserving 
variant is the
+                   next rule work (svtools went first precisely because its 
scenario
+                   builds its document via SOfficeFactory and needs none); 
(ii) per-module
+                   fixtures — svl/qa/complex/ConfigItems needs a C++ helper 
component,
+                   and several qa/complex dirs (writerfilter's among them) 
have no
+                   makefile.mk at all, i.e. were never wired upstream either.
+                   See main/test/readme.md.
 testtools     🔨  bridgetest GREEN 2026-08-05, ALL THREE halves — 
//main/testtools:
                    bridgetest (C++ object in-process, ~1.1s), :bridgetest_java 
(Java
                    object over the java_uno JNI bridge, ~1.4s) and 
:bridgetest_urp
@@ -375,10 +474,22 @@ testtools     🔨  bridgetest GREEN 2026-08-05, ALL THREE 
halves — //main/tes
                    reported.  Fixing either is a source change.
 qadevOOo      🔨  OOoRunner.jar built (//main/qadevOOo:OOoRunner — qadevOOo QA
                    framework, ~2137 classes; classpath ridl/unoil/jurt/juh_jar/
-                   java_uno_jar; .csv objdsc NOT jarred, manifest omitted).
-                   Unblocks bridges java_uno tests (acquire, java_remote).  
STILL
-                   ⬜: OOoRunnerLight, qa/complex, JunitTest_qadevOOo_unoapi,
-                   testdocs — need running-soffice/OfficeConnection fixture.
+                   java_uno_jar; manifest omitted).  Unblocks bridges java_uno
+                   tests (acquire, java_remote).
+                   objdsc/*.csv ARE NOW JARRED (2026-08-06) — a deliberate
+                   divergence from the ant jar target (whose <include> list 
omits
+                   .csv) and the thing that makes the whole unoapi CATEGORY
+                   runnable: APIDescGetter takes a description either from a
+                   -objdsc DIRECTORY or, absent that argument, from the 
CLASSPATH
+                   resource /objdsc/<module> (it has a JarURLConnection branch 
for
+                   exactly that), and the qa/unoapi Test.java adapters pass no
+                   -objdsc ⇒ against upstream's csv-less jar every unoapi suite
+                   dies at its first object with "couldn't find module".
+                   JunitTest_qadevOOo_unoapi GREEN 2026-08-06 (//main/qadevOOo:
+                   qa_unoapi, ~16s) via uno_junit_test fixture_starts_office — 
see
+                   the OFFICE SHUTDOWN DEADLOCK note in the test bucket.
+                   STILL ⬜: OOoRunnerLight, testdocs (needed by 11 of the 18
+                   unoapi adapters, which pass -tdoc).
 testgraphical ⬜  (graphical/visual regression tests; needs instsetoo_native + 
qadevOOo)
 
 ── Remaining: Java-based ────────────────────────────────────────────────
diff --git a/MODULE.bazel b/MODULE.bazel
index 3aa2f888c7..2161c1cc54 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -82,6 +82,23 @@ http_file(
     downloaded_file_path = "apache-rat-0.10.jar",
 )
 
+# JUnit 4 — the runner for the Java/UNO integration tests 
(//build/rules:junit_test.bzl).
+# Upstream never bundled it: configure takes --with-junit and the whole
+# JunitTest/javatest machinery is a no-op when OOO_JUNIT_JAR is empty, which 
is why
+# no junit jar exists in ext_sources.
+#
+# 4.10, not a later 4.x, and that is load-bearing: junit-4.10.jar EMBEDS
+# hamcrest-core 1.1, while 4.11+ split it out into a second jar.  Upstream's 
build
+# carries the same fork in every recipe (`.IF "$(HAMCREST_CORE_JAR)" != ""`);
+# pinning 4.10 means the tests need exactly one classpath entry and there is no
+# second jar to keep in sync.
+http_file(
+    name = "junit_jar",
+    url = "https://repo1.maven.org/maven2/junit/junit/4.10/junit-4.10.jar";,
+    sha256 = 
"36a747ca1e0b86f6ea88055b8723bb87030d627766da6288bf077afdeeb0f75a",
+    downloaded_file_path = "junit-4.10.jar",
+)
+
 register_toolchains("//build/toolchain:cc_toolchain_x86_vs2008_def")
 register_toolchains("//build/toolchain:cc_toolchain_x64_vs2008_def")
 
diff --git a/build/jre.bzl b/build/jre.bzl
index 05120073b9..e3bfa75434 100644
--- a/build/jre.bzl
+++ b/build/jre.bzl
@@ -1,12 +1,18 @@
-"""The JRE a *test* starts in-process, per target architecture.
+"""The JRE a *test* runs a JVM from, per target architecture.
 
-Any test that puts jvmfwk into DIRECT mode (UNO_JAVA_JFW_JREHOME) loads a
-jvm.dll into its OWN process, so the JRE has to match the TARGET arch, not the
-host's — a 32-bit default build cannot load the 64-bit JVM that $JAVA_HOME
-almost certainly points at, and one JAVA_HOME cannot be right for both arches
-anyway.  Hence a select(), and hence not the build JDK: @remotejdk21_win is the
-toolchain that COMPILES (and is x64-only), which is a different question from
-which JVM a test may load.
+The arch has to match the TARGET, not the host, and for two independent 
reasons:
+
+  * a test that puts jvmfwk into DIRECT mode (UNO_JAVA_JFW_JREHOME) loads a
+    jvm.dll into its OWN process — see jre_home_env();
+  * a JUnit test that drives an office (uno_junit_test) runs in a JVM that 
loads
+    jpipe.dll, the JNI half of the named-pipe UNO connection, out of the staged
+    program/ — see jre_java_exe().
+
+Either way a 32-bit default build cannot use the 64-bit JVM that $JAVA_HOME
+almost certainly points at, and one JAVA_HOME cannot be right for both arches.
+Hence a select(), and hence not the build JDK: @remotejdk21_win is the 
toolchain
+that COMPILES (and is x64-only), which is a different question from which JVM a
+test may run.
 
 This is machine-specific and the one genuinely unhermetic input in the test
 suite.  It lives here rather than in each BUILD file so that pointing the tree
@@ -14,16 +20,25 @@ at a different JDK is a single edit; //main/bridges' 
java_run_test still takes
 its own `jvm_path_dirs` (it needs the DIRECTORY containing jvm.dll for PATH,
 not a home URL), so that is a related but distinct knob.
 
-A URL, not a path, and mind two encodings:
-  * forward slashes — every rtl::Bootstrap value is macro-expanded, where a
-    backslash is an ESCAPE character, so "C:\\Program" comes back "C:Program";
-  * %20 for spaces — osl's file-URL parsing requires it.
+The native path is the source of truth and the URL is DERIVED from it, because
+the same JDK has to be spelled two ways and a hand-maintained pair drifts:
+  * a native path for a command line (`"<home>\\bin\\java.exe"`);
+  * a file:/// URL for a bootstrap variable, which needs forward slashes (every
+    rtl::Bootstrap value is macro-expanded, where a backslash is an ESCAPE
+    character, so "C:\\Program" comes back "C:Program") and %20 for spaces
+    (osl's file-URL parsing requires it).
 And note the launcher doubles '%' for cmd before substituting (gtest_test.bzl
 _expand_tokens); without that "%20" reaches the test as "0".
 """
 
-_JRE_X86 = 
"file:///C:/Program%20Files%20(x86)/Eclipse%20Adoptium/jdk-8.0.452.9-hotspot"
-_JRE_X64 = 
"file:///C:/Program%20Files/Eclipse%20Adoptium/jdk-8.0.452.9-hotspot"
+_JDK_X86 = "C:\\Program Files (x86)\\Eclipse Adoptium\\jdk-8.0.452.9-hotspot"
+_JDK_X64 = "C:\\Program Files\\Eclipse Adoptium\\jdk-8.0.452.9-hotspot"
+
+def _url(path):
+    return "file:///" + path.replace("\\", "/").replace(" ", "%20")
+
+_JRE_X86 = _url(_JDK_X86)
+_JRE_X64 = _url(_JDK_X64)
 
 def jre_home_env():
     """select() giving UNO_JAVA_JFW_JREHOME for the target arch.
@@ -44,3 +59,35 @@ def jre_home_env():
         "//build:arch_x64": {"UNO_JAVA_JFW_JREHOME": _JRE_X64},
         "//conditions:default": {"UNO_JAVA_JFW_JREHOME": _JRE_X86},
     })
+
+def jre_java_exe():
+    """select() giving the native path of java.exe for the target arch.
+
+    For uno_junit_test, which runs the JUnit suite in a JVM of its own (the
+    office it drives is a separate process, started by the test).  The arch has
+    to match the target anyway, because org.openoffice.test.OfficeConnection
+    connects over a NAMED PIPE and jurt's PipeConnection is JNI: the JVM loads
+    jpipe.dll out of the staged program/, and a 64-bit JVM cannot load the
+    32-bit one a default build produces.
+
+    JDK 8 also happens to be what these sources were written for — qadevOOo and
+    the qa/complex suites are Java 1.4/5-era code compiled here with --release 
8.
+    """
+    return select({
+        "//build:arch_x64": _JDK_X64 + "\\bin\\java.exe",
+        "//conditions:default": _JDK_X86 + "\\bin\\java.exe",
+    })
+
+def jre_home_native():
+    """select() giving the native path of the JRE home for the target arch.
+
+    Exported for JAVA_HOME, which is how the office under test picks its own 
JVM
+    once a test passes -env:UNO_JAVA_JFW_ENV_JREHOME=true (which
+    OfficeConnection always does).  Without it the office would take whatever
+    JAVA_HOME the developer's shell happens to hold — very likely the 64-bit 
JDK,
+    which a 32-bit office cannot load.
+    """
+    return select({
+        "//build:arch_x64": _JDK_X64,
+        "//conditions:default": _JDK_X86,
+    })
diff --git a/build/rules/gtest_test.bzl b/build/rules/gtest_test.bzl
index 4bab3b81a7..34a15f1b89 100644
--- a/build/rules/gtest_test.bzl
+++ b/build/rules/gtest_test.bzl
@@ -183,6 +183,15 @@ def _server_lines(server_exe, server_args, port):
         ":_portup",
     ]
 
+# ── shared with junit_test.bzl ───────────────────────────────────────────────
+# The Java/UNO tests emit a launcher .bat of their own and need exactly these 
two
+# primitives: a %~dp0-relative path to the staged install (bazel test's working
+# directory is neither the execroot nor the exe's directory), and the escaping
+# rules for a value baked into a .bat (the percent landmine above).  
Re-exported
+# rather than copied, so both launchers keep one definition of each.
+launcher_relpath = _windows_relpath
+launcher_expand_tokens = _expand_tokens
+
 def _staged_gtest_test_impl(ctx):
     # The staging dir is normally "<name>.run".  bin_layout makes it
     # "<name>.run/bin" instead — the ONE thing the child-process suites need.
diff --git a/build/rules/junit_test.bzl b/build/rules/junit_test.bzl
new file mode 100644
index 0000000000..eaf297802f
--- /dev/null
+++ b/build/rules/junit_test.bzl
@@ -0,0 +1,355 @@
+###############################################################################
+# uno_junit_test — a JUnit suite that drives a real office over UNO.
+#
+# The Java counterpart of gtest_test(office_connection = True).  Both implement
+# the SAME fixture — launch soffice with -accept=…;urp, resolve a component
+# context over it, terminate it afterwards — but from opposite sides:
+#
+#   C++   test::OfficeConnection      reads "arg-soffice" / "arg-user" through
+#         (main/test/source/cpp)      rtl::Bootstrap, i.e. the ENVIRONMENT
+#   Java  org.openoffice.test.        reads "org.openoffice.test.arg.soffice" /
+#         OfficeConnection            "…arg.user" through System.getProperty,
+#         (main/test/java)            i.e. -D on the java command line
+#
+# and they differ in transport: the C++ side uses a socket, the Java side a
+# NAMED PIPE (pipe,name=oootest<uuid>), which is why this rule needs no
+# port bookkeeping and no `exclusive` tag — concurrent runs cannot collide.
+#
+# The recipe ported here is solenv/inc/installationtest.mk's `javatest` rule
+# (identical in solenv/gbuild/JunitTest.mk), which is:
+#
+#     rm -rf <user> && mkdir -p <user>
+#     java -cp "<junit>;<classpath>" \
+#          -Dorg.openoffice.test.arg.soffice=path:<soffice> \
+#          -Dorg.openoffice.test.arg.user=file:///<user> \
+#          <-Dorg.openoffice.test.arg.testarg.*> \
+#          org.junit.runner.JUnitCore <classes>
+#     rm -rf <user>
+#
+# Usage:
+#   load("//build/rules:junit_test.bzl", "uno_junit_test")
+#   uno_junit_test(
+#       name = "qa_complex_junitskeleton",
+#       srcs = glob(["qa/complex/junitskeleton/*.java"]),
+#       classes = ["complex.junitskeleton.Skeleton"],
+#       deps = [":OOoRunner", "//main/test:test_jar", ...],
+#       data_tree = {"//main/qadevOOo:…README.txt": 
"test_documents/README.txt"},
+#       uno_install = "//main/staging:install",
+#   )
+###############################################################################
+
+"""JUnit suites that drive a staged office over UNO (see header above)."""
+
+load("@rules_java//java:defs.bzl", "java_library")
+load("@rules_java//java/common:java_info.bzl", "JavaInfo")
+load("//build:jre.bzl", "jre_home_native", "jre_java_exe")
+load(":gtest_test.bzl", "launcher_expand_tokens", "launcher_relpath")
+
+_JUNIT = "//build/third_party/junit:junit"
+_RUNNER = "org.junit.runner.JUnitCore"
+
+def _uno_junit_test_impl(ctx):
+    d = ctx.label.name + ".run"
+    staged = []
+
+    # ── classpath ───────────────────────────────────────────────────────────
+    # Every runtime jar staged flat beside the launcher, so the classpath is a
+    # list of names under one directory the launcher locates with %~dp0 — the
+    # same self-locating trick gtest_test uses, and for the same reason: `bazel
+    # test` sets the working directory to neither the execroot nor the staged
+    # dir, so nothing relative to %CD% resolves.
+    jars = depset(transitive = [
+        dep[JavaInfo].transitive_runtime_jars
+        for dep in ctx.attr.runtime_deps
+    ]).to_list()
+
+    cp_names = []
+    seen = {}
+    for j in jars:
+        name = j.basename
+        if name in seen:
+            if seen[name] == j.path:
+                continue
+
+            # Two different jars with one basename: Bazel names a 
java_library's
+            # output after the TARGET, and target names are only unique within 
a
+            # package.  Disambiguate rather than silently dropping one, which
+            # would surface as a NoClassDefFoundError far from the cause.
+            name = str(len(seen)) + "_" + name
+        seen[name] = j.path
+        o = ctx.actions.declare_file(d + "/" + name)
+        ctx.actions.symlink(output = o, target_file = j)
+        staged.append(o)
+        cp_names.append(name)
+
+    # ── fixture files ───────────────────────────────────────────────────────
+    # Staged at a chosen path, like gtest_test's data_tree: a test document is
+    # reached as new File("test_documents", name) relative to the working
+    # directory, so the SHAPE matters, not just co-location.
+    for label, rel in ctx.attr.data_tree.items():
+        files = label[DefaultInfo].files.to_list()
+        want = rel.split("/")[-1]
+        if len(files) > 1:
+            files = [f for f in files if f.basename == want]
+        if len(files) != 1:
+            fail(("data_tree entry %s → %s: expected one file, got %d. Give 
the " +
+                  "staged path the same basename as the artifact you want.") %
+                 (label.label, rel, len(files)))
+        o = ctx.actions.declare_file(d + "/" + rel)
+        ctx.actions.symlink(output = o, target_file = files[0])
+        staged.append(o)
+
+    # ── the staged office ───────────────────────────────────────────────────
+    uno_program_dir = None
+    for f in ctx.files.uno_install:
+        if f.path.endswith("/program/fundamental.ini"):
+            uno_program_dir = f.dirname
+            break
+    if uno_program_dir == None:
+        fail("uno_junit_test needs uno_install = //main/staging:install — the 
" +
+             "office the suite launches, and the jpipe.dll its JVM loads, both 
" +
+             "come from the staged program/.")
+
+    launcher = ctx.actions.declare_file(d + "/" + ctx.label.name + "_run.bat")
+    lines = [
+        "@echo off",
+        "setlocal",
+        'for %%I in ("%~dp0.") do set "_RUN=%%~fI"',
+        'set "_RUNU=%_RUN:\\=/%"',
+
+        # The per-run user installation.  javatest wipes it before AND after, 
so
+        # a run that died without cleaning up cannot hand its state to the next
+        # one — a stale user profile is exactly what makes an office failure
+        # unreproducible.
+        'set "_SCRATCH=%TEST_TMPDIR%\\scratch"',
+        'if "%TEST_TMPDIR%"=="" set "_SCRATCH=%_RUN%\\scratch"',
+        'set "_SCRATCH=%_SCRATCH:/=\\%"',
+        'if exist "%_SCRATCH%" rmdir /s /q "%_SCRATCH%"',
+        'mkdir "%_SCRATCH%" || exit /b 1',
+        'set "_SCRATCHU=%_SCRATCH:\\=/%"',
+        'for %%I in ("%~dp0' + launcher_relpath(launcher.dirname, 
uno_program_dir) +
+        '") do set "_PROG=%%~fI"',
+        'set "_PROGU=%_PROG:\\=/%"',
+
+        # jpipe.dll (the JNI half of jurt's named-pipe connection) is loaded by
+        # System.loadLibrary from the TEST's JVM, and java.library.path 
includes
+        # PATH on Windows.  The office itself needs nothing from PATH — it 
lives
+        # in program/, whose own directory the loader searches first.
+        'set "PATH=%_PROG%;%PATH%"',
+
+        # OfficeConnection always passes -env:UNO_JAVA_JFW_ENV_JREHOME=true to
+        # the office, which makes ITS jvmfwk take JAVA_HOME.  Pin it to the
+        # arch-matched JDK: inheriting the developer's shell value would hand a
+        # 32-bit office the 64-bit JDK it cannot load, and only for the tests
+        # that actually use office-side Java, i.e. intermittently.
+        'set "JAVA_HOME=' + ctx.attr.java_home + '"',
+
+        # Non-product builds route tools-library assertions to a MESSAGE BOX by
+        # default.  A modal dialog in a launched office is invisible under
+        # `bazel test` and hangs the run until the timeout kills it; javatest
+        # exports the same value for the same reason.
+        'set "DBGSV_ERROR_OUT=shell"',
+
+        # The working directory is the staged dir, not program/ (the C++ 
default):
+        # a Java suite reaches its fixture documents by relative path, and 
unlike
+        # the C++ side it needs nothing from program/ as a working directory.
+        'cd /d "%~dp0" || exit /b 1',
+    ]
+
+    if ctx.attr.fixture_starts_office:
+        # ── the office is the FIXTURE's, not the test's ──────────────────────
+        # `-Dorg...soffice=connect:<desc>` makes OfficeConnection ATTACH to an
+        # office someone else started (`path:` makes it launch one). Then
+        # `process` stays null, and its tearDown — which is what calls
+        # XDesktop.terminate() and blocks in Process.waitFor() until the office
+        # exits — becomes a no-op.
+        #
+        # That is the point. A suite that leaves the office in a state where it
+        # will not shut down otherwise hangs until the test timeout, with its 
own
+        # assertions long since green. Moving the office's lifetime into the
+        # launcher makes the verdict the JUnit result, and makes the shutdown a
+        # `taskkill`. It is the same division of labour as `server_args` in
+        # gtest_test (bridgetest_urp).
+        #
+        # It is a DEVIATION from installationtest.mk::javatest, and it costs 
the
+        # one check tearDown performs — that the office terminates cleanly and
+        # exits 0. So it is opt-in, not the default: use it only for a suite 
that
+        # cannot run without it, and only when the shutdown failure is 
understood
+        # and recorded. Suites that shut down cleanly must keep proving it.
+        #
+        # The pipe name has to be unique per run (two of these must not attach 
to
+        # each other's office) and has to be recoverable afterwards to kill the
+        # right process — %RANDOM% twice, then matched against the command 
line.
+        lines += [
+            'set "_PIPE=oootest%RANDOM%%RANDOM%"',
+            'start "" /b "%_PROG%\\soffice.exe" -quickstart=no 
-nofirststartwizard' +
+            ' -norestore "-accept=pipe,name=%_PIPE%;urp"' +
+            ' "-env:UserInstallation=file:///%_SCRATCHU%"' +
+            ' -env:UNO_JAVA_JFW_ENV_JREHOME=true',
+            # Readiness, the pipe analogue of gtest_test's server_ready_port.
+            # Not optional: OfficeConnection's resolve loop has NO sleep when
+            # `process` is null (it only waits on the process it STARTED), so a
+            # premature start would spin a core until the office came up.
+            #
+            # osl's Windows pipes appear in the pipe device namespace as
+            # \\.\pipe\OSL_PIPE_<SID>_<name>, so match the NAME at the end — 
the
+            # SID part is per-user.  The whole wait is ONE PowerShell process
+            # rather than a batch loop for two reasons: cmd cannot list that
+            # namespace at all (`dir \\.\pipe\` answers "invalid parameter" —
+            # it is a device path, not a directory), and re-launching 
PowerShell
+            # per iteration would cost more than the office's own startup.
+            'powershell -NoProfile -Command "$n = $env:_PIPE; for ($i = 0; ' +
+            "$i -lt 120; $i++) { if 
([System.IO.Directory]::GetFiles('\\\\.\\pipe\\') " +
+            "-match ('_' + $n + '$')) { exit 0 }; Start-Sleep -Milliseconds 
500 }; " +
+            'exit 1"',
+            "if not errorlevel 1 goto _pipeup",
+            'echo ERROR: office never accepted on pipe %_PIPE% 1>&2',
+            "exit /b 1",
+            ":_pipeup",
+        ]
+
+    # "path:" makes OfficeConnection LAUNCH an office; "connect:<desc>" 
attaches
+    # to the one the launcher started (see fixture_starts_office).  NOTE the
+    # connect form is NOT token-expanded: it names a launcher variable 
directly,
+    # and _expand_tokens doubles every literal '%' for cmd.
+    soffice_arg = ("connect:pipe,name=%_PIPE%" if 
ctx.attr.fixture_starts_office
+                   else launcher_expand_tokens("path:$(PROGRAM)\\soffice.exe"))
+    args = [
+        "-Dorg.openoffice.test.arg.soffice=" + soffice_arg,
+        # A file:/// URL, NOT the native path the C++ side takes: Java passes 
it
+        # straight to -env:UserInstallation=, which wants a URL.
+        "-Dorg.openoffice.test.arg.user=" +
+        launcher_expand_tokens("file:///$(SCRATCH_URL)"),
+    ]
+    args += ['-D%s=%s' % (k, launcher_expand_tokens(ctx.attr.test_args[k]))
+             for k in sorted(ctx.attr.test_args)]
+
+    cp = ";".join(["%_RUN%\\" + n for n in cp_names])
+    lines += [
+        '"%s" %s -cp "%s" %s %s %s %%*' % (
+            ctx.attr.java_exe,
+            " ".join(ctx.attr.jvm_flags),
+            cp,
+            " ".join(['"%s"' % a for a in args]),
+            _RUNNER,
+            " ".join(ctx.attr.classes),
+        ),
+        # JUnitCore's main() exits 1 on any failure, so the verdict needs no
+        # wrapper.  Capture it before the cleanup clobbers ERRORLEVEL.
+        'set "_RC=%ERRORLEVEL%"',
+    ]
+
+    if ctx.attr.fixture_starts_office:
+        # Kill OUR office and nothing else.  `taskkill /im soffice.exe` would
+        # take down a developer's session and any concurrent suite; the unique
+        # pipe name is on the command line, so match on that.  Ignore failure —
+        # a suite whose tearDown DID manage to terminate leaves nothing to 
kill.
+        #
+        # No nested double quotes anywhere in the PowerShell: cmd would eat 
them.
+        # The pipe name is read from the ENVIRONMENT ($env:_PIPE), which is 
also
+        # why nothing here needs %-escaping.
+        lines += [
+            'powershell -NoProfile -Command "Get-CimInstance Win32_Process | ' 
+
+            "Where-Object { $_.Name -eq 'soffice.exe' -and $_.CommandLine 
-like " +
+            "('*' + $env:_PIPE + '*') } | ForEach-Object { Stop-Process -Id " +
+            '$_.ProcessId -Force }" >nul 2>nul',
+        ]
+
+    lines += [
+        'rmdir /s /q "%_SCRATCH%" 2>nul',
+        "exit /b %_RC%",
+        "",
+    ]
+
+    ctx.actions.write(output = launcher, content = "\r\n".join(lines), 
is_executable = True)
+    staged.append(launcher)
+
+    return [DefaultInfo(
+        executable = launcher,
+        runfiles = ctx.runfiles(files = staged + ctx.files.uno_install),
+        files = depset([launcher]),
+    )]
+
+_uno_junit_test = rule(
+    implementation = _uno_junit_test_impl,
+    test = True,
+    attrs = {
+        "runtime_deps": attr.label_list(providers = [[JavaInfo]]),
+        "classes": attr.string_list(mandatory = True),
+        "test_args": attr.string_dict(),
+        "jvm_flags": attr.string_list(),
+        "uno_install": attr.label(allow_files = True, mandatory = True),
+        "data_tree": attr.label_keyed_string_dict(allow_files = True),
+        "java_exe": attr.string(mandatory = True),
+        "java_home": attr.string(mandatory = True),
+        "fixture_starts_office": attr.bool(default = False),
+    },
+)
+
+def uno_junit_test(
+        name,
+        srcs,
+        classes,
+        deps = [],
+        uno_install = "//main/staging:install",
+        data_tree = {},
+        test_args = {},
+        jvm_flags = [],
+        fixture_starts_office = False,
+        javacopts = ["--release", "8", "-XepDisableAllChecks"],
+        size = "medium",
+        **kwargs):
+    """A JUnit suite run against a freshly launched office.
+
+    srcs / deps / javacopts: compiled into one java_library. JUnit itself is
+    added automatically — every one of these suites uses org.junit.Assert, and
+    org.openoffice.test.OfficeConnection does too. javacopts defaults to the
+    legacy-source settings the rest of the tree uses (these are Java 1.4/5-era
+    sources; modern javac compiles them with warnings only).
+
+    classes: fully-qualified test classes handed to JUnitCore — upstream's
+    JAVATESTFILES, i.e. only the files that carry an @Test.
+
+    test_args: extra "org.openoffice.test.arg.*" system properties, e.g. the
+    `sce`/`xcl` pair the qadevOOo unoapi runner takes. Values expand $(RUNDIR),
+    $(SCRATCH), $(PROGRAM) and the _URL form of each, exactly as in gtest_test 
—
+    which is how a value can name a staged fixture file.
+
+    data_tree: {label: "relative/staged/path"} fixture files. The test runs 
with
+    its working directory set to the staged dir, so a suite that does
+    new File("test_documents", …) finds them.
+
+    fixture_starts_office: the LAUNCHER starts and kills the office, and the
+    test attaches to it (`connect:`) instead of launching it (`path:`). Use 
this
+    only for a suite that leaves the office unable to shut down, which 
otherwise
+    hangs in OfficeConnection.tearDown()'s Process.waitFor() until the test
+    timeout with all its own assertions already green. It costs the one check
+    tearDown performs — that the office terminates cleanly and exits 0 — so it
+    is opt-in and each use must record WHY the shutdown fails.
+
+    size defaults to "medium" (300s): this fixture BOOTS AN OFFICE, always cold
+    because the user installation is recreated per run, and OfficeConnection's
+    resolve loop is unbounded — the timeout is the only thing that ends a run
+    where the office never comes up. Do not drop it to "small".
+    """
+    java_library(
+        name = name + "_lib",
+        srcs = srcs,
+        deps = deps + [_JUNIT],
+        javacopts = javacopts,
+        testonly = True,
+    )
+    _uno_junit_test(
+        name = name,
+        runtime_deps = [":" + name + "_lib"],
+        classes = classes,
+        test_args = test_args,
+        jvm_flags = jvm_flags,
+        fixture_starts_office = fixture_starts_office,
+        uno_install = uno_install,
+        data_tree = data_tree,
+        java_exe = jre_java_exe(),
+        java_home = jre_home_native(),
+        size = size,
+        **kwargs
+    )
diff --git a/build/third_party/junit/BUILD.bazel 
b/build/third_party/junit/BUILD.bazel
new file mode 100644
index 0000000000..bcc5b2d608
--- /dev/null
+++ b/build/third_party/junit/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@rules_java//java:defs.bzl", "java_import")
+
+# JUnit 4.10 — the test framework the Java/UNO integration suites are written
+# against (org.junit.Test / org.junit.Assert, run by 
org.junit.runner.JUnitCore).
+#
+# Upstream calls this OOO_JUNIT_JAR, supplied by `configure --with-junit`; it 
is
+# not in ext_sources because nothing in AOO ever built without it being 
optional.
+# 4.10 embeds hamcrest-core, so this single entry is the whole classpath
+# contribution — see the MODULE.bazel comment on @junit_jar.
+#
+# testonly: this is a test dependency, and nothing shipped may link it.
+java_import(
+    name = "junit",
+    jars = ["@junit_jar//file"],
+    testonly = True,
+    visibility = ["//visibility:public"],
+)
diff --git a/main/external/msvcp90/BUILD.bazel 
b/main/external/msvcp90/BUILD.bazel
index 592dd8e753..afba55a9e7 100644
--- a/main/external/msvcp90/BUILD.bazel
+++ b/main/external/msvcp90/BUILD.bazel
@@ -83,6 +83,34 @@ genrule(
     visibility = ["//visibility:public"],
 )
 
+# ── Embedded manifest for a DLL loaded by a FOREIGN process ──────────────────
+# Same idea as vc90_app_manifest_res, but at RT_MANIFEST id 2 (the DLL slot).
+# Needed by a /MD DLL that gets loaded into a process this build does not
+# produce — jurt's jpipe.dll, which a stock java.exe loads via
+# System.loadLibrary().  See vc90_dll_manifest.rc for the full reasoning.
+filegroup(
+    name = "vc90_dll_manifest_rc",
+    srcs = select({
+        "//build:arch_x64": ["vc90_dll_manifest_amd64.rc"],
+        "//conditions:default": ["vc90_dll_manifest.rc"],
+    }),
+)
+
+genrule(
+    name = "vc90_dll_manifest_res",
+    srcs = [
+        ":vc90_dll_manifest_rc",
+        ":vc90_app_manifest",
+    ],
+    outs = ["vc90_dll_manifest.res"],
+    cmd_bat = (
+        "set \"_RC=C:\\Program Files\\Microsoft 
SDKs\\Windows\\v7.0\\Bin\\RC.Exe\"&&" +
+        "\"!_RC!\" /nologo /I \"main/external/msvcp90\"" +
+        " /fo \"$(OUTS)\" \"$(location :vc90_dll_manifest_rc)\""
+    ),
+    visibility = ["//visibility:public"],
+)
+
 # ── Debug CRT (/MDd, --compilation_mode=dbg) ─────────────────────────────────
 # The debug analog of crt_dlls.  Under --compilation_mode=dbg the toolchain
 # links /MDd, so binaries import MSVCR90D.dll / MSVCP90D.dll / MSVCM90D.dll,
diff --git a/main/external/msvcp90/vc90_dll_manifest.rc 
b/main/external/msvcp90/vc90_dll_manifest.rc
new file mode 100644
index 0000000000..c7e323db9f
--- /dev/null
+++ b/main/external/msvcp90/vc90_dll_manifest.rc
@@ -0,0 +1,25 @@
+// Embed the VC90-CRT dependency manifest as RT_MANIFEST (type 24), resource 
id 2
+// (ISOLATIONAWARE_MANIFEST_RESOURCE_ID — the DLL manifest slot).
+//
+// The exe form (id 1, vc90_app_manifest.rc) covers a process we launch
+// ourselves.  This one is for a DLL loaded into a process we do NOT control:
+// jurt's jpipe.dll is loaded by System.loadLibrary() from a stock java.exe,
+// whose image carries no VC90 dependency at all, so nothing in that process
+// supplies an activation context.  Without a manifest of its own the loader
+// resolves MSVCR90.dll by plain search and reports "Can't find dependent
+// libraries" (or, if a loose copy is on PATH, R6034 — an unmanifested CRT 
load).
+// With it, the loader builds an activation context from the DLL's own manifest
+// while binding its imports and finds the SxS assembly.
+//
+// This is what upstream's link step does for every DLL (solenv embeds the
+// generated manifest with mt.exe); this tree links /MANIFEST:NO throughout,
+// which is invisible inside soffice.exe — its own manifest covers the whole
+// process — and only surfaces for a DLL a FOREIGN host loads.
+//
+// Same manifest XML as the exe form, deliberately: the assembly identity and
+// version are the part that must not drift, and the trustInfo element it also
+// carries is simply ignored in a DLL manifest.
+//
+// x86 variant: the manifest sits at the package root.  See
+// vc90_dll_manifest_amd64.rc for the x64 one.
+2 24 "vc90_app.manifest"
diff --git a/main/external/msvcp90/vc90_dll_manifest_amd64.rc 
b/main/external/msvcp90/vc90_dll_manifest_amd64.rc
new file mode 100644
index 0000000000..0130e9febb
--- /dev/null
+++ b/main/external/msvcp90/vc90_dll_manifest_amd64.rc
@@ -0,0 +1,6 @@
+// x64 variant of vc90_dll_manifest.rc — see there for why a DLL needs its own
+// embedded manifest (resource id 2, ISOLATIONAWARE_MANIFEST_RESOURCE_ID).
+//
+// Both .rc files are compiled with /I main/external/msvcp90, so the path here
+// is relative to the package root.
+2 24 "amd64/vc90_app.manifest"
diff --git a/main/jurt/BUILD.bazel b/main/jurt/BUILD.bazel
index 74b15e20f1..f268c74bb1 100644
--- a/main/jurt/BUILD.bazel
+++ b/main/jurt/BUILD.bazel
@@ -47,6 +47,18 @@ uno_jar(
     visibility = ["//visibility:public"],
 )
 
+# ── the VC90-CRT manifest, EMBEDDED (both pipe DLLs) ─────────────────────────
+# These two are the only DLLs in the tree loaded into a process this build does
+# not produce: a stock java.exe, via System.loadLibrary("jpipe").  That process
+# has no VC90 dependency of its own, so nothing supplies an activation context
+# and the loader cannot resolve their MSVCR90.dll import — System.loadLibrary
+# reports "Can't find dependent libraries", and putting a loose msvcr90.dll on
+# PATH would only trade that for R6034.  Embedding the manifest at RT_MANIFEST
+# id 2 makes each DLL carry its own context.  Everything else here is loaded
+# into soffice.exe (or a staged test exe), whose own manifest already covers 
the
+# whole process.  See //main/external/msvcp90:vc90_dll_manifest_res.
+_DLL_MANIFEST_RES = "//main/external/msvcp90:vc90_dll_manifest_res"
+
 # ── jpipx.dll 
─────────────────────────────────────────────────────────────────
 # Real JNI pipe implementation.  On Windows, functions are exported with
 # truncated names (PipeConnection_create etc., __cdecl) so wrapper.c can call
@@ -63,9 +75,13 @@ cc_binary(
         "//main/sal:sal_headers",
         "@rules_java//toolchains:jni",
     ],
-    additional_linker_inputs = ["//main/sal:sal_implib"],
+    additional_linker_inputs = [
+        "//main/sal:sal_implib",
+        _DLL_MANIFEST_RES,
+    ],
     linkopts = [
         "$(execpath //main/sal:sal_implib)",
+        "$(execpath %s)" % _DLL_MANIFEST_RES,
         "/MANIFEST:NO",
     ],
     visibility = ["//visibility:public"],
@@ -88,6 +104,10 @@ cc_binary(
         "//main/sal:sal_headers",
         "@rules_java//toolchains:jni",
     ],
-    linkopts = ["/MANIFEST:NO"],
+    additional_linker_inputs = [_DLL_MANIFEST_RES],
+    linkopts = [
+        "$(execpath %s)" % _DLL_MANIFEST_RES,
+        "/MANIFEST:NO",
+    ],
     visibility = ["//visibility:public"],
 )
diff --git a/main/qadevOOo/BUILD.bazel b/main/qadevOOo/BUILD.bazel
index 10a758af9e..6dc8f1ee4a 100644
--- a/main/qadevOOo/BUILD.bazel
+++ b/main/qadevOOo/BUILD.bazel
@@ -1,4 +1,5 @@
 load("@rules_java//java:defs.bzl", "java_library")
+load("//build/rules:junit_test.bzl", "uno_junit_test")
 
 # ── OOoRunner.jar ────────────────────────────────────────────────────────────
 # The qadevOOo test framework (lib.*, base.*, util.*, share.*, mod.* …) used 
as a
@@ -7,10 +8,20 @@ load("@rules_java//java:defs.bzl", "java_library")
 #
 # Classpath (build.xml main.classpath): ridl + unoil + jurt + juh + java_uno.
 #
-# Resources: src/main/resources holds ONLY the 670 objdsc/*.csv 
object-description
-# files, which the ant jar target does NOT bundle (.csv is absent from its 
include
-# list) — the runner reads them from the filesystem via an -objdsc parameter. 
So
-# this jar is compiled classes only, matching upstream.
+# Resources: src/main/resources holds ONLY the objdsc/*.csv object-description
+# files — one per UNO implementation, listing the interfaces and properties the
+# UNOAPI runner is to check on it.
+#
+# DIVERGENCE (deliberate, and it FIXES a suite): they are bundled here, where
+# upstream's ant jar target drops them (its <include> list covers .class,
+# .properties, .xml, … but not .csv).  helper/APIDescGetter loads a description
+# in one of two ways — from a directory named by the -objdsc parameter, or, 
when
+# that parameter is absent, as the CLASSPATH RESOURCE "/objdsc/<module>", with 
an
+# explicit JarURLConnection branch for the jar case.  The qa/unoapi suites'
+# Test.java passes no -objdsc, so the classpath route is the only one they 
have,
+# and against upstream's csv-less jar every one of them fails at the first 
object
+# with "couldn't find module '<module>'".  Bundling costs a few hundred KB in a
+# test-only jar and is what makes those suites runnable at all.
 #
 # Manifest (Class-Path: ridl.jar unoil.jar / RegistrationClassName:
 # org.openoffice.RunnerService) is a deployment/UNO-registration detail not 
needed
@@ -18,6 +29,8 @@ load("@rules_java//java:defs.bzl", "java_library")
 java_library(
     name = "OOoRunner",
     srcs = glob(["java/OOoRunner/src/main/java/**/*.java"]),
+    resources = glob(["java/OOoRunner/src/main/resources/**"]),
+    resource_strip_prefix = "main/qadevOOo/java/OOoRunner/src/main/resources",
     # Legacy Java 1.4-era sources: pin --release 8 and silence Error Prone, 
same
     # as jurt (modern JDK compiles them with deprecation/raw-type warnings 
only).
     javacopts = ["--release", "8", "-XepDisableAllChecks"],
@@ -30,3 +43,90 @@ java_library(
     ],
     visibility = ["//visibility:public"],
 )
+
+# ── qa/complex/junitskeleton ─────────────────────────────────────────────────
+# Upstream's own worked example of a qa/complex suite, and therefore the right
+# first consumer of uno_junit_test: it exercises the whole Java-side fixture in
+# one go — connect to a launched office, get its XMultiServiceFactory, build a
+# qadevOOo TestParameters over it, resolve a fixture document by relative path,
+# LOAD that document into the office, close it, and ask the office for its temp
+# directory.  Nothing here is skeleton-specific; anything that fails, fails for
+# every later suite too.
+#
+# JAVATESTFILES in the makefile lists only Skeleton.java (the file carrying the
+# @Test methods) while JAVAFILES adds justatest + TestDocument, which is 
exactly
+# the split between `classes` and `srcs` here.
+#
+# The document is README.txt reached as new File("test_documents", …), i.e.
+# relative to the working directory — which uno_junit_test sets to the staged
+# dir, hence data_tree rather than a flat copy.
+# ── qa/unoapi ────────────────────────────────────────────────────────────────
+# JunitTest_qadevOOo_unoapi: the runner testing ITSELF.  Its scenario names one
+# object, qadevOOo.SelfTest, whose interface "tests" are canned passes and
+# failures, so it checks that the UNOAPI machinery reports both correctly 
rather
+# than checking any office component — the guard on the framework every other
+# unoapi suite is built out of.  Unlike svtools' adapter it takes no -tdoc.
+#
+# fixture_starts_office, and this is the suite that motivated it.  With the
+# faithful `path:` fixture the assertions all pass — scenario green in 3 s,
+# "0 of 1 tests failed" — and then the run hangs until the 300 s timeout, with
+# nothing printed after the job.  Diagnosed rather than guessed:
+#
+#   * the main thread is blocked in Process.waitFor() (OfficeConnection:126),
+#     i.e. XDesktop.terminate() has already returned and the office has not
+#     exited;
+#   * the office is still alive, idle, with NO visible window, and does not
+#     answer a fresh UNO connection;
+#   * a non-invasive cdb stack dump shows the office's MAIN thread inside a
+#     WinProc dispatch that entered a NESTED VCL message wait
+#     (Application::Execute → DispatchMessageW → … → GetMessageW), so it holds
+#     the SolarMutex, while two URP threads sit in vos::OMutex::acquire trying
+#     to get it — one an incoming binaryurp request into fwk's LockHelper, the
+#     other an sw proxy release coming back through msci_uno.
+#
+# That is an office SHUTDOWN DEADLOCK between the solar mutex and in-flight
+# bridge calls, not a fixture defect: the three suites that hold no stale
+# proxies at teardown terminate cleanly.  Fixing it is a source change in the
+# office, so the fixture takes ownership of the office instead — the 
assertions,
+# which are what this target is for, then run and report.  What is given up is
+# tearDown's check that the office exits 0, which for this suite is exactly the
+# thing that is broken and would otherwise cost 300 s per run to re-observe.
+uno_junit_test(
+    name = "qa_unoapi",
+    srcs = ["qa/unoapi/Test.java"],
+    classes = ["org.openoffice.qadevOOo.qa.unoapi.Test"],
+    fixture_starts_office = True,
+    data_tree = {
+        "qa/unoapi/qadevOOo.sce": "qadevOOo.sce",
+        "qa/unoapi/knownissues.xcl": "knownissues.xcl",
+    },
+    test_args = {
+        "org.openoffice.test.arg.sce": "$(RUNDIR)\\qadevOOo.sce",
+        "org.openoffice.test.arg.xcl": "$(RUNDIR)\\knownissues.xcl",
+    },
+    deps = [
+        ":OOoRunner",
+        "//main/test:test_jar",
+        "//main/ridljar:ridl",
+        "//main/unoil:unoil",
+        "//main/jurt:jurt",
+        "//main/javaunohelper:juh_jar",
+    ],
+)
+
+uno_junit_test(
+    name = "qa_complex_junitskeleton",
+    srcs = glob(["qa/complex/junitskeleton/*.java"]),
+    classes = ["complex.junitskeleton.Skeleton"],
+    data_tree = {
+        "qa/complex/junitskeleton/test_documents/README.txt": 
"test_documents/README.txt",
+    },
+    deps = [
+        ":OOoRunner",
+        "//main/test:test_jar",
+        "//main/ridljar:ridl",
+        "//main/unoil:unoil",
+        "//main/jurt:jurt",
+        "//main/javaunohelper:juh_jar",
+    ],
+)
diff --git a/main/staging/BUILD.bazel b/main/staging/BUILD.bazel
index 6dafb75f84..9b8d85768f 100644
--- a/main/staging/BUILD.bazel
+++ b/main/staging/BUILD.bazel
@@ -92,6 +92,18 @@ collect_outputs(
         # (java_uno.jar) needs the program/classes/ classpath staging, which 
does
         # not exist yet — see readme.md "jni_uno" notes.
         "//main/bridges:java_uno",
+        # jpipe.dll + jpipx.dll — the JNI half of jurt's NAMED-PIPE connection
+        # (com.sun.star.lib.connections.pipe.PipeConnection does
+        # System.loadLibrary("jpipe"), and jpipe's DllMain then loads jpipx
+        # beside it).  Nothing in the office links either, and the office never
+        # loads them itself — it is the JAVA CLIENT PROCESS that does, which is
+        # why they were missing without breaking anything visible.  Any 
external
+        # Java program connecting to a running office over a pipe needs them
+        # (upstream ships both in the URE lib dir); here it is the qa/complex
+        # suites, whose org.openoffice.test.OfficeConnection resolves
+        # "uno:pipe,name=…;urp;StarOffice.ComponentContext".
+        "//main/jurt:jpipe",
+        "//main/jurt:jpipx",
         # com.sun.star.loader.Java2, in two halves: javavm starts/locates the
         # JVM through jvmfwk, javaloader is the loader that builds a class
         # loader over a component's jar.  Registered in services.rdb by
diff --git a/main/svl/BUILD.bazel b/main/svl/BUILD.bazel
index 92ef5712a2..3864952686 100644
--- a/main/svl/BUILD.bazel
+++ b/main/svl/BUILD.bazel
@@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"])
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
 load("//build/rules:rsc_pipeline.bzl", "rsc_res")
 load("//build/rules:gtest_test.bzl", "gtest_test")
+load("//build/rules:junit_test.bzl", "uno_junit_test")
 
 _DEFINES = [
     "WNT", "GUI", "WIN32",
@@ -261,5 +262,31 @@ gtest_test(
     uno_install = "//main/staging:install",
 )
 
+# ── qa/complex/passwordcontainer ─────────────────────────────────────────────
+# The Java half of svl's test coverage, and the first suite here that tests a
+# real SERVICE rather than the fixture: com.sun.star.task.PasswordContainer as
+# the office exposes it over UNO — store/retrieve per-URL credentials, with and
+# without a master password, persistent and session-only (Test01/02/03), driven
+# through an XInteractionHandler the test supplies itself 
(MasterPasswdHandler).
+#
+# It is a UNO-level test by nature and has no C++ equivalent: the service lives
+# behind the office's configuration and its persistence layer, so it can only 
be
+# checked against a running office.  Upstream's makefile.mk lists
+# PasswordContainerUnitTest.java as the only JAVATESTFILES entry, with the
+# other five files as plain JAVAFILES — the `classes` / `srcs` split here.
+uno_junit_test(
+    name = "qa_complex_passwordcontainer",
+    srcs = glob(["qa/complex/passwordcontainer/*.java"]),
+    classes = ["complex.passwordcontainer.PasswordContainerUnitTest"],
+    deps = [
+        "//main/qadevOOo:OOoRunner",
+        "//main/test:test_jar",
+        "//main/ridljar:ridl",
+        "//main/unoil:unoil",
+        "//main/jurt:jurt",
+        "//main/javaunohelper:juh_jar",
+    ],
+)
+
 exports_files(glob(["**/*.component"]))
 
diff --git a/main/svtools/BUILD.bazel b/main/svtools/BUILD.bazel
index 0dce111fa1..377b44e1ca 100644
--- a/main/svtools/BUILD.bazel
+++ b/main/svtools/BUILD.bazel
@@ -2,6 +2,7 @@ package(default_visibility = ["//visibility:public"])
 
 load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
 load("//build/rules:rsc_pipeline.bzl", "rsc_res")
+load("//build/rules:junit_test.bzl", "uno_junit_test")
 
 _DEFINES = [
     "WNT", "GUI", "WIN32",
@@ -223,5 +224,48 @@ rsc_res(
     visibility  = ["//visibility:public"],
 )
 
+# ── qa/unoapi ────────────────────────────────────────────────────────────────
+# The other kind of Java suite: not a hand-written JUnit test but the qadevOOo
+# UNOAPI runner.  Test.java is a three-line adapter — connect, then hand
+# org.openoffice.Runner a SCENARIO file and the connection description; the
+# Runner instantiates each named object in the running office and checks every
+# interface and property it declares against the UNO type description.
+#
+# What it actually runs is whatever svtools.sce lists, and all but ONE of its
+# entries are commented out with the issue number that broke them (#i110988,
+# #i88276, …), so this suite is exactly one object today:
+# svtools.AccessibleTabBar.  knownissues.xcl is the second half of that
+# mechanism — a list of individual interface tests to skip.  Both are staged
+# rather than passed as source paths, because the arguments are read at run 
time
+# and bazel test's working directory is neither the source tree nor the 
execroot.
+uno_junit_test(
+    name = "qa_unoapi",
+    srcs = ["qa/unoapi/Test.java"],
+    classes = ["org.openoffice.svtools.qa.unoapi.Test"],
+    data_tree = {
+        "qa/unoapi/svtools.sce": "svtools.sce",
+        "qa/unoapi/knownissues.xcl": "knownissues.xcl",
+    },
+    test_args = {
+        "org.openoffice.test.arg.sce": "$(RUNDIR)\\svtools.sce",
+        "org.openoffice.test.arg.xcl": "$(RUNDIR)\\knownissues.xcl",
+        # The runner's test-document root.  AccessibleTabBar builds its 
document
+        # through SOfficeFactory and loads nothing from disk, so no qadevOOo
+        # testdocs tree is staged here; a suite whose .sce names objects that 
DO
+        # open a fixture document will need one.  Upstream leaves this argument
+        # unset entirely (the makefile defines no DEFS), which is why its own
+        # Test.java would pass null.
+        "org.openoffice.test.arg.tdoc": "$(RUNDIR)",
+    },
+    deps = [
+        "//main/qadevOOo:OOoRunner",
+        "//main/test:test_jar",
+        "//main/ridljar:ridl",
+        "//main/unoil:unoil",
+        "//main/jurt:jurt",
+        "//main/javaunohelper:juh_jar",
+    ],
+)
+
 exports_files(glob(["**/*.component"]))
 
diff --git a/main/test/BUILD.bazel b/main/test/BUILD.bazel
index a91f345a8e..11fc8657b4 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("@rules_java//java:defs.bzl", "java_library")
 load("//build/rules:gtest_test.bzl", "gtest_test")
 
 # main/test — libtest, AOO's C++ unit-test support library (test.dll).
@@ -89,6 +90,34 @@ filegroup(
     visibility = ["//visibility:public"],
 )
 
+# ── test.jar — the JAVA half of the same fixture ──────────────────────
+# org.openoffice.test.OfficeConnection: launch an office with -accept=…;urp,
+# resolve a component context over it, terminate it in tearDown.  Exactly what
+# the C++ test::OfficeConnection above does, for the Java qa/complex and
+# qa/unoapi suites — see //build/rules:junit_test.bzl for how the two differ.
+#
+# Ant_test.mk builds this as test.jar with no manifest; the name here follows
+# juh_jar, i.e. "_jar" marks the Java artifact where a C++ target already owns
+# the plain name.
+#
+# Not in this jar: test/java/test-tools (org.openoffice.test.tools.*, a 
document
+# helper layer built as a SECOND jar by Ant_test-tools.mk).  Add it as its own
+# target when a suite that needs it is wired.
+java_library(
+    name = "test_jar",
+    srcs = glob(["java/test/src/main/java/org/openoffice/test/*.java"]),
+    javacopts = ["--release", "8", "-XepDisableAllChecks"],
+    deps = [
+        "//main/ridljar:ridl",
+        "//main/jurt:jurt",
+        "//main/javaunohelper:juh_jar",
+        "//main/unoil:unoil",
+        "//build/third_party/junit:junit",
+    ],
+    testonly = True,
+    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
diff --git a/main/test/readme.md b/main/test/readme.md
index fd69423c74..30f251e679 100644
--- a/main/test/readme.md
+++ b/main/test/readme.md
@@ -30,7 +30,7 @@ holdouts).  This brings the test layer onto Bazel so suites 
run under
 | `@gtest` | `ext_libraries/modules/gtest/1.7.0/` | GoogleTest 1.7.0 bzlmod 
wrap (zip cached in `ext_sources`). Built with `/Zc:wchar_t-` so its `wchar_t` 
ABI matches `sal_Unicode` test code. |
 | `gtest_test` rule | 
[//build/rules:gtest_test.bzl](../../build/rules/gtest_test.bzl) | Reusable 
runnable-test rule. The `/MD` toolchain embeds no manifest, so a bare `cc_test` 
exe can't launch (DLLs land in runfiles subdirs; loose CRT → R6034). This 
stages the exe + runtime DLLs + VC90 CRT + an external `<exe>.manifest` into 
ONE flat dir (the test analog of `//main/idl:svidl_bundle`). Pass `data_files` 
for fixture inputs the test opens by relative path — see "working directory" 
below. |
 | `sal_process_init` | 
[//build/testsupport](../../build/testsupport/BUILD.bazel) | Migration-authored 
TU that runs `sal_detail_initialize()` from a dynamic initialiser, for suites 
whose own `main()` skips the `SAL_IMPLEMENT_MAIN` boilerplate (⇒ no 
`WSAStartup`). |
-| `libtest` | [//main/test:test](BUILD.bazel) | `test.dll` — 
`test::OfficeConnection` + arg/url helpers, for *subsequent* (UNO) tests that 
bootstrap a running soffice over URP. Built; not yet exercised. |
+| `libtest` | [//main/test:test](BUILD.bazel) | `test.dll` — 
`test::OfficeConnection` + arg/url helpers, for *subsequent* (UNO) tests that 
bootstrap a running soffice over URP. Exercised by `:test_qa_officeconnection`; 
its Java twin is `:test_jar` (see "The JAVA side of the same fixture" below). |
 | `vc90_app_manifest_res` | 
[//main/external/msvcp90](../external/msvcp90/BUILD.bazel) | The VC90-CRT 
manifest compiled to a `.res` and linked into every `gtest_test` exe at 
`RT_MANIFEST` id 1. See "the CRT activation context" below. |
 | `sal_qa_test` macro | [//main/sal:sal_qa.bzl](../sal/sal_qa.bzl) | Thin 
`gtest_test` wrapper for the sal/qa suites (common copts/deps + per-dir 
`*_Const.h` include). |
 
@@ -343,6 +343,140 @@ holdouts).  This brings the test layer onto Bazel so 
suites run under
   won't build at all).  Don't be fooled by the half-fix: `/FIwindows.h` +
   advapi32 satisfies its Win32 SID/registry calls but not the testshl2 include.
 
+## The JAVA side of the same fixture — `uno_junit_test`
+
+Everything above is C++. The `qa/` directories also hold a large body of 
**Java**
+suites — `qa/complex/*` (hand-written JUnit) and `qa/unoapi` (the qadevOOo
+UNOAPI runner) — which drive a real office over UNO. They are the *only* form 
of
+coverage for anything that exists solely as a UNO service, and they are wired
+through [//build/rules:junit_test.bzl](../../build/rules/junit_test.bzl).
+
+| Piece | Path | Role |
+| ----- | ---- | ---- |
+| `@junit_jar` | `MODULE.bazel` → 
[//build/third_party/junit](../../build/third_party/junit/BUILD.bazel) | JUnit 
**4.10** from Maven Central — upstream's `OOO_JUNIT_JAR`, never bundled in the 
tree. 4.10 and not later: it *embeds* hamcrest-core, so there is no second jar 
to keep in sync (upstream forks on `HAMCREST_CORE_JAR` in every recipe). |
+| `test.jar` | [//main/test:test_jar](BUILD.bazel) | 
`org.openoffice.test.OfficeConnection` — the Java twin of 
`test::OfficeConnection`. |
+| `OOoRunner.jar` | [//main/qadevOOo:OOoRunner](../qadevOOo/BUILD.bazel) | The 
UNOAPI framework, **now carrying its `objdsc/*.csv` resources** — see below. |
+| `uno_junit_test` | 
[//build/rules:junit_test.bzl](../../build/rules/junit_test.bzl) | 
`java_library` + a launcher `.bat` mirroring `installationtest.mk::javatest`. |
+
+### The two OfficeConnections are the same fixture, spelled differently
+
+| | C++ (`gtest_test(office_connection=True)`) | Java (`uno_junit_test`) |
+| --- | --- | --- |
+| arguments | `arg-soffice` / `arg-user` via `rtl::Bootstrap` → the 
**environment** | `org.openoffice.test.arg.soffice` / `.user` via 
`System.getProperty` → **`-D` on the command line** |
+| user installation | a **native path** (fed to `getFileURLFromSystemPath`) | 
a **`file:///` URL** (fed to `-env:UserInstallation=`) |
+| transport | socket | **named pipe** (`pipe,name=oootest<uuid>`) |
+| consequence | — | no port to reserve, so no `exclusive` tag: these suites 
run concurrently |
+
+The recipe ported is `solenv/inc/installationtest.mk`'s `javatest` (identical 
in
+`solenv/gbuild/JunitTest.mk`), including its wipe of the user installation
+*before* as well as after.
+
+### Which JVM, and why it is not the build JDK
+
+`//build:jre.bzl` now also exports `jre_java_exe()` / `jre_home_native()`. The
+test JVM has to match the **target** arch even though the office is a separate
+process, because `OfficeConnection` connects over a named pipe and jurt's
+`PipeConnection` is JNI: the JVM loads `jpipe.dll` out of the staged 
`program/`,
+and a 64-bit JVM cannot load the 32-bit one a default build produces. 
`JAVA_HOME`
+is pinned to the same JDK, because `OfficeConnection` always passes
+`-env:UNO_JAVA_JFW_ENV_JREHOME=true`, which is what the *office's* jvmfwk 
reads.
+
+### Three things were missing from the product, not from the test
+
+Each of these was invisible until a foreign process loaded our DLLs:
+
+1. **`jpipe.dll` / `jpipx.dll` were never staged.** Nothing in the office links
+   or loads them — it is the *client* JVM that does — so their absence broke
+   nothing visible. Any external Java program talking to a running office over 
a
+   pipe needs them; upstream ships both in the URE lib dir. Now in
+   [//main/staging](../staging/BUILD.bazel).
+2. **A /MD DLL loaded by a process we do not build needs its OWN embedded
+   manifest.** This tree links `/MANIFEST:NO` throughout, which is invisible
+   inside `soffice.exe` (its manifest covers the whole process) and fatal for a
+   DLL a stock `java.exe` loads: nothing supplies an activation context, the
+   `MSVCR90.dll` import cannot be resolved, and `System.loadLibrary` reports 
the
+   uninformative **"Can't find dependent libraries"**. Putting a loose
+   `msvcr90.dll` on `PATH` only trades that for R6034. The fix is
+   `//main/external/msvcp90:vc90_dll_manifest_res` — the same manifest at
+   `RT_MANIFEST` **id 2** (the DLL slot; id 1 is the exe slot) — linked into 
both
+   pipe DLLs. Upstream gets this for free: solenv embeds a manifest in every
+   DLL.
+3. **`OOoRunner.jar` had no object descriptions.** `helper/APIDescGetter` finds
+   a description either under a `-objdsc` directory or, when that argument is
+   absent, as the classpath resource `/objdsc/<module>` (it has an explicit
+   `JarURLConnection` branch for exactly that). The `qa/unoapi` `Test.java`
+   adapters pass no `-objdsc`, and upstream's Ant `jar` target does not include
+   `*.csv` — so against upstream's jar every unoapi suite dies at its first
+   object with `couldn't find module '<module>'`. Bundling them is a deliberate
+   divergence, and it is what makes the whole unoapi category runnable.
+
+### Green
+
+| Target | What it covers | Time |
+| --- | --- | --- |
+| `//main/qadevOOo:qa_complex_junitskeleton` | upstream's worked example, and 
therefore the whole fixture: connect → `XMultiServiceFactory` → qadevOOo 
`TestParameters` → resolve a fixture document by relative path → **load it into 
the office** → close → ask the office for its temp dir (3 tests) | ~14 s |
+| `//main/svl:qa_complex_passwordcontainer` | 
`com.sun.star.task.PasswordContainer` over UNO: per-URL credentials with and 
without a master password, persistent and session-only, driven through the 
test's own `XInteractionHandler` (3 tests) | ~13 s |
+| `//main/svtools:qa_unoapi` | the first UNOAPI suite ever run here — 26 
interface/property checks on `svtools.AccessibleTabBar` | ~25 s |
+| `//main/qadevOOo:qa_unoapi` | the runner testing itself 
(`qadevOOo.SelfTest`) — the guard on the framework the other unoapi suites are 
built out of. Needs `fixture_starts_office`, see below | ~16 s |
+
+### When the office will not shut down: `fixture_starts_office`
+
+`qadevOOo:qa_unoapi` is the suite that motivated this. With the faithful
+fixture its assertions all pass — scenario green in 3 s — and then the run 
hangs
+until the 300 s timeout with nothing printed after the job. Diagnosed rather
+than guessed:
+
+- the JVM's main thread is in `Process.waitFor()` (`OfficeConnection:126`), so
+  `XDesktop.terminate()` has already returned and the office has not exited;
+- the office is still alive, idle, has **no visible window**, and no longer
+  answers a fresh UNO connection;
+- a non-invasive `cdb -pv` stack dump shows the office's **main thread inside a
+  WinProc dispatch that entered a nested VCL message wait**
+  (`Application::Execute` → `DispatchMessageW` → … → `GetMessageW`) — so it
+  holds the SolarMutex — while **two URP threads block in
+  `vos::OMutex::acquire`**: an incoming `binaryurp` request into fwk's
+  `LockHelper`, and an `sw` proxy release coming back through `msci_uno`.
+
+That is an office **shutdown deadlock** between the solar mutex and in-flight
+bridge calls, not a fixture defect — the suites that hold no stale proxies at
+teardown terminate cleanly. Fixing it is a source change in the office.
+
+`fixture_starts_office = True` makes the **launcher** start and kill the office
+and the test attach to it (`connect:` instead of `path:`), which leaves
+`OfficeConnection.tearDown()` a no-op — `process` is null, so it neither
+terminates nor waits. The verdict becomes the JUnit result, and the shutdown
+becomes a `taskkill`. Same division of labour as `server_args` in `gtest_test`.
+
+Two mechanics worth knowing before reusing it:
+
+- **the readiness wait is one PowerShell process, not a batch loop.** cmd 
cannot
+  list the pipe namespace at all (`dir \\.\pipe\` answers "invalid parameter" —
+  it is a device path), and osl's pipes appear as
+  `\\.\pipe\OSL_PIPE_<SID>_<name>`, so the probe matches the name at the end.
+  It is not optional: `OfficeConnection`'s resolve loop has **no sleep** when 
it
+  did not start the process itself, so it would spin a core.
+- **the kill matches the unique pipe name on the command line**, never
+  `taskkill /im soffice.exe` — which would take down a developer's session and
+  any concurrently running suite. All four suites run in parallel today.
+
+It is **opt-in and costs something**: `tearDown`'s check that the office
+terminates cleanly and exits 0. Use it only where that check is the thing that
+is broken, and record why.
+
+### The rest of the category, and what each needs
+
+There are **18** `qa/unoapi` and **24** `qa/complex` directories in the tree, 
so
+this is a queue, not a leftover. Two things gate the remainder:
+
+- **`-tdoc`** — 11 of the 18 unoapi adapters pass a test-document root, which
+  means staging `qadevOOo/testdocs` (53 entries, some of them directories). The
+  rule's `data_tree` stages one *file* per entry; a directory-preserving
+  variant is the next piece of rule work. `svtools`' scenario builds its
+  document through `SOfficeFactory` and needs none, which is why it went first.
+- **per-module fixtures** — `svl/qa/complex/ConfigItems` needs a C++ helper
+  component built alongside; `writerfilter/qa/complex` and several others have
+  no `makefile.mk` at all upstream, i.e. they were never wired there either.
+
 ## The sal suite is deliberately NOT a green gate
 
 `//main/sal:sal_tests` runs **every** migrated self-contained sal/qa test — 48

Reply via email to