xref: /trunk/test/aoo-test/tests/harness/test_install.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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