1*2cb6d6f1SJim Jagielski################################################################ 2*2cb6d6f1SJim Jagielski# 3*2cb6d6f1SJim Jagielski# Licensed to the Apache Software Foundation (ASF) under one 4*2cb6d6f1SJim Jagielski# or more contributor license agreements. See the NOTICE file 5*2cb6d6f1SJim Jagielski# distributed with this work for additional information 6*2cb6d6f1SJim Jagielski# regarding copyright ownership. The ASF licenses this file 7*2cb6d6f1SJim Jagielski# to you under the Apache License, Version 2.0 (the 8*2cb6d6f1SJim Jagielski# "License"); you may not use this file except in compliance 9*2cb6d6f1SJim Jagielski# with the License. You may obtain a copy of the License at 10*2cb6d6f1SJim Jagielski# 11*2cb6d6f1SJim Jagielski# http://www.apache.org/licenses/LICENSE-2.0 12*2cb6d6f1SJim Jagielski# 13*2cb6d6f1SJim Jagielski# Unless required by applicable law or agreed to in writing, 14*2cb6d6f1SJim Jagielski# software distributed under the License is distributed on an 15*2cb6d6f1SJim Jagielski# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*2cb6d6f1SJim Jagielski# KIND, either express or implied. See the License for the 17*2cb6d6f1SJim Jagielski# specific language governing permissions and limitations 18*2cb6d6f1SJim Jagielski# under the License. 19*2cb6d6f1SJim Jagielski# 20*2cb6d6f1SJim Jagielski################################################################ 21*2cb6d6f1SJim Jagielski 22*2cb6d6f1SJim Jagielski"""install.py against fake macOS, Linux and Windows trees, and the UNO Python search with a faked probe.""" 23*2cb6d6f1SJim Jagielskiimport os 24*2cb6d6f1SJim Jagielskiimport subprocess 25*2cb6d6f1SJim Jagielskiimport sys 26*2cb6d6f1SJim Jagielski 27*2cb6d6f1SJim Jagielskiimport pytest 28*2cb6d6f1SJim Jagielski 29*2cb6d6f1SJim Jagielskifrom aootest import install 30*2cb6d6f1SJim Jagielski 31*2cb6d6f1SJim Jagielskipytestmark = pytest.mark.selftest 32*2cb6d6f1SJim Jagielski 33*2cb6d6f1SJim JagielskiFORMS = { 34*2cb6d6f1SJim Jagielski "macos": [lambda a: a, lambda a: a / "Contents", lambda a: a / "Contents" / "MacOS" / "soffice", lambda a: a / "Contents" / "program", 35*2cb6d6f1SJim Jagielski lambda a: a / "Contents" / "program" / "soffice"], 36*2cb6d6f1SJim Jagielski "posix": [lambda r: r, lambda r: r / "program", lambda r: r / "program" / "soffice", lambda r: r / "program" / "soffice.bin"], 37*2cb6d6f1SJim Jagielski "windows": [lambda r: r, lambda r: r / "program", lambda r: r / "program" / "soffice.exe"], 38*2cb6d6f1SJim Jagielski} 39*2cb6d6f1SJim JagielskiCASES = [pytest.param(kind, i, id="%s-form%d" % (kind, i)) for kind, forms in FORMS.items() for i in range(len(forms))] 40*2cb6d6f1SJim Jagielski 41*2cb6d6f1SJim Jagielski 42*2cb6d6f1SJim Jagielski@pytest.mark.parametrize("kind, form", CASES) 43*2cb6d6f1SJim Jagielskidef test_every_way_of_pointing_at_an_install_finds_the_same_layout(fake, kind, form): 44*2cb6d6f1SJim Jagielski top = fake(kind) 45*2cb6d6f1SJim Jagielski lay = install.find_layout(FORMS[kind][form](top)) 46*2cb6d6f1SJim Jagielski assert lay.platform == kind 47*2cb6d6f1SJim Jagielski assert lay.version.base == "5.1" and lay.version.package == "5.1.0" and lay.version.tuple == (5, 1) 48*2cb6d6f1SJim Jagielski assert lay.program.name == "program" and lay.program.parent.name in ("Contents", top.name) 49*2cb6d6f1SJim Jagielski assert lay.version_file.name == ("version.ini" if kind == "windows" else "versionrc") 50*2cb6d6f1SJim Jagielski assert lay.fundamental.name == ("fundamental.ini" if kind == "windows" else "fundamentalrc") 51*2cb6d6f1SJim Jagielski assert lay.soffice.name == ("soffice.exe" if kind == "windows" else "soffice") 52*2cb6d6f1SJim Jagielski assert (lay.soffice.parent.name == "MacOS") == (kind == "macos") 53*2cb6d6f1SJim Jagielski assert lay.unopkg is not None and lay.unopkg.name.startswith("unopkg") 54*2cb6d6f1SJim Jagielski assert lay.pyuno_module.name == ("pyuno.pyd" if kind == "windows" else "pyuno.so") 55*2cb6d6f1SJim Jagielski 56*2cb6d6f1SJim Jagielski 57*2cb6d6f1SJim Jagielskidef test_windows_prefers_the_console_unopkg(fake): 58*2cb6d6f1SJim Jagielski assert install.find_layout(fake("windows")).unopkg.name == "unopkg.com" 59*2cb6d6f1SJim Jagielski 60*2cb6d6f1SJim Jagielski 61*2cb6d6f1SJim Jagielskidef test_version_from_productmajor_alone(fake): 62*2cb6d6f1SJim Jagielski lay = install.find_layout(fake("macos", with_base=False, major="510")) 63*2cb6d6f1SJim Jagielski assert (lay.version.base, lay.version.tuple) == ("5.1", (5, 1)) 64*2cb6d6f1SJim Jagielski 65*2cb6d6f1SJim Jagielski 66*2cb6d6f1SJim Jagielskidef test_newer_major_is_accepted(fake): 67*2cb6d6f1SJim Jagielski assert install.find_layout(fake("posix", base="6.0", pkg="6.0.0", major="600", minor="0")).version.tuple == (6, 0) 68*2cb6d6f1SJim Jagielski 69*2cb6d6f1SJim Jagielski 70*2cb6d6f1SJim Jagielski@pytest.mark.parametrize("kind", ["macos", "posix", "windows"]) 71*2cb6d6f1SJim Jagielskidef test_aoo_4_is_refused(fake, kind): 72*2cb6d6f1SJim Jagielski with pytest.raises(install.UnsupportedVersion, match="5.0 or later"): 73*2cb6d6f1SJim Jagielski install.find_layout(fake(kind, base="4.1", pkg="4.1.17", major="410", minor="1")) 74*2cb6d6f1SJim Jagielski 75*2cb6d6f1SJim Jagielski 76*2cb6d6f1SJim Jagielskidef test_not_an_install(tmp_path): 77*2cb6d6f1SJim Jagielski (tmp_path / "program").mkdir() 78*2cb6d6f1SJim Jagielski with pytest.raises(install.LayoutError, match="not an OpenOffice install"): 79*2cb6d6f1SJim Jagielski install.find_layout(tmp_path) 80*2cb6d6f1SJim Jagielski with pytest.raises(install.LayoutError, match="no such file"): 81*2cb6d6f1SJim Jagielski install.find_layout(tmp_path / "missing") 82*2cb6d6f1SJim Jagielski 83*2cb6d6f1SJim Jagielski 84*2cb6d6f1SJim Jagielskidef test_version_file_without_a_version(fake): 85*2cb6d6f1SJim Jagielski top = fake("posix") 86*2cb6d6f1SJim Jagielski (top / "program" / "versionrc").write_text("[Version]\nProductSource=AOO510\n") 87*2cb6d6f1SJim Jagielski with pytest.raises(install.LayoutError, match="no OOOBaseVersion"): 88*2cb6d6f1SJim Jagielski install.find_layout(top) 89*2cb6d6f1SJim Jagielski 90*2cb6d6f1SJim Jagielski 91*2cb6d6f1SJim Jagielskidef test_missing_launcher(fake): 92*2cb6d6f1SJim Jagielski top = fake("posix") 93*2cb6d6f1SJim Jagielski for n in ("soffice", "soffice.bin"): 94*2cb6d6f1SJim Jagielski (top / "program" / n).unlink() 95*2cb6d6f1SJim Jagielski with pytest.raises(install.LayoutError, match="no soffice launcher"): 96*2cb6d6f1SJim Jagielski install.find_layout(top) 97*2cb6d6f1SJim Jagielski 98*2cb6d6f1SJim Jagielski 99*2cb6d6f1SJim Jagielskidef test_discover_prefers_the_location_then_the_environment_then_the_default_places(fake, monkeypatch, tmp_path): 100*2cb6d6f1SJim Jagielski five = fake("posix") 101*2cb6d6f1SJim Jagielski assert install.discover(str(five)).version.tuple == (5, 1) 102*2cb6d6f1SJim Jagielski assert install.discover(None, {"AOO_OFFICE": str(five)}).location == five.resolve() 103*2cb6d6f1SJim Jagielski four = tmp_path / "openoffice4" 104*2cb6d6f1SJim Jagielski (four / "program").mkdir(parents=True) 105*2cb6d6f1SJim Jagielski (four / "program" / "soffice").write_text("x") 106*2cb6d6f1SJim Jagielski (four / "program" / "versionrc").write_text("[Version]\nOOOBaseVersion=4.1\n") 107*2cb6d6f1SJim Jagielski monkeypatch.setattr(install, "default_locations", lambda environ=None: [str(four), str(five), str(tmp_path / "nothing")]) 108*2cb6d6f1SJim Jagielski assert install.discover(None, {}).location == five.resolve() # the 4.x one is skipped, the missing one ignored 109*2cb6d6f1SJim Jagielski 110*2cb6d6f1SJim Jagielski 111*2cb6d6f1SJim Jagielskidef test_discover_with_nothing_found(monkeypatch): 112*2cb6d6f1SJim Jagielski monkeypatch.setattr(install, "default_locations", lambda environ=None: []) 113*2cb6d6f1SJim Jagielski with pytest.raises(install.LayoutError, match="give the location"): 114*2cb6d6f1SJim Jagielski install.discover(None, {}) 115*2cb6d6f1SJim Jagielski 116*2cb6d6f1SJim Jagielski 117*2cb6d6f1SJim Jagielski# ---- parsing what libpyuno links --------------------------------------------------------------------------------- 118*2cb6d6f1SJim Jagielski 119*2cb6d6f1SJim JagielskiOTOOL = """/Applications/OpenOffice5.app/Contents/program/libpyuno.dylib: 120*2cb6d6f1SJim Jagielski\t@_______OOO/libpyuno.dylib (compatibility version 0.0.0, current version 0.0.0) 121*2cb6d6f1SJim Jagielski\t@loader_path/libuno_cppu.dylib (compatibility version 0.0.0, current version 0.0.0) 122*2cb6d6f1SJim Jagielski\t/opt/local/Library/Frameworks/Python.framework/Versions/3.14/Python (compatibility version 3.14.0, current version 3.14.0) 123*2cb6d6f1SJim Jagielski\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0) 124*2cb6d6f1SJim Jagielski""" 125*2cb6d6f1SJim JagielskiLDD = "\tlinux-vdso.so.1 (0x0000)\n\tlibpython3.11.so.1.0 => /usr/lib/libpython3.11.so.1.0 (0x00007f)\n\tlibc.so.6 => /lib/libc.so.6 (0x0000)\n" 126*2cb6d6f1SJim Jagielski 127*2cb6d6f1SJim Jagielski 128*2cb6d6f1SJim Jagielskidef test_parse_otool_and_ldd(): 129*2cb6d6f1SJim Jagielski assert install.parse_otool(OTOOL) == "/opt/local/Library/Frameworks/Python.framework/Versions/3.14/Python" 130*2cb6d6f1SJim Jagielski assert install.parse_otool("\t/usr/lib/libSystem.B.dylib (x)\n") is None 131*2cb6d6f1SJim Jagielski assert install.parse_ldd(LDD) == "libpython3.11.so.1.0" 132*2cb6d6f1SJim Jagielski assert install.parse_ldd("libc.so.6 => /lib/libc.so.6\n") is None 133*2cb6d6f1SJim Jagielski 134*2cb6d6f1SJim Jagielski 135*2cb6d6f1SJim Jagielskidef test_interpreter_for_a_framework_path(tmp_path): 136*2cb6d6f1SJim Jagielski fw = tmp_path / "Library/Frameworks/Python.framework/Versions/3.14" 137*2cb6d6f1SJim Jagielski (fw / "bin").mkdir(parents=True) 138*2cb6d6f1SJim Jagielski linked = str(fw / "Python") 139*2cb6d6f1SJim Jagielski assert install.interpreter_for(linked) is None # no interpreter there 140*2cb6d6f1SJim Jagielski (fw / "bin" / "python3").write_text("x") 141*2cb6d6f1SJim Jagielski assert install.interpreter_for(linked) == str(fw / "bin" / "python3") 142*2cb6d6f1SJim Jagielski (fw / "bin" / "python3.14").write_text("x") 143*2cb6d6f1SJim Jagielski assert install.interpreter_for(linked) == str(fw / "bin" / "python3.14") # the exact version wins 144*2cb6d6f1SJim Jagielski assert install.interpreter_for("libpython3.11.so.1.0", which=lambda n: "/usr/bin/" + n) == "/usr/bin/python3.11" 145*2cb6d6f1SJim Jagielski assert install.interpreter_for(None) is None 146*2cb6d6f1SJim Jagielski 147*2cb6d6f1SJim Jagielski 148*2cb6d6f1SJim Jagielski# ---- the search ---------------------------------------------------------------------------------------------------- 149*2cb6d6f1SJim Jagielski 150*2cb6d6f1SJim Jagielskidef searcher(top, passing, linked=None, environ=None): 151*2cb6d6f1SJim Jagielski """(layout, asked, find): find() runs find_uno_python with a probe that passes only for `passing` and logs each question.""" 152*2cb6d6f1SJim Jagielski lay = install.find_layout(top) 153*2cb6d6f1SJim Jagielski asked = [] 154*2cb6d6f1SJim Jagielski 155*2cb6d6f1SJim Jagielski def prober(python, layout): 156*2cb6d6f1SJim Jagielski asked.append(python) 157*2cb6d6f1SJim Jagielski return (True, "3.14.0") if python in passing else (False, "No module named 'uno'") 158*2cb6d6f1SJim Jagielski 159*2cb6d6f1SJim Jagielski def find(**kw): 160*2cb6d6f1SJim Jagielski return install.find_uno_python(lay, prober=prober, linked=lambda l: linked, which=lambda n: None, environ=environ or {}, **kw) 161*2cb6d6f1SJim Jagielski return lay, asked, find 162*2cb6d6f1SJim Jagielski 163*2cb6d6f1SJim Jagielski 164*2cb6d6f1SJim Jagielskidef test_bundled_python_is_tried_before_the_system_ones(fake): 165*2cb6d6f1SJim Jagielski top = fake("posix") 166*2cb6d6f1SJim Jagielski (top / "program" / "python").write_text("x") 167*2cb6d6f1SJim Jagielski py = str(top / "program" / "python") 168*2cb6d6f1SJim Jagielski lay, asked, find = searcher(top, {py, sys.executable}) 169*2cb6d6f1SJim Jagielski res = find() 170*2cb6d6f1SJim Jagielski assert res.path == py and res.source == "bundled program/python" and asked == [py] 171*2cb6d6f1SJim Jagielski 172*2cb6d6f1SJim Jagielski 173*2cb6d6f1SJim Jagielskidef test_falls_through_to_the_interpreter_libpyuno_links(fake, tmp_path): 174*2cb6d6f1SJim Jagielski top = fake("macos") 175*2cb6d6f1SJim Jagielski fw = tmp_path / "fw/Python.framework/Versions/3.14" 176*2cb6d6f1SJim Jagielski (fw / "bin").mkdir(parents=True) 177*2cb6d6f1SJim Jagielski (fw / "bin" / "python3.14").write_text("x") 178*2cb6d6f1SJim Jagielski lay, asked, find = searcher(top, {str(fw / "bin" / "python3.14")}, linked=str(fw / "Python")) 179*2cb6d6f1SJim Jagielski res = find() 180*2cb6d6f1SJim Jagielski assert res.path == str(fw / "bin" / "python3.14") and res.source.startswith("linked by libpyuno") 181*2cb6d6f1SJim Jagielski assert asked[0] == res.path # nothing bundled, so the linked one is first 182*2cb6d6f1SJim Jagielski 183*2cb6d6f1SJim Jagielski 184*2cb6d6f1SJim Jagielskidef test_explicit_python_is_the_only_candidate(fake): 185*2cb6d6f1SJim Jagielski top = fake("posix") 186*2cb6d6f1SJim Jagielski (top / "program" / "python").write_text("x") 187*2cb6d6f1SJim Jagielski lay, asked, find = searcher(top, {str(top / "program" / "python")}) 188*2cb6d6f1SJim Jagielski with pytest.raises(install.NoUnoPython) as e: 189*2cb6d6f1SJim Jagielski find(explicit="/nonexistent/python") 190*2cb6d6f1SJim Jagielski assert asked == ["/nonexistent/python"] and e.value.tried[0][1] == "explicit" 191*2cb6d6f1SJim Jagielski 192*2cb6d6f1SJim Jagielski 193*2cb6d6f1SJim Jagielskidef test_environment_override_is_explicit_too(fake): 194*2cb6d6f1SJim Jagielski lay, asked, find = searcher(fake("posix"), {"/my/python"}, environ={"AOO_UNO_PYTHON": "/my/python"}) 195*2cb6d6f1SJim Jagielski res = find() 196*2cb6d6f1SJim Jagielski assert res.path == "/my/python" and res.source == "explicit" and asked == ["/my/python"] 197*2cb6d6f1SJim Jagielski 198*2cb6d6f1SJim Jagielski 199*2cb6d6f1SJim Jagielskidef test_failure_names_the_python_pyuno_needs(fake): 200*2cb6d6f1SJim Jagielski lay, asked, find = searcher(fake("macos"), set(), linked="/nonexistent/Library/Frameworks/Python.framework/Versions/3.14/Python") 201*2cb6d6f1SJim Jagielski with pytest.raises(install.NoUnoPython) as e: 202*2cb6d6f1SJim Jagielski find() 203*2cb6d6f1SJim Jagielski assert "/nonexistent/Library/Frameworks/Python.framework/Versions/3.14/Python (not found on this machine)" in str(e.value) 204*2cb6d6f1SJim Jagielski assert e.value.tried and all(r == "No module named 'uno'" for _, _, r in e.value.tried) 205*2cb6d6f1SJim Jagielski 206*2cb6d6f1SJim Jagielski 207*2cb6d6f1SJim Jagielskidef test_result_is_cached_per_install_and_pyuno_build(fake, tmp_path): 208*2cb6d6f1SJim Jagielski cache = tmp_path / "uno-python.json" 209*2cb6d6f1SJim Jagielski lay, asked, find = searcher(fake("posix"), {sys.executable}) 210*2cb6d6f1SJim Jagielski assert find(cache_file=cache).path == sys.executable and len(asked) == 1 211*2cb6d6f1SJim Jagielski again = find(cache_file=cache) 212*2cb6d6f1SJim Jagielski assert again.source.endswith("(cached)") and len(asked) == 1 # no second probe 213*2cb6d6f1SJim Jagielski os.utime(lay.pyuno_module, (1, 1)) # a different pyuno build 214*2cb6d6f1SJim Jagielski find(cache_file=cache) 215*2cb6d6f1SJim Jagielski assert len(asked) == 2 216*2cb6d6f1SJim Jagielski cache.write_text("not json") 217*2cb6d6f1SJim Jagielski assert find(cache_file=cache).path == sys.executable # a damaged cache is ignored 218*2cb6d6f1SJim Jagielski 219*2cb6d6f1SJim Jagielski 220*2cb6d6f1SJim Jagielskidef test_cache_is_not_used_for_an_explicit_choice(fake, tmp_path): 221*2cb6d6f1SJim Jagielski cache = tmp_path / "c.json" 222*2cb6d6f1SJim Jagielski lay, asked, find = searcher(fake("posix"), {sys.executable, "/other/python"}) 223*2cb6d6f1SJim Jagielski find(cache_file=cache) 224*2cb6d6f1SJim Jagielski n = len(asked) 225*2cb6d6f1SJim Jagielski assert find(cache_file=cache, explicit="/other/python").path == "/other/python" and len(asked) == n + 1 226*2cb6d6f1SJim Jagielski 227*2cb6d6f1SJim Jagielski 228*2cb6d6f1SJim Jagielski# ---- the probe itself ---------------------------------------------------------------------------------------------- 229*2cb6d6f1SJim Jagielski 230*2cb6d6f1SJim Jagielskidef runner(returncode=0, stdout="3.14.0\n", stderr="", exc=None): 231*2cb6d6f1SJim Jagielski seen = {} 232*2cb6d6f1SJim Jagielski 233*2cb6d6f1SJim Jagielski def run(cmd, **kw): 234*2cb6d6f1SJim Jagielski seen.update(cmd=cmd, env=kw["env"]) 235*2cb6d6f1SJim Jagielski if exc: 236*2cb6d6f1SJim Jagielski raise exc 237*2cb6d6f1SJim Jagielski return subprocess.CompletedProcess(cmd, returncode, stdout, stderr) 238*2cb6d6f1SJim Jagielski run.seen = seen 239*2cb6d6f1SJim Jagielski return run 240*2cb6d6f1SJim Jagielski 241*2cb6d6f1SJim Jagielski 242*2cb6d6f1SJim Jagielski@pytest.mark.parametrize("kind, var", [("macos", "DYLD_FALLBACK_LIBRARY_PATH"), ("posix", "LD_LIBRARY_PATH"), ("windows", "PATH")]) 243*2cb6d6f1SJim Jagielskidef test_probe_environment(fake, kind, var): 244*2cb6d6f1SJim Jagielski lay = install.find_layout(fake(kind)) 245*2cb6d6f1SJim Jagielski run = runner() 246*2cb6d6f1SJim Jagielski assert install.probe("/p/python", lay, runner=run) == (True, "3.14.0") 247*2cb6d6f1SJim Jagielski env = run.seen["env"] 248*2cb6d6f1SJim Jagielski assert env["URE_BOOTSTRAP"] == "vnd.sun.star.pathname:" + str(lay.fundamental) 249*2cb6d6f1SJim Jagielski assert env["PYTHONPATH"].split(os.pathsep)[0] == str(lay.program) 250*2cb6d6f1SJim Jagielski assert env[var].split(os.pathsep)[0] == str(lay.program) 251*2cb6d6f1SJim Jagielski assert run.seen["cmd"][:2] == ["/p/python", "-c"] and "import sys, uno, pyuno" in run.seen["cmd"][2] 252*2cb6d6f1SJim Jagielski 253*2cb6d6f1SJim Jagielski 254*2cb6d6f1SJim Jagielskidef test_probe_failures(fake): 255*2cb6d6f1SJim Jagielski lay = install.find_layout(fake("posix")) 256*2cb6d6f1SJim Jagielski assert install.probe("p", lay, runner=runner(1, "", "Traceback\nImportError: no uno\n")) == (False, "ImportError: no uno") 257*2cb6d6f1SJim Jagielski assert install.probe("p", lay, runner=runner(1, "", ""))[1] == "exit 1" 258*2cb6d6f1SJim Jagielski assert "older than 3.11" in install.probe("p", lay, runner=runner(stdout="3.9.2\n"))[1] 259*2cb6d6f1SJim Jagielski assert "unexpected output" in install.probe("p", lay, runner=runner(stdout="hello\n"))[1] 260*2cb6d6f1SJim Jagielski assert "could not run" in install.probe("p", lay, runner=runner(exc=FileNotFoundError("p")))[1] 261*2cb6d6f1SJim Jagielski assert "could not run" in install.probe("p", lay, runner=runner(exc=subprocess.TimeoutExpired("p", 1)))[1] 262