ansible-core 2.13.11__py3-none-any.whl → 2.13.12rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansible-core might be problematic. Click here for more details.
- ansible/cli/config.py +1 -0
- ansible/cli/galaxy.py +6 -2
- ansible/cli/inventory.py +1 -1
- ansible/galaxy/role.py +33 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/release.py +1 -1
- ansible_core-2.13.12rc1.dist-info/METADATA +128 -0
- {ansible_core-2.13.11.dist-info → ansible_core-2.13.12rc1.dist-info}/RECORD +39 -38
- {ansible_core-2.13.11.dist-info → ansible_core-2.13.12rc1.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/remote.txt +2 -2
- ansible_test/_data/requirements/sanity.ansible-doc.txt +2 -0
- ansible_test/_data/requirements/sanity.changelog.txt +2 -0
- ansible_test/_data/requirements/sanity.import.plugin.txt +2 -0
- ansible_test/_data/requirements/sanity.import.txt +2 -0
- ansible_test/_data/requirements/sanity.integration-aliases.txt +2 -0
- ansible_test/_data/requirements/sanity.pylint.txt +2 -0
- ansible_test/_data/requirements/sanity.runtime-metadata.txt +2 -0
- ansible_test/_data/requirements/sanity.validate-modules.txt +2 -0
- ansible_test/_data/requirements/sanity.yamllint.txt +2 -0
- ansible_test/_internal/ansible_util.py +57 -4
- ansible_test/_internal/classification/__init__.py +6 -13
- ansible_test/_internal/classification/python.py +0 -1
- ansible_test/_internal/commands/sanity/__init__.py +35 -1
- ansible_test/_internal/commands/sanity/bin_symlinks.py +102 -0
- ansible_test/_internal/commands/sanity/integration_aliases.py +401 -0
- ansible_test/_internal/commands/sanity/validate_modules.py +5 -1
- ansible_test/_internal/constants.py +1 -0
- ansible_test/_internal/delegation.py +5 -2
- ansible_test/_internal/provider/layout/ansible.py +1 -1
- ansible_test/_internal/provider/source/unversioned.py +0 -3
- ansible_test/_internal/python_requirements.py +7 -0
- ansible_test/_internal/util.py +0 -2
- ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1 +1 -1
- ansible_test/_util/target/setup/bootstrap.sh +2 -0
- ansible_test/_util/target/setup/requirements.py +63 -0
- ansible_core-2.13.11.dist-info/METADATA +0 -154
- ansible_test/_internal/commands/sanity/sanity_docs.py +0 -60
- {ansible_core-2.13.11.data → ansible_core-2.13.12rc1.data}/scripts/ansible-test +0 -0
- {ansible_core-2.13.11.dist-info → ansible_core-2.13.12rc1.dist-info}/COPYING +0 -0
- {ansible_core-2.13.11.dist-info → ansible_core-2.13.12rc1.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.13.11.dist-info → ansible_core-2.13.12rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Sanity test to check integration test aliases."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
import json
|
|
6
|
+
import textwrap
|
|
7
|
+
import os
|
|
8
|
+
import typing as t
|
|
9
|
+
|
|
10
|
+
from . import (
|
|
11
|
+
SanitySingleVersion,
|
|
12
|
+
SanityMessage,
|
|
13
|
+
SanityFailure,
|
|
14
|
+
SanitySuccess,
|
|
15
|
+
SanityTargets,
|
|
16
|
+
SANITY_ROOT,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ...test import (
|
|
20
|
+
TestResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ...config import (
|
|
24
|
+
SanityConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from ...target import (
|
|
28
|
+
filter_targets,
|
|
29
|
+
walk_posix_integration_targets,
|
|
30
|
+
walk_windows_integration_targets,
|
|
31
|
+
walk_integration_targets,
|
|
32
|
+
walk_module_targets,
|
|
33
|
+
CompletionTarget,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ..integration.cloud import (
|
|
37
|
+
get_cloud_platforms,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from ...io import (
|
|
41
|
+
read_text_file,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from ...util import (
|
|
45
|
+
display,
|
|
46
|
+
raw_command,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from ...util_common import (
|
|
50
|
+
get_docs_url,
|
|
51
|
+
write_json_test_results,
|
|
52
|
+
ResultType,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
from ...host_configs import (
|
|
56
|
+
PythonConfig,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class IntegrationAliasesTest(SanitySingleVersion):
|
|
61
|
+
"""Sanity test to evaluate integration test aliases."""
|
|
62
|
+
CI_YML = '.azure-pipelines/azure-pipelines.yml'
|
|
63
|
+
TEST_ALIAS_PREFIX = 'shippable' # this will be changed at some point in the future
|
|
64
|
+
|
|
65
|
+
DISABLED = 'disabled/'
|
|
66
|
+
UNSTABLE = 'unstable/'
|
|
67
|
+
UNSUPPORTED = 'unsupported/'
|
|
68
|
+
|
|
69
|
+
EXPLAIN_URL = get_docs_url('https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/integration-aliases.html')
|
|
70
|
+
|
|
71
|
+
TEMPLATE_DISABLED = """
|
|
72
|
+
The following integration tests are **disabled** [[explain]({explain_url}#disabled)]:
|
|
73
|
+
|
|
74
|
+
{tests}
|
|
75
|
+
|
|
76
|
+
Consider fixing the integration tests before or alongside changes.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
TEMPLATE_UNSTABLE = """
|
|
80
|
+
The following integration tests are **unstable** [[explain]({explain_url}#unstable)]:
|
|
81
|
+
|
|
82
|
+
{tests}
|
|
83
|
+
|
|
84
|
+
Tests may need to be restarted due to failures unrelated to changes.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
TEMPLATE_UNSUPPORTED = """
|
|
88
|
+
The following integration tests are **unsupported** [[explain]({explain_url}#unsupported)]:
|
|
89
|
+
|
|
90
|
+
{tests}
|
|
91
|
+
|
|
92
|
+
Consider running the tests manually or extending test infrastructure to add support.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
TEMPLATE_UNTESTED = """
|
|
96
|
+
The following modules have **no integration tests** [[explain]({explain_url}#untested)]:
|
|
97
|
+
|
|
98
|
+
{tests}
|
|
99
|
+
|
|
100
|
+
Consider adding integration tests before or alongside changes.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
ansible_only = True
|
|
104
|
+
|
|
105
|
+
def __init__(self):
|
|
106
|
+
super().__init__()
|
|
107
|
+
|
|
108
|
+
self._ci_config = {} # type: t.Dict[str, t.Any]
|
|
109
|
+
self._ci_test_groups = {} # type: t.Dict[str, t.List[int]]
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def can_ignore(self): # type: () -> bool
|
|
113
|
+
"""True if the test supports ignore entries."""
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def no_targets(self): # type: () -> bool
|
|
118
|
+
"""True if the test does not use test targets. Mutually exclusive with all_targets."""
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def load_ci_config(self, python): # type: (PythonConfig) -> t.Dict[str, t.Any]
|
|
122
|
+
"""Load and return the CI YAML configuration."""
|
|
123
|
+
if not self._ci_config:
|
|
124
|
+
self._ci_config = self.load_yaml(python, self.CI_YML)
|
|
125
|
+
|
|
126
|
+
return self._ci_config
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def ci_test_groups(self): # type: () -> t.Dict[str, t.List[int]]
|
|
130
|
+
"""Return a dictionary of CI test names and their group(s)."""
|
|
131
|
+
if not self._ci_test_groups:
|
|
132
|
+
test_groups = {} # type: t.Dict[str, t.Set[int]]
|
|
133
|
+
|
|
134
|
+
for stage in self._ci_config['stages']:
|
|
135
|
+
for job in stage['jobs']:
|
|
136
|
+
if job.get('template') != 'templates/matrix.yml':
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
parameters = job['parameters']
|
|
140
|
+
|
|
141
|
+
groups = parameters.get('groups', [])
|
|
142
|
+
test_format = parameters.get('testFormat', '{0}')
|
|
143
|
+
test_group_format = parameters.get('groupFormat', '{0}/{{1}}')
|
|
144
|
+
|
|
145
|
+
for target in parameters['targets']:
|
|
146
|
+
test = target.get('test') or target.get('name')
|
|
147
|
+
|
|
148
|
+
if groups:
|
|
149
|
+
tests_formatted = [test_group_format.format(test_format).format(test, group) for group in groups]
|
|
150
|
+
else:
|
|
151
|
+
tests_formatted = [test_format.format(test)]
|
|
152
|
+
|
|
153
|
+
for test_formatted in tests_formatted:
|
|
154
|
+
parts = test_formatted.split('/')
|
|
155
|
+
key = parts[0]
|
|
156
|
+
|
|
157
|
+
if key in ('sanity', 'units'):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
group = int(parts[-1])
|
|
162
|
+
except ValueError:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if group < 1 or group > 99:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
group_set = test_groups.setdefault(key, set())
|
|
169
|
+
group_set.add(group)
|
|
170
|
+
|
|
171
|
+
self._ci_test_groups = dict((key, sorted(value)) for key, value in test_groups.items())
|
|
172
|
+
|
|
173
|
+
return self._ci_test_groups
|
|
174
|
+
|
|
175
|
+
def format_test_group_alias(self, name, fallback=''): # type: (str, str) -> str
|
|
176
|
+
"""Return a test group alias using the given name and fallback."""
|
|
177
|
+
group_numbers = self.ci_test_groups.get(name, None)
|
|
178
|
+
|
|
179
|
+
if group_numbers:
|
|
180
|
+
if min(group_numbers) != 1:
|
|
181
|
+
display.warning('Min test group "%s" in %s is %d instead of 1.' % (name, self.CI_YML, min(group_numbers)), unique=True)
|
|
182
|
+
|
|
183
|
+
if max(group_numbers) != len(group_numbers):
|
|
184
|
+
display.warning('Max test group "%s" in %s is %d instead of %d.' % (name, self.CI_YML, max(group_numbers), len(group_numbers)), unique=True)
|
|
185
|
+
|
|
186
|
+
if max(group_numbers) > 9:
|
|
187
|
+
alias = '%s/%s/group(%s)/' % (self.TEST_ALIAS_PREFIX, name, '|'.join(str(i) for i in range(min(group_numbers), max(group_numbers) + 1)))
|
|
188
|
+
elif len(group_numbers) > 1:
|
|
189
|
+
alias = '%s/%s/group[%d-%d]/' % (self.TEST_ALIAS_PREFIX, name, min(group_numbers), max(group_numbers))
|
|
190
|
+
else:
|
|
191
|
+
alias = '%s/%s/group%d/' % (self.TEST_ALIAS_PREFIX, name, min(group_numbers))
|
|
192
|
+
elif fallback:
|
|
193
|
+
alias = '%s/%s/group%d/' % (self.TEST_ALIAS_PREFIX, fallback, 1)
|
|
194
|
+
else:
|
|
195
|
+
raise Exception('cannot find test group "%s" in %s' % (name, self.CI_YML))
|
|
196
|
+
|
|
197
|
+
return alias
|
|
198
|
+
|
|
199
|
+
def load_yaml(self, python, path): # type: (PythonConfig, str) -> t.Dict[str, t.Any]
|
|
200
|
+
"""Load the specified YAML file and return the contents."""
|
|
201
|
+
yaml_to_json_path = os.path.join(SANITY_ROOT, self.name, 'yaml_to_json.py')
|
|
202
|
+
return json.loads(raw_command([python.path, yaml_to_json_path], data=read_text_file(path), capture=True)[0])
|
|
203
|
+
|
|
204
|
+
def test(self, args, targets, python): # type: (SanityConfig, SanityTargets, PythonConfig) -> TestResult
|
|
205
|
+
if args.explain:
|
|
206
|
+
return SanitySuccess(self.name)
|
|
207
|
+
|
|
208
|
+
if not os.path.isfile(self.CI_YML):
|
|
209
|
+
return SanityFailure(self.name, messages=[SanityMessage(
|
|
210
|
+
message='file missing',
|
|
211
|
+
path=self.CI_YML,
|
|
212
|
+
)])
|
|
213
|
+
|
|
214
|
+
results = Results(
|
|
215
|
+
comments=[],
|
|
216
|
+
labels={},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self.load_ci_config(python)
|
|
220
|
+
self.check_changes(args, results)
|
|
221
|
+
|
|
222
|
+
write_json_test_results(ResultType.BOT, 'data-sanity-ci.json', results.__dict__)
|
|
223
|
+
|
|
224
|
+
messages = []
|
|
225
|
+
|
|
226
|
+
messages += self.check_posix_targets(args)
|
|
227
|
+
messages += self.check_windows_targets()
|
|
228
|
+
|
|
229
|
+
if messages:
|
|
230
|
+
return SanityFailure(self.name, messages=messages)
|
|
231
|
+
|
|
232
|
+
return SanitySuccess(self.name)
|
|
233
|
+
|
|
234
|
+
def check_posix_targets(self, args): # type: (SanityConfig) -> t.List[SanityMessage]
|
|
235
|
+
"""Check POSIX integration test targets and return messages with any issues found."""
|
|
236
|
+
posix_targets = tuple(walk_posix_integration_targets())
|
|
237
|
+
|
|
238
|
+
clouds = get_cloud_platforms(args, posix_targets)
|
|
239
|
+
cloud_targets = ['cloud/%s/' % cloud for cloud in clouds]
|
|
240
|
+
|
|
241
|
+
all_cloud_targets = tuple(filter_targets(posix_targets, ['cloud/'], directories=False, errors=False))
|
|
242
|
+
invalid_cloud_targets = tuple(filter_targets(all_cloud_targets, cloud_targets, include=False, directories=False, errors=False))
|
|
243
|
+
|
|
244
|
+
messages = []
|
|
245
|
+
|
|
246
|
+
for target in invalid_cloud_targets:
|
|
247
|
+
for alias in target.aliases:
|
|
248
|
+
if alias.startswith('cloud/') and alias != 'cloud/':
|
|
249
|
+
if any(alias.startswith(cloud_target) for cloud_target in cloud_targets):
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
messages.append(SanityMessage('invalid alias `%s`' % alias, '%s/aliases' % target.path))
|
|
253
|
+
|
|
254
|
+
messages += self.check_ci_group(
|
|
255
|
+
targets=tuple(filter_targets(posix_targets, ['cloud/', '%s/generic/' % self.TEST_ALIAS_PREFIX], include=False, directories=False, errors=False)),
|
|
256
|
+
find=self.format_test_group_alias('linux').replace('linux', 'posix'),
|
|
257
|
+
find_incidental=['%s/posix/incidental/' % self.TEST_ALIAS_PREFIX],
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
messages += self.check_ci_group(
|
|
261
|
+
targets=tuple(filter_targets(posix_targets, ['%s/generic/' % self.TEST_ALIAS_PREFIX], directories=False, errors=False)),
|
|
262
|
+
find=self.format_test_group_alias('generic'),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
for cloud in clouds:
|
|
266
|
+
if cloud == 'httptester':
|
|
267
|
+
find = self.format_test_group_alias('linux').replace('linux', 'posix')
|
|
268
|
+
find_incidental = ['%s/posix/incidental/' % self.TEST_ALIAS_PREFIX]
|
|
269
|
+
else:
|
|
270
|
+
find = self.format_test_group_alias(cloud, 'generic')
|
|
271
|
+
find_incidental = ['%s/%s/incidental/' % (self.TEST_ALIAS_PREFIX, cloud), '%s/cloud/incidental/' % self.TEST_ALIAS_PREFIX]
|
|
272
|
+
|
|
273
|
+
messages += self.check_ci_group(
|
|
274
|
+
targets=tuple(filter_targets(posix_targets, ['cloud/%s/' % cloud], directories=False, errors=False)),
|
|
275
|
+
find=find,
|
|
276
|
+
find_incidental=find_incidental,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return messages
|
|
280
|
+
|
|
281
|
+
def check_windows_targets(self):
|
|
282
|
+
"""
|
|
283
|
+
:rtype: list[SanityMessage]
|
|
284
|
+
"""
|
|
285
|
+
windows_targets = tuple(walk_windows_integration_targets())
|
|
286
|
+
|
|
287
|
+
messages = []
|
|
288
|
+
|
|
289
|
+
messages += self.check_ci_group(
|
|
290
|
+
targets=windows_targets,
|
|
291
|
+
find=self.format_test_group_alias('windows'),
|
|
292
|
+
find_incidental=['%s/windows/incidental/' % self.TEST_ALIAS_PREFIX],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return messages
|
|
296
|
+
|
|
297
|
+
def check_ci_group(
|
|
298
|
+
self,
|
|
299
|
+
targets, # type: t.Tuple[CompletionTarget, ...]
|
|
300
|
+
find, # type: str
|
|
301
|
+
find_incidental=None, # type: t.Optional[t.List[str]]
|
|
302
|
+
): # type: (...) -> t.List[SanityMessage]
|
|
303
|
+
"""Check the CI groups set in the provided targets and return a list of messages with any issues found."""
|
|
304
|
+
all_paths = set(target.path for target in targets)
|
|
305
|
+
supported_paths = set(target.path for target in filter_targets(targets, [find], directories=False, errors=False))
|
|
306
|
+
unsupported_paths = set(target.path for target in filter_targets(targets, [self.UNSUPPORTED], directories=False, errors=False))
|
|
307
|
+
|
|
308
|
+
if find_incidental:
|
|
309
|
+
incidental_paths = set(target.path for target in filter_targets(targets, find_incidental, directories=False, errors=False))
|
|
310
|
+
else:
|
|
311
|
+
incidental_paths = set()
|
|
312
|
+
|
|
313
|
+
unassigned_paths = all_paths - supported_paths - unsupported_paths - incidental_paths
|
|
314
|
+
conflicting_paths = supported_paths & unsupported_paths
|
|
315
|
+
|
|
316
|
+
unassigned_message = 'missing alias `%s` or `%s`' % (find.strip('/'), self.UNSUPPORTED.strip('/'))
|
|
317
|
+
conflicting_message = 'conflicting alias `%s` and `%s`' % (find.strip('/'), self.UNSUPPORTED.strip('/'))
|
|
318
|
+
|
|
319
|
+
messages = []
|
|
320
|
+
|
|
321
|
+
for path in unassigned_paths:
|
|
322
|
+
if path == 'test/integration/targets/ansible-test-container':
|
|
323
|
+
continue # special test target which uses group 6 -- nothing else should be in that group
|
|
324
|
+
|
|
325
|
+
messages.append(SanityMessage(unassigned_message, '%s/aliases' % path))
|
|
326
|
+
|
|
327
|
+
for path in conflicting_paths:
|
|
328
|
+
messages.append(SanityMessage(conflicting_message, '%s/aliases' % path))
|
|
329
|
+
|
|
330
|
+
return messages
|
|
331
|
+
|
|
332
|
+
def check_changes(self, args, results): # type: (SanityConfig, Results) -> None
|
|
333
|
+
"""Check changes and store results in the provided result dictionary."""
|
|
334
|
+
integration_targets = list(walk_integration_targets())
|
|
335
|
+
module_targets = list(walk_module_targets())
|
|
336
|
+
|
|
337
|
+
integration_targets_by_name = dict((target.name, target) for target in integration_targets)
|
|
338
|
+
module_names_by_path = dict((target.path, target.module) for target in module_targets)
|
|
339
|
+
|
|
340
|
+
disabled_targets = []
|
|
341
|
+
unstable_targets = []
|
|
342
|
+
unsupported_targets = []
|
|
343
|
+
|
|
344
|
+
for command in [command for command in args.metadata.change_description.focused_command_targets if 'integration' in command]:
|
|
345
|
+
for target in args.metadata.change_description.focused_command_targets[command]:
|
|
346
|
+
if self.DISABLED in integration_targets_by_name[target].aliases:
|
|
347
|
+
disabled_targets.append(target)
|
|
348
|
+
elif self.UNSTABLE in integration_targets_by_name[target].aliases:
|
|
349
|
+
unstable_targets.append(target)
|
|
350
|
+
elif self.UNSUPPORTED in integration_targets_by_name[target].aliases:
|
|
351
|
+
unsupported_targets.append(target)
|
|
352
|
+
|
|
353
|
+
untested_modules = []
|
|
354
|
+
|
|
355
|
+
for path in args.metadata.change_description.no_integration_paths:
|
|
356
|
+
module = module_names_by_path.get(path)
|
|
357
|
+
|
|
358
|
+
if module:
|
|
359
|
+
untested_modules.append(module)
|
|
360
|
+
|
|
361
|
+
comments = [
|
|
362
|
+
self.format_comment(self.TEMPLATE_DISABLED, disabled_targets),
|
|
363
|
+
self.format_comment(self.TEMPLATE_UNSTABLE, unstable_targets),
|
|
364
|
+
self.format_comment(self.TEMPLATE_UNSUPPORTED, unsupported_targets),
|
|
365
|
+
self.format_comment(self.TEMPLATE_UNTESTED, untested_modules),
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
comments = [comment for comment in comments if comment]
|
|
369
|
+
|
|
370
|
+
labels = dict(
|
|
371
|
+
needs_tests=bool(untested_modules),
|
|
372
|
+
disabled_tests=bool(disabled_targets),
|
|
373
|
+
unstable_tests=bool(unstable_targets),
|
|
374
|
+
unsupported_tests=bool(unsupported_targets),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
results.comments += comments
|
|
378
|
+
results.labels.update(labels)
|
|
379
|
+
|
|
380
|
+
def format_comment(self, template, targets): # type: (str, t.List[str]) -> t.Optional[str]
|
|
381
|
+
"""Format and return a comment based on the given template and targets, or None if there are no targets."""
|
|
382
|
+
if not targets:
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
tests = '\n'.join('- %s' % target for target in targets)
|
|
386
|
+
|
|
387
|
+
data = dict(
|
|
388
|
+
explain_url=self.EXPLAIN_URL,
|
|
389
|
+
tests=tests,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
message = textwrap.dedent(template).strip().format(**data)
|
|
393
|
+
|
|
394
|
+
return message
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclasses.dataclass
|
|
398
|
+
class Results:
|
|
399
|
+
"""Check results."""
|
|
400
|
+
comments: t.List[str]
|
|
401
|
+
labels: t.Dict[str, bool]
|
|
@@ -154,7 +154,11 @@ class ValidateModulesTest(SanitySingleVersion):
|
|
|
154
154
|
temp_dir = process_scoped_temporary_directory(args)
|
|
155
155
|
|
|
156
156
|
with tarfile.open(path) as file:
|
|
157
|
-
|
|
157
|
+
# deprecated: description='extractall fallback without filter' python_version='3.11'
|
|
158
|
+
if hasattr(tarfile, 'data_filter'):
|
|
159
|
+
file.extractall(temp_dir, filter='data') # type: ignore[call-arg]
|
|
160
|
+
else:
|
|
161
|
+
file.extractall(temp_dir)
|
|
158
162
|
|
|
159
163
|
cmd.extend([
|
|
160
164
|
'--original-plugins', temp_dir,
|
|
@@ -36,6 +36,7 @@ SECCOMP_CHOICES = [
|
|
|
36
36
|
# This bin symlink map must exactly match the contents of the bin directory.
|
|
37
37
|
# It is necessary for payload creation to reconstruct the bin directory when running ansible-test from an installed version of ansible.
|
|
38
38
|
# It is also used to construct the injector directory at runtime.
|
|
39
|
+
# It is also used to construct entry points when not running ansible-test from source.
|
|
39
40
|
ANSIBLE_BIN_SYMLINK_MAP = {
|
|
40
41
|
'ansible': '../lib/ansible/cli/adhoc.py',
|
|
41
42
|
'ansible-config': '../lib/ansible/cli/config.py',
|
|
@@ -28,7 +28,6 @@ from .util import (
|
|
|
28
28
|
SubprocessError,
|
|
29
29
|
display,
|
|
30
30
|
filter_args,
|
|
31
|
-
ANSIBLE_BIN_PATH,
|
|
32
31
|
ANSIBLE_LIB_ROOT,
|
|
33
32
|
ANSIBLE_TEST_ROOT,
|
|
34
33
|
OutputStream,
|
|
@@ -39,6 +38,10 @@ from .util_common import (
|
|
|
39
38
|
process_scoped_temporary_directory,
|
|
40
39
|
)
|
|
41
40
|
|
|
41
|
+
from .ansible_util import (
|
|
42
|
+
get_ansible_bin_path,
|
|
43
|
+
)
|
|
44
|
+
|
|
42
45
|
from .containers import (
|
|
43
46
|
support_container_context,
|
|
44
47
|
ContainerDatabase,
|
|
@@ -140,7 +143,7 @@ def delegate_command(args, host_state, exclude, require): # type: (EnvironmentC
|
|
|
140
143
|
con.extract_archive(chdir=working_directory, src=payload_file)
|
|
141
144
|
else:
|
|
142
145
|
content_root = working_directory
|
|
143
|
-
ansible_bin_path =
|
|
146
|
+
ansible_bin_path = get_ansible_bin_path(args)
|
|
144
147
|
|
|
145
148
|
command = generate_command(args, host_state.controller_profile.python, ansible_bin_path, content_root, exclude, require)
|
|
146
149
|
|
|
@@ -20,7 +20,7 @@ class AnsibleLayout(LayoutProvider):
|
|
|
20
20
|
@staticmethod
|
|
21
21
|
def is_content_root(path): # type: (str) -> bool
|
|
22
22
|
"""Return True if the given path is a content root for this provider."""
|
|
23
|
-
return os.path.
|
|
23
|
+
return os.path.isfile(os.path.join(path, 'pyproject.toml')) and os.path.isdir(os.path.join(path, 'test/lib/ansible_test'))
|
|
24
24
|
|
|
25
25
|
def create(self, root, paths): # type: (str, t.List[str]) -> ContentLayout
|
|
26
26
|
"""Create a Layout using the given root and paths."""
|
|
@@ -245,6 +245,13 @@ def collect_requirements(
|
|
|
245
245
|
# installed packages may have run-time dependencies on setuptools
|
|
246
246
|
uninstall_packages.remove('setuptools')
|
|
247
247
|
|
|
248
|
+
# hack to allow the package-data sanity test to keep wheel in the venv
|
|
249
|
+
install_commands = [command for command in commands if isinstance(command, PipInstall)]
|
|
250
|
+
install_wheel = any(install.has_package('wheel') for install in install_commands)
|
|
251
|
+
|
|
252
|
+
if install_wheel:
|
|
253
|
+
uninstall_packages.remove('wheel')
|
|
254
|
+
|
|
248
255
|
commands.extend(collect_uninstall(packages=uninstall_packages))
|
|
249
256
|
|
|
250
257
|
return commands
|
ansible_test/_internal/util.py
CHANGED
|
@@ -69,14 +69,12 @@ ANSIBLE_TEST_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
69
69
|
|
|
70
70
|
# assume running from install
|
|
71
71
|
ANSIBLE_ROOT = os.path.dirname(ANSIBLE_TEST_ROOT)
|
|
72
|
-
ANSIBLE_BIN_PATH = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
73
72
|
ANSIBLE_LIB_ROOT = os.path.join(ANSIBLE_ROOT, 'ansible')
|
|
74
73
|
ANSIBLE_SOURCE_ROOT = None
|
|
75
74
|
|
|
76
75
|
if not os.path.exists(ANSIBLE_LIB_ROOT):
|
|
77
76
|
# running from source
|
|
78
77
|
ANSIBLE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(ANSIBLE_TEST_ROOT)))
|
|
79
|
-
ANSIBLE_BIN_PATH = os.path.join(ANSIBLE_ROOT, 'bin')
|
|
80
78
|
ANSIBLE_LIB_ROOT = os.path.join(ANSIBLE_ROOT, 'lib', 'ansible')
|
|
81
79
|
ANSIBLE_SOURCE_ROOT = ANSIBLE_ROOT
|
|
82
80
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# To run this script in Powershell:
|
|
16
16
|
#
|
|
17
17
|
# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
18
|
-
# $url = "https://raw.githubusercontent.com/ansible/ansible/devel/
|
|
18
|
+
# $url = "https://raw.githubusercontent.com/ansible/ansible/devel/test/lib/ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1"
|
|
19
19
|
# $file = "$env:temp\ConfigureRemotingForAnsible.ps1"
|
|
20
20
|
#
|
|
21
21
|
# (New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
|
|
@@ -171,6 +171,8 @@ bootstrap_remote_freebsd()
|
|
|
171
171
|
# Declare platform/python version combinations which do not have supporting OS packages available.
|
|
172
172
|
# For these combinations ansible-test will use pip to install the requirements instead.
|
|
173
173
|
case "${platform_version}/${python_version}" in
|
|
174
|
+
"12.4/3.9")
|
|
175
|
+
;;
|
|
174
176
|
"13.0/3.8")
|
|
175
177
|
jinja2_pkg="" # not available
|
|
176
178
|
cryptography_pkg="" # not available
|
|
@@ -134,6 +134,14 @@ def install(pip, options): # type: (str, t.Dict[str, t.Any]) -> None
|
|
|
134
134
|
options.extend(packages)
|
|
135
135
|
|
|
136
136
|
for path, content in requirements:
|
|
137
|
+
if path.split(os.sep)[0] in ('test', 'requirements'):
|
|
138
|
+
# Support for pre-build is currently limited to requirements embedded in ansible-test and those used by ansible-core.
|
|
139
|
+
# Requirements from ansible-core can be found in the 'test' and 'requirements' directories.
|
|
140
|
+
# This feature will probably be extended to support collections after further testing.
|
|
141
|
+
# Requirements from collections can be found in the 'tests' directory.
|
|
142
|
+
for pre_build in parse_pre_build_instructions(content):
|
|
143
|
+
pre_build.execute(pip)
|
|
144
|
+
|
|
137
145
|
write_text_file(os.path.join(tempdir, path), content, True)
|
|
138
146
|
options.extend(['-r', path])
|
|
139
147
|
|
|
@@ -150,6 +158,61 @@ def install(pip, options): # type: (str, t.Dict[str, t.Any]) -> None
|
|
|
150
158
|
remove_tree(tempdir)
|
|
151
159
|
|
|
152
160
|
|
|
161
|
+
class PreBuild:
|
|
162
|
+
"""Parsed pre-build instructions."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, requirement): # type: (str) -> None
|
|
165
|
+
self.requirement = requirement
|
|
166
|
+
self.constraints = [] # type: list[str]
|
|
167
|
+
|
|
168
|
+
def execute(self, pip): # type: (str) -> None
|
|
169
|
+
"""Execute these pre-build instructions."""
|
|
170
|
+
tempdir = tempfile.mkdtemp(prefix='ansible-test-', suffix='-pre-build')
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
options = common_pip_options()
|
|
174
|
+
options.append(self.requirement)
|
|
175
|
+
|
|
176
|
+
constraints = '\n'.join(self.constraints) + '\n'
|
|
177
|
+
constraints_path = os.path.join(tempdir, 'constraints.txt')
|
|
178
|
+
|
|
179
|
+
write_text_file(constraints_path, constraints, True)
|
|
180
|
+
|
|
181
|
+
env = common_pip_environment()
|
|
182
|
+
env.update(PIP_CONSTRAINT=constraints_path)
|
|
183
|
+
|
|
184
|
+
command = [sys.executable, pip, 'wheel'] + options
|
|
185
|
+
|
|
186
|
+
execute_command(command, env=env, cwd=tempdir)
|
|
187
|
+
finally:
|
|
188
|
+
remove_tree(tempdir)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def parse_pre_build_instructions(requirements): # type: (str) -> list[PreBuild]
|
|
192
|
+
"""Parse the given pip requirements and return a list of extracted pre-build instructions."""
|
|
193
|
+
# CAUTION: This code must be kept in sync with the sanity test hashing code in:
|
|
194
|
+
# test/lib/ansible_test/_internal/commands/sanity/__init__.py
|
|
195
|
+
|
|
196
|
+
pre_build_prefix = '# pre-build '
|
|
197
|
+
pre_build_requirement_prefix = pre_build_prefix + 'requirement: '
|
|
198
|
+
pre_build_constraint_prefix = pre_build_prefix + 'constraint: '
|
|
199
|
+
|
|
200
|
+
lines = requirements.splitlines()
|
|
201
|
+
pre_build_lines = [line for line in lines if line.startswith(pre_build_prefix)]
|
|
202
|
+
|
|
203
|
+
instructions = [] # type: list[PreBuild]
|
|
204
|
+
|
|
205
|
+
for line in pre_build_lines:
|
|
206
|
+
if line.startswith(pre_build_requirement_prefix):
|
|
207
|
+
instructions.append(PreBuild(line[len(pre_build_requirement_prefix):]))
|
|
208
|
+
elif line.startswith(pre_build_constraint_prefix):
|
|
209
|
+
instructions[-1].constraints.append(line[len(pre_build_constraint_prefix):])
|
|
210
|
+
else:
|
|
211
|
+
raise RuntimeError('Unsupported pre-build comment: ' + line)
|
|
212
|
+
|
|
213
|
+
return instructions
|
|
214
|
+
|
|
215
|
+
|
|
153
216
|
def uninstall(pip, options): # type: (str, t.Dict[str, t.Any]) -> None
|
|
154
217
|
"""Perform a pip uninstall."""
|
|
155
218
|
packages = options['packages']
|