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

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 

8 

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""" 

13 

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) 

33 

34# -- ApioPlatform instances. 

35LINUX = get_apio_platforms()["linux-x86-64"] 

36DARWIN = get_apio_platforms()["darwin-arm64"] 

37WINDOWS = get_apio_platforms()["windows-amd64"] 

38 

39 

40def test_fix_flag_only_for_pyinstaller_on_linux(): 

41 """The fix is requested only for a pyinstaller app on Linux.""" 

42 

43 empty_env_spec = ApioPlatformEnvSpec() 

44 

45 mutations = get_env_mutations_for_subprocess(LINUX, True, empty_env_spec) 

46 assert mutations.pyinstaller_linux_fix 

47 

48 mutations = get_env_mutations_for_subprocess(LINUX, False, empty_env_spec) 

49 assert not mutations.pyinstaller_linux_fix 

50 

51 mutations = get_env_mutations_for_subprocess(DARWIN, True, empty_env_spec) 

52 assert not mutations.pyinstaller_linux_fix 

53 

54 mutations = get_env_mutations_for_subprocess(DARWIN, False, empty_env_spec) 

55 assert not mutations.pyinstaller_linux_fix 

56 

57 

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.""" 

62 

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"} 

69 

70 

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.""" 

74 

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"} 

82 

83 

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.""" 

87 

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"} 

91 

92 

93def test_path_mutation_with_unset_or_empty_path(apio_runner: ApioRunner): 

94 """An unset or empty PATH is a fatal error.""" 

95 

96 # -- The sandbox configures the apio console. 

97 with apio_runner.in_sandbox(): 

98 mutations = EnvMutations(add_to_path=["/a/bin", "/b/bin"]) 

99 

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 

107 

108 

109def test_path_mutation_prepends_to_path(): 

110 """The added path items are prepended, in order, to the old PATH.""" 

111 

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"])} 

116 

117 

118def test_path_conflicts(apio_runner: ApioRunner): 

119 """Test that there are no conflicting items along the Apio PATH 

120 mutations.""" 

121 

122 with apio_runner.in_sandbox() as sb: 

123 

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 ) 

130 

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 

137 

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) 

148 

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)}") 

157 

158 

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.""" 

163 

164 # -- The sandbox configures the apio console. 

165 with apio_runner.in_sandbox(): 

166 

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 ) 

172 

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 

181 

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