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