Coverage for tests/unit_tests/common/test_subprocess_env.py: 92%
85 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""
10Tests of subprocess_env.py, in particular the LD_LIBRARY_PATH fix for the
11Linux pyinstaller bundle. See https://github.com/FPGAwars/apio/issues/887
12"""
14import os
15from pathlib import Path
16import pytest
17from tests.conftest import ApioRunner
18from apio.common.apio_console import cunstyle
19from apio.common.apio_platforms import get_apio_platforms
20from apio.common.proto.stubs.apio_common_pb2 import EnvMutations, NameValue
21from apio.common.subprocess_env import (
22 apply_env_mutations,
23 get_env_mutations_for_subprocess,
24 show_env_mutations,
25)
26from apio.common.proto.stubs.apio_config_pb2 import ApioPlatformEnvSpec
27from apio.apio_context import (
28 ApioContext,
29 PackagesPolicy,
30 ProjectPolicy,
31 RemoteConfigPolicy,
32)
34# -- ApioPlatform instances.
35LINUX = get_apio_platforms()["linux-x86-64"]
36DARWIN = get_apio_platforms()["darwin-arm64"]
37WINDOWS = get_apio_platforms()["windows-amd64"]
40def test_fix_flag_only_for_pyinstaller_on_linux():
41 """The fix is requested only for a pyinstaller app on Linux."""
43 empty_env_spec = ApioPlatformEnvSpec()
45 mutations = get_env_mutations_for_subprocess(LINUX, True, empty_env_spec)
46 assert mutations.pyinstaller_linux_fix
48 mutations = get_env_mutations_for_subprocess(LINUX, False, empty_env_spec)
49 assert not mutations.pyinstaller_linux_fix
51 mutations = get_env_mutations_for_subprocess(DARWIN, True, empty_env_spec)
52 assert not mutations.pyinstaller_linux_fix
54 mutations = get_env_mutations_for_subprocess(DARWIN, False, empty_env_spec)
55 assert not mutations.pyinstaller_linux_fix
58def test_fix_unsets_ld_library_path_when_it_was_not_set_originally():
59 """No LD_LIBRARY_PATH_ORIG means that the user had no LD_LIBRARY_PATH and
60 the one in the env is the bundle's. The subprocess must not see it,
61 otherwise the system's 'sh' loads the bundled libreadline."""
63 env = {
64 "PATH": "/usr/bin",
65 "LD_LIBRARY_PATH": "/home/me/.apio/bin/_internal",
66 }
67 apply_env_mutations(EnvMutations(pyinstaller_linux_fix=True), env)
68 assert env == {"PATH": "/usr/bin"}
71def test_fix_restores_the_original_ld_library_path():
72 """A user's LD_LIBRARY_PATH, saved by pyinstaller in LD_LIBRARY_PATH_ORIG,
73 is restored and the helper var is hidden from the subprocess."""
75 env = {
76 "PATH": "/usr/bin",
77 "LD_LIBRARY_PATH": "/home/me/.apio/bin/_internal:/opt/user/lib",
78 "LD_LIBRARY_PATH_ORIG": "/opt/user/lib",
79 }
80 apply_env_mutations(EnvMutations(pyinstaller_linux_fix=True), env)
81 assert env == {"PATH": "/usr/bin", "LD_LIBRARY_PATH": "/opt/user/lib"}
84def test_no_fix_leaves_ld_library_path_alone():
85 """When the fix is not requested (python package, macOS, windows) the
86 LD_LIBRARY_PATH of the user is passed through untouched."""
88 env = {"PATH": "/usr/bin", "LD_LIBRARY_PATH": "/opt/user/lib"}
89 apply_env_mutations(EnvMutations(pyinstaller_linux_fix=False), env)
90 assert env == {"PATH": "/usr/bin", "LD_LIBRARY_PATH": "/opt/user/lib"}
93def test_path_mutation_with_unset_or_empty_path(apio_runner: ApioRunner):
94 """An unset or empty PATH is a fatal error."""
96 # -- The sandbox configures the apio console.
97 with apio_runner.in_sandbox():
98 mutations = EnvMutations(add_to_path=["/a/bin", "/b/bin"])
100 env: dict[str, str]
101 for env in [{}, {"PATH": ""}]:
102 with apio_runner.with_logger() as log:
103 with pytest.raises(SystemExit) as e:
104 apply_env_mutations(mutations, env)
105 assert e.value.code == 1, env
106 assert "The PATH env var is not set or is empty" in log.out, env
109def test_path_mutation_prepends_to_path():
110 """The added path items are prepended, in order, to the old PATH."""
112 mutations = EnvMutations(add_to_path=["/a/bin", "/b/bin"])
113 env = {"PATH": "/usr/bin"}
114 apply_env_mutations(mutations, env)
115 assert env == {"PATH": os.pathsep.join(["/a/bin", "/b/bin", "/usr/bin"])}
118def test_path_conflicts(apio_runner: ApioRunner):
119 """Test that there are no conflicting items along the Apio PATH
120 mutations."""
122 with apio_runner.in_sandbox() as sb:
124 # -- Create an ApioContext with access to Apio packages.
125 apio_ctx = ApioContext(
126 project_policy=ProjectPolicy.NO_PROJECT,
127 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
128 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
129 )
131 # -- Get the path mutations in order of search (first is highest
132 # -- priority)
133 mutations: EnvMutations = apio_ctx.get_env_mutations_for_subprocess(
134 include_apio_packages=True
135 )
136 apio_path_dirs = mutations.add_to_path
138 # -- Collect items on path
139 print(f"{apio_path_dirs=}")
140 matches: dict[str, list[Path]] = {}
141 for dir in apio_path_dirs:
142 dir_path = Path(dir)
143 files_paths: list[Path] = [
144 p for p in dir_path.glob("*") if p.is_file()
145 ]
146 for file_path in files_paths:
147 matches.setdefault(file_path.name, []).append(file_path)
149 # -- Report
150 for name, files in matches.items():
151 if len(files) > 1: 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true
152 data = files[0].read_bytes()
153 same = all(p.read_bytes() == data for p in files[1:])
154 print(f"\n{name} {same}")
155 for file in files:
156 print(f"- {str(file)}")
159def test_show_env_mutations(apio_runner: ApioRunner):
160 """Tests that the env mutations are shown as valid shell commands,
161 with added vars exported on non windows platforms so user can
162 copy/paste them."""
164 # -- The sandbox configures the apio console.
165 with apio_runner.in_sandbox():
167 mutations = EnvMutations(
168 add_to_path=["/my/bin"],
169 delete_env_var=["PYTHONPATH"],
170 add_env_vars=[NameValue(name="MY_VAR", value="my-value")],
171 )
173 # -- Non windows.
174 with apio_runner.with_logger() as log:
175 show_env_mutations(mutations, LINUX)
176 out = cunstyle(log.out)
177 assert 'PATH="/my/bin:$PATH"' in out
178 assert 'export PATH="' not in out
179 assert "unset PYTHONPATH" in out
180 assert 'export MY_VAR="my-value"' in out
182 # -- Windows.
183 with apio_runner.with_logger() as log:
184 show_env_mutations(mutations, WINDOWS)
185 out = cunstyle(log.out)
186 assert "set PATH=/my/bin;%PATH%" in out
187 assert "set PYTHONPATH=" in out
188 assert "set MY_VAR=my-value" in out
189 assert "export" not in out