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
