Skip to content

Commit 2721451

Browse files
committed
tests: Use a subprocess to check discovered python == running
This replaces the use of `os.path.realpath()` which gave incorrect results on macOS - depending on the exact Python build, Python version, macOS version, installation method, and phase of the moon. realpath information kept around to aid debugging.
1 parent c6c8bfb commit 2721451

File tree

2 files changed

+111
-3
lines changed

2 files changed

+111
-3
lines changed

tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
that:
100100
- auto_out.ansible_facts.discovered_interpreter_python is defined
101101
- auto_out.ansible_facts.discovered_interpreter_python == echoout.discovered_python.as_seen
102-
- echoout.discovered_python.resolved == echoout.running_python.sys.executable.resolved
102+
- echoout.discovered_python.sys.executable.as_seen == echoout.running_python.sys.executable.as_seen
103103
fail_msg:
104104
- "auto_out: {{ auto_out }}"
105105
- "echoout: {{ echoout }}"

tests/ansible/lib/modules/test_echo_module.py

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,97 @@
1010
__metaclass__ = type
1111

1212
import os
13+
import stat
1314
import platform
15+
import subprocess
1416
import sys
17+
1518
from ansible.module_utils.basic import AnsibleModule
1619

1720

21+
# trace_realpath() and _join_tracepath() adapated from stdlib posixpath.py
22+
# https://github.com/python/cpython/blob/v3.12.6/Lib/posixpath.py#L423-L492
23+
# Copyright (c) 2001 - 2023 Python Software Foundation
24+
# Copyright (c) 2024 Alex Willmer <[email protected]>
25+
# License: Python Software Foundation License Version 2
26+
27+
def trace_realpath(filename, strict=False):
28+
"""
29+
Return the canonical path of the specified filename, and a trace of
30+
the route taken, eliminating any symbolic links encountered in the path.
31+
"""
32+
path, trace, ok = _join_tracepath(filename[:0], filename, strict, seen={}, trace=[])
33+
return os.path.abspath(path), trace
34+
35+
36+
def _join_tracepath(path, rest, strict, seen, trace):
37+
"""
38+
Join two paths, normalizing and eliminating any symbolic links encountered
39+
in the second path.
40+
"""
41+
trace.append(rest)
42+
if isinstance(path, bytes):
43+
sep = b'/'
44+
curdir = b'.'
45+
pardir = b'..'
46+
else:
47+
sep = '/'
48+
curdir = '.'
49+
pardir = '..'
50+
51+
if os.path.isabs(rest):
52+
rest = rest[1:]
53+
path = sep
54+
55+
while rest:
56+
name, _, rest = rest.partition(sep)
57+
if not name or name == curdir:
58+
# current dir
59+
continue
60+
if name == pardir:
61+
# parent dir
62+
if path:
63+
path, name = os.path.split(path)
64+
if name == pardir:
65+
path = os.path.join(path, pardir, pardir)
66+
else:
67+
path = pardir
68+
continue
69+
newpath = os.path.join(path, name)
70+
try:
71+
st = os.lstat(newpath)
72+
except OSError:
73+
if strict:
74+
raise
75+
is_link = False
76+
else:
77+
is_link = stat.S_ISLNK(st.st_mode)
78+
if not is_link:
79+
path = newpath
80+
continue
81+
# Resolve the symbolic link
82+
if newpath in seen:
83+
# Already seen this path
84+
path = seen[newpath]
85+
if path is not None:
86+
# use cached value
87+
continue
88+
# The symlink is not resolved, so we must have a symlink loop.
89+
if strict:
90+
# Raise OSError(errno.ELOOP)
91+
os.stat(newpath)
92+
else:
93+
# Return already resolved part + rest of the path unchanged.
94+
return os.path.join(newpath, rest), trace, False
95+
seen[newpath] = None # not resolved symlink
96+
path, trace, ok = _join_tracepath(path, os.readlink(newpath), strict, seen, trace)
97+
if not ok:
98+
return os.path.join(path, rest), False
99+
seen[newpath] = path # resolved symlink
100+
101+
return path, trace, True
102+
103+
18104
def main():
19105
module = AnsibleModule(argument_spec=dict(
20106
facts_copy=dict(type=dict, default={}),
@@ -33,7 +119,18 @@ def main():
33119
sys.executable = "/usr/bin/python"
34120

35121
facts_copy = module.params['facts_copy']
122+
36123
discovered_interpreter_python = facts_copy['discovered_interpreter_python']
124+
d_i_p_realpath, d_i_p_trace = trace_realpath(discovered_interpreter_python)
125+
d_i_p_proc = subprocess.Popen(
126+
[discovered_interpreter_python, '-c', 'import sys; print(sys.executable)'],
127+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
128+
129+
)
130+
d_i_p_stdout, d_i_p_stderr = d_i_p_proc.communicate()
131+
132+
sys_exec_realpath, sys_exec_trace = trace_realpath(sys.executable)
133+
37134
result = {
38135
'changed': False,
39136
'ansible_facts': module.params['facts_to_override'],
@@ -43,7 +140,17 @@ def main():
43140
),
44141
'discovered_python': {
45142
'as_seen': discovered_interpreter_python,
46-
'resolved': os.path.realpath(discovered_interpreter_python),
143+
'resolved': d_i_p_realpath,
144+
'trace': [os.path.abspath(p) for p in d_i_p_trace],
145+
'sys': {
146+
'executable': {
147+
'as_seen': d_i_p_stdout.decode('ascii').rstrip('\n'),
148+
'proc': {
149+
'stderr': d_i_p_stderr.decode('ascii'),
150+
'returncode': d_i_p_proc.returncode,
151+
},
152+
},
153+
},
47154
},
48155
'running_python': {
49156
'platform': {
@@ -54,7 +161,8 @@ def main():
54161
'sys': {
55162
'executable': {
56163
'as_seen': sys.executable,
57-
'resolved': os.path.realpath(sys.executable),
164+
'resolved': sys_exec_realpath,
165+
'trace': [os.path.abspath(p) for p in sys_exec_trace],
58166
},
59167
'platform': sys.platform,
60168
'version_info': {

0 commit comments

Comments
 (0)