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