ansible-core 2.15.2rc1__py3-none-any.whl → 2.15.3__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 +20 -3
- ansible/cli/inventory.py +1 -1
- ansible/executor/task_executor.py +1 -2
- ansible/galaxy/collection/__init__.py +3 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/urls.py +16 -12
- ansible/modules/getent.py +1 -1
- ansible/modules/git.py +1 -1
- ansible/modules/setup.py +1 -1
- ansible/modules/stat.py +3 -3
- ansible/modules/validate_argument_spec.py +1 -1
- ansible/modules/yum.py +1 -1
- ansible/plugins/action/__init__.py +1 -1
- ansible/plugins/filter/split.yml +1 -1
- ansible/release.py +1 -1
- ansible/utils/encrypt.py +9 -2
- ansible_core-2.15.3.dist-info/METADATA +129 -0
- {ansible_core-2.15.2rc1.dist-info → ansible_core-2.15.3.dist-info}/RECORD +51 -49
- {ansible_core-2.15.2rc1.dist-info → ansible_core-2.15.3.dist-info}/WHEEL +1 -1
- 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 +1 -1
- ansible_test/_internal/classification/__init__.py +6 -13
- ansible_test/_internal/classification/python.py +0 -1
- ansible_test/_internal/commands/integration/cloud/cs.py +1 -1
- ansible_test/_internal/commands/sanity/__init__.py +44 -7
- ansible_test/_internal/commands/sanity/ansible_doc.py +3 -0
- ansible_test/_internal/commands/sanity/bin_symlinks.py +102 -0
- ansible_test/_internal/commands/sanity/import.py +1 -1
- ansible_test/_internal/commands/sanity/integration_aliases.py +430 -0
- ansible_test/_internal/commands/sanity/mypy.py +6 -1
- 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 +1 -1
- ansible_test/_internal/util_common.py +7 -2
- ansible_test/_util/controller/sanity/mypy/packaging.ini +20 -0
- ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1 +1 -1
- ansible_test/_util/target/setup/requirements.py +63 -0
- ansible_core-2.15.2rc1.dist-info/METADATA +0 -155
- ansible_test/_internal/commands/sanity/sanity_docs.py +0 -61
- {ansible_core-2.15.2rc1.data → ansible_core-2.15.3.data}/scripts/ansible-test +0 -0
- {ansible_core-2.15.2rc1.dist-info → ansible_core-2.15.3.dist-info}/COPYING +0 -0
- {ansible_core-2.15.2rc1.dist-info → ansible_core-2.15.3.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.15.2rc1.dist-info → ansible_core-2.15.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Sanity test for symlinks in the bin directory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from . import (
|
|
7
|
+
SanityVersionNeutral,
|
|
8
|
+
SanityMessage,
|
|
9
|
+
SanityFailure,
|
|
10
|
+
SanitySuccess,
|
|
11
|
+
SanityTargets,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ...constants import (
|
|
15
|
+
__file__ as symlink_map_full_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ...test import (
|
|
19
|
+
TestResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ...config import (
|
|
23
|
+
SanityConfig,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from ...data import (
|
|
27
|
+
data_context,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ...payload import (
|
|
31
|
+
ANSIBLE_BIN_SYMLINK_MAP,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from ...util import (
|
|
35
|
+
ANSIBLE_BIN_PATH,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BinSymlinksTest(SanityVersionNeutral):
|
|
40
|
+
"""Sanity test for symlinks in the bin directory."""
|
|
41
|
+
|
|
42
|
+
ansible_only = True
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def can_ignore(self) -> bool:
|
|
46
|
+
"""True if the test supports ignore entries."""
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def no_targets(self) -> bool:
|
|
51
|
+
"""True if the test does not use test targets. Mutually exclusive with all_targets."""
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def test(self, args: SanityConfig, targets: SanityTargets) -> TestResult:
|
|
55
|
+
bin_root = ANSIBLE_BIN_PATH
|
|
56
|
+
bin_names = os.listdir(bin_root)
|
|
57
|
+
bin_paths = sorted(os.path.join(bin_root, path) for path in bin_names)
|
|
58
|
+
|
|
59
|
+
errors: list[tuple[str, str]] = []
|
|
60
|
+
|
|
61
|
+
symlink_map_path = os.path.relpath(symlink_map_full_path, data_context().content.root)
|
|
62
|
+
|
|
63
|
+
for bin_path in bin_paths:
|
|
64
|
+
if not os.path.islink(bin_path):
|
|
65
|
+
errors.append((bin_path, 'not a symbolic link'))
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
dest = os.readlink(bin_path)
|
|
69
|
+
|
|
70
|
+
if not os.path.exists(bin_path):
|
|
71
|
+
errors.append((bin_path, 'points to non-existent path "%s"' % dest))
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if not os.path.isfile(bin_path):
|
|
75
|
+
errors.append((bin_path, 'points to non-file "%s"' % dest))
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
map_dest = ANSIBLE_BIN_SYMLINK_MAP.get(os.path.basename(bin_path))
|
|
79
|
+
|
|
80
|
+
if not map_dest:
|
|
81
|
+
errors.append((bin_path, 'missing from ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % symlink_map_path))
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
if dest != map_dest:
|
|
85
|
+
errors.append((bin_path, 'points to "%s" instead of "%s" from ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % (dest, map_dest, symlink_map_path)))
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if not os.access(bin_path, os.X_OK):
|
|
89
|
+
errors.append((bin_path, 'points to non-executable file "%s"' % dest))
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
for bin_name, dest in ANSIBLE_BIN_SYMLINK_MAP.items():
|
|
93
|
+
if bin_name not in bin_names:
|
|
94
|
+
bin_path = os.path.join(bin_root, bin_name)
|
|
95
|
+
errors.append((bin_path, 'missing symlink to "%s" defined in ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % (dest, symlink_map_path)))
|
|
96
|
+
|
|
97
|
+
messages = [SanityMessage(message=message, path=os.path.relpath(path, data_context().content.root), confidence=100) for path, message in errors]
|
|
98
|
+
|
|
99
|
+
if errors:
|
|
100
|
+
return SanityFailure(self.name, messages=messages)
|
|
101
|
+
|
|
102
|
+
return SanitySuccess(self.name)
|
|
@@ -140,7 +140,7 @@ class ImportTest(SanityMultipleVersion):
|
|
|
140
140
|
display.warning(f'Skipping sanity test "{self.name}" on Python {python.version} due to missing virtual environment support.')
|
|
141
141
|
return SanitySkipped(self.name, python.version)
|
|
142
142
|
|
|
143
|
-
virtualenv_yaml = check_sanity_virtualenv_yaml(virtualenv_python)
|
|
143
|
+
virtualenv_yaml = args.explain or check_sanity_virtualenv_yaml(virtualenv_python)
|
|
144
144
|
|
|
145
145
|
if virtualenv_yaml is False:
|
|
146
146
|
display.warning(f'Sanity test "{self.name}" ({import_type}) on Python {python.version} may be slow due to missing libyaml support in PyYAML.')
|
|
@@ -0,0 +1,430 @@
|
|
|
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 re
|
|
9
|
+
import typing as t
|
|
10
|
+
|
|
11
|
+
from . import (
|
|
12
|
+
SanitySingleVersion,
|
|
13
|
+
SanityMessage,
|
|
14
|
+
SanityFailure,
|
|
15
|
+
SanitySuccess,
|
|
16
|
+
SanityTargets,
|
|
17
|
+
SANITY_ROOT,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ...test import (
|
|
21
|
+
TestResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from ...config import (
|
|
25
|
+
SanityConfig,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from ...target import (
|
|
29
|
+
filter_targets,
|
|
30
|
+
walk_posix_integration_targets,
|
|
31
|
+
walk_windows_integration_targets,
|
|
32
|
+
walk_integration_targets,
|
|
33
|
+
walk_module_targets,
|
|
34
|
+
CompletionTarget,
|
|
35
|
+
IntegrationTargetType,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from ..integration.cloud import (
|
|
39
|
+
get_cloud_platforms,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from ...io import (
|
|
43
|
+
read_text_file,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from ...util import (
|
|
47
|
+
display,
|
|
48
|
+
raw_command,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from ...util_common import (
|
|
52
|
+
get_docs_url,
|
|
53
|
+
write_json_test_results,
|
|
54
|
+
ResultType,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from ...host_configs import (
|
|
58
|
+
PythonConfig,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IntegrationAliasesTest(SanitySingleVersion):
|
|
63
|
+
"""Sanity test to evaluate integration test aliases."""
|
|
64
|
+
|
|
65
|
+
CI_YML = '.azure-pipelines/azure-pipelines.yml'
|
|
66
|
+
TEST_ALIAS_PREFIX = 'shippable' # this will be changed at some point in the future
|
|
67
|
+
|
|
68
|
+
DISABLED = 'disabled/'
|
|
69
|
+
UNSTABLE = 'unstable/'
|
|
70
|
+
UNSUPPORTED = 'unsupported/'
|
|
71
|
+
|
|
72
|
+
EXPLAIN_URL = get_docs_url('https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/integration-aliases.html')
|
|
73
|
+
|
|
74
|
+
TEMPLATE_DISABLED = """
|
|
75
|
+
The following integration tests are **disabled** [[explain]({explain_url}#disabled)]:
|
|
76
|
+
|
|
77
|
+
{tests}
|
|
78
|
+
|
|
79
|
+
Consider fixing the integration tests before or alongside changes.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
TEMPLATE_UNSTABLE = """
|
|
83
|
+
The following integration tests are **unstable** [[explain]({explain_url}#unstable)]:
|
|
84
|
+
|
|
85
|
+
{tests}
|
|
86
|
+
|
|
87
|
+
Tests may need to be restarted due to failures unrelated to changes.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
TEMPLATE_UNSUPPORTED = """
|
|
91
|
+
The following integration tests are **unsupported** [[explain]({explain_url}#unsupported)]:
|
|
92
|
+
|
|
93
|
+
{tests}
|
|
94
|
+
|
|
95
|
+
Consider running the tests manually or extending test infrastructure to add support.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
TEMPLATE_UNTESTED = """
|
|
99
|
+
The following modules have **no integration tests** [[explain]({explain_url}#untested)]:
|
|
100
|
+
|
|
101
|
+
{tests}
|
|
102
|
+
|
|
103
|
+
Consider adding integration tests before or alongside changes.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
ansible_only = True
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
super().__init__()
|
|
110
|
+
|
|
111
|
+
self._ci_config: dict[str, t.Any] = {}
|
|
112
|
+
self._ci_test_groups: dict[str, list[int]] = {}
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def can_ignore(self) -> bool:
|
|
116
|
+
"""True if the test supports ignore entries."""
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def no_targets(self) -> bool:
|
|
121
|
+
"""True if the test does not use test targets. Mutually exclusive with all_targets."""
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def load_ci_config(self, python: PythonConfig) -> dict[str, t.Any]:
|
|
125
|
+
"""Load and return the CI YAML configuration."""
|
|
126
|
+
if not self._ci_config:
|
|
127
|
+
self._ci_config = self.load_yaml(python, self.CI_YML)
|
|
128
|
+
|
|
129
|
+
return self._ci_config
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def ci_test_groups(self) -> dict[str, list[int]]:
|
|
133
|
+
"""Return a dictionary of CI test names and their group(s)."""
|
|
134
|
+
if not self._ci_test_groups:
|
|
135
|
+
test_groups: dict[str, set[int]] = {}
|
|
136
|
+
|
|
137
|
+
for stage in self._ci_config['stages']:
|
|
138
|
+
for job in stage['jobs']:
|
|
139
|
+
if job.get('template') != 'templates/matrix.yml':
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
parameters = job['parameters']
|
|
143
|
+
|
|
144
|
+
groups = parameters.get('groups', [])
|
|
145
|
+
test_format = parameters.get('testFormat', '{0}')
|
|
146
|
+
test_group_format = parameters.get('groupFormat', '{0}/{{1}}')
|
|
147
|
+
|
|
148
|
+
for target in parameters['targets']:
|
|
149
|
+
test = target.get('test') or target.get('name')
|
|
150
|
+
|
|
151
|
+
if groups:
|
|
152
|
+
tests_formatted = [test_group_format.format(test_format).format(test, group) for group in groups]
|
|
153
|
+
else:
|
|
154
|
+
tests_formatted = [test_format.format(test)]
|
|
155
|
+
|
|
156
|
+
for test_formatted in tests_formatted:
|
|
157
|
+
parts = test_formatted.split('/')
|
|
158
|
+
key = parts[0]
|
|
159
|
+
|
|
160
|
+
if key in ('sanity', 'units'):
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
group = int(parts[-1])
|
|
165
|
+
except ValueError:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if group < 1 or group > 99:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
group_set = test_groups.setdefault(key, set())
|
|
172
|
+
group_set.add(group)
|
|
173
|
+
|
|
174
|
+
self._ci_test_groups = dict((key, sorted(value)) for key, value in test_groups.items())
|
|
175
|
+
|
|
176
|
+
return self._ci_test_groups
|
|
177
|
+
|
|
178
|
+
def format_test_group_alias(self, name: str, fallback: str = '') -> str:
|
|
179
|
+
"""Return a test group alias using the given name and fallback."""
|
|
180
|
+
group_numbers = self.ci_test_groups.get(name, None)
|
|
181
|
+
|
|
182
|
+
if group_numbers:
|
|
183
|
+
if min(group_numbers) != 1:
|
|
184
|
+
display.warning('Min test group "%s" in %s is %d instead of 1.' % (name, self.CI_YML, min(group_numbers)), unique=True)
|
|
185
|
+
|
|
186
|
+
if max(group_numbers) != len(group_numbers):
|
|
187
|
+
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)
|
|
188
|
+
|
|
189
|
+
if max(group_numbers) > 9:
|
|
190
|
+
alias = '%s/%s/group(%s)/' % (self.TEST_ALIAS_PREFIX, name, '|'.join(str(i) for i in range(min(group_numbers), max(group_numbers) + 1)))
|
|
191
|
+
elif len(group_numbers) > 1:
|
|
192
|
+
alias = '%s/%s/group[%d-%d]/' % (self.TEST_ALIAS_PREFIX, name, min(group_numbers), max(group_numbers))
|
|
193
|
+
else:
|
|
194
|
+
alias = '%s/%s/group%d/' % (self.TEST_ALIAS_PREFIX, name, min(group_numbers))
|
|
195
|
+
elif fallback:
|
|
196
|
+
alias = '%s/%s/group%d/' % (self.TEST_ALIAS_PREFIX, fallback, 1)
|
|
197
|
+
else:
|
|
198
|
+
raise Exception('cannot find test group "%s" in %s' % (name, self.CI_YML))
|
|
199
|
+
|
|
200
|
+
return alias
|
|
201
|
+
|
|
202
|
+
def load_yaml(self, python: PythonConfig, path: str) -> dict[str, t.Any]:
|
|
203
|
+
"""Load the specified YAML file and return the contents."""
|
|
204
|
+
yaml_to_json_path = os.path.join(SANITY_ROOT, self.name, 'yaml_to_json.py')
|
|
205
|
+
return json.loads(raw_command([python.path, yaml_to_json_path], data=read_text_file(path), capture=True)[0])
|
|
206
|
+
|
|
207
|
+
def test(self, args: SanityConfig, targets: SanityTargets, python: PythonConfig) -> TestResult:
|
|
208
|
+
if args.explain:
|
|
209
|
+
return SanitySuccess(self.name)
|
|
210
|
+
|
|
211
|
+
if not os.path.isfile(self.CI_YML):
|
|
212
|
+
return SanityFailure(self.name, messages=[SanityMessage(
|
|
213
|
+
message='file missing',
|
|
214
|
+
path=self.CI_YML,
|
|
215
|
+
)])
|
|
216
|
+
|
|
217
|
+
results = Results(
|
|
218
|
+
comments=[],
|
|
219
|
+
labels={},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self.load_ci_config(python)
|
|
223
|
+
self.check_changes(args, results)
|
|
224
|
+
|
|
225
|
+
write_json_test_results(ResultType.BOT, 'data-sanity-ci.json', results.__dict__)
|
|
226
|
+
|
|
227
|
+
messages = []
|
|
228
|
+
|
|
229
|
+
messages += self.check_posix_targets(args)
|
|
230
|
+
messages += self.check_windows_targets()
|
|
231
|
+
|
|
232
|
+
if messages:
|
|
233
|
+
return SanityFailure(self.name, messages=messages)
|
|
234
|
+
|
|
235
|
+
return SanitySuccess(self.name)
|
|
236
|
+
|
|
237
|
+
def check_posix_targets(self, args: SanityConfig) -> list[SanityMessage]:
|
|
238
|
+
"""Check POSIX integration test targets and return messages with any issues found."""
|
|
239
|
+
posix_targets = tuple(walk_posix_integration_targets())
|
|
240
|
+
|
|
241
|
+
clouds = get_cloud_platforms(args, posix_targets)
|
|
242
|
+
cloud_targets = ['cloud/%s/' % cloud for cloud in clouds]
|
|
243
|
+
|
|
244
|
+
all_cloud_targets = tuple(filter_targets(posix_targets, ['cloud/'], errors=False))
|
|
245
|
+
invalid_cloud_targets = tuple(filter_targets(all_cloud_targets, cloud_targets, include=False, errors=False))
|
|
246
|
+
|
|
247
|
+
messages = []
|
|
248
|
+
|
|
249
|
+
for target in invalid_cloud_targets:
|
|
250
|
+
for alias in target.aliases:
|
|
251
|
+
if alias.startswith('cloud/') and alias != 'cloud/':
|
|
252
|
+
if any(alias.startswith(cloud_target) for cloud_target in cloud_targets):
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
messages.append(SanityMessage('invalid alias `%s`' % alias, '%s/aliases' % target.path))
|
|
256
|
+
|
|
257
|
+
messages += self.check_ci_group(
|
|
258
|
+
targets=tuple(filter_targets(posix_targets, ['cloud/', '%s/generic/' % self.TEST_ALIAS_PREFIX], include=False, errors=False)),
|
|
259
|
+
find=self.format_test_group_alias('linux').replace('linux', 'posix'),
|
|
260
|
+
find_incidental=['%s/posix/incidental/' % self.TEST_ALIAS_PREFIX],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
messages += self.check_ci_group(
|
|
264
|
+
targets=tuple(filter_targets(posix_targets, ['%s/generic/' % self.TEST_ALIAS_PREFIX], errors=False)),
|
|
265
|
+
find=self.format_test_group_alias('generic'),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
for cloud in clouds:
|
|
269
|
+
if cloud == 'httptester':
|
|
270
|
+
find = self.format_test_group_alias('linux').replace('linux', 'posix')
|
|
271
|
+
find_incidental = ['%s/posix/incidental/' % self.TEST_ALIAS_PREFIX]
|
|
272
|
+
else:
|
|
273
|
+
find = self.format_test_group_alias(cloud, 'generic')
|
|
274
|
+
find_incidental = ['%s/%s/incidental/' % (self.TEST_ALIAS_PREFIX, cloud), '%s/cloud/incidental/' % self.TEST_ALIAS_PREFIX]
|
|
275
|
+
|
|
276
|
+
messages += self.check_ci_group(
|
|
277
|
+
targets=tuple(filter_targets(posix_targets, ['cloud/%s/' % cloud], errors=False)),
|
|
278
|
+
find=find,
|
|
279
|
+
find_incidental=find_incidental,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
target_type_groups = {
|
|
283
|
+
IntegrationTargetType.TARGET: (1, 2),
|
|
284
|
+
IntegrationTargetType.CONTROLLER: (3, 4, 5),
|
|
285
|
+
IntegrationTargetType.CONFLICT: (),
|
|
286
|
+
IntegrationTargetType.UNKNOWN: (),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for target in posix_targets:
|
|
290
|
+
if target.name == 'ansible-test-container':
|
|
291
|
+
continue # special test target which uses group 6 -- nothing else should be in that group
|
|
292
|
+
|
|
293
|
+
if f'{self.TEST_ALIAS_PREFIX}/posix/' not in target.aliases:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
found_groups = [alias for alias in target.aliases if re.search(f'^{self.TEST_ALIAS_PREFIX}/posix/group[0-9]+/$', alias)]
|
|
297
|
+
expected_groups = [f'{self.TEST_ALIAS_PREFIX}/posix/group{group}/' for group in target_type_groups[target.target_type]]
|
|
298
|
+
valid_groups = [group for group in found_groups if group in expected_groups]
|
|
299
|
+
invalid_groups = [group for group in found_groups if not any(group.startswith(expected_group) for expected_group in expected_groups)]
|
|
300
|
+
|
|
301
|
+
if not valid_groups:
|
|
302
|
+
messages.append(SanityMessage(f'Target of type {target.target_type.name} must be in at least one of these groups: {", ".join(expected_groups)}',
|
|
303
|
+
f'{target.path}/aliases'))
|
|
304
|
+
|
|
305
|
+
if invalid_groups:
|
|
306
|
+
messages.append(SanityMessage(f'Target of type {target.target_type.name} cannot be in these groups: {", ".join(invalid_groups)}',
|
|
307
|
+
f'{target.path}/aliases'))
|
|
308
|
+
|
|
309
|
+
return messages
|
|
310
|
+
|
|
311
|
+
def check_windows_targets(self) -> list[SanityMessage]:
|
|
312
|
+
"""Check Windows integration test targets and return messages with any issues found."""
|
|
313
|
+
windows_targets = tuple(walk_windows_integration_targets())
|
|
314
|
+
|
|
315
|
+
messages = []
|
|
316
|
+
|
|
317
|
+
messages += self.check_ci_group(
|
|
318
|
+
targets=windows_targets,
|
|
319
|
+
find=self.format_test_group_alias('windows'),
|
|
320
|
+
find_incidental=['%s/windows/incidental/' % self.TEST_ALIAS_PREFIX],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return messages
|
|
324
|
+
|
|
325
|
+
def check_ci_group(
|
|
326
|
+
self,
|
|
327
|
+
targets: tuple[CompletionTarget, ...],
|
|
328
|
+
find: str,
|
|
329
|
+
find_incidental: t.Optional[list[str]] = None,
|
|
330
|
+
) -> list[SanityMessage]:
|
|
331
|
+
"""Check the CI groups set in the provided targets and return a list of messages with any issues found."""
|
|
332
|
+
all_paths = set(target.path for target in targets)
|
|
333
|
+
supported_paths = set(target.path for target in filter_targets(targets, [find], errors=False))
|
|
334
|
+
unsupported_paths = set(target.path for target in filter_targets(targets, [self.UNSUPPORTED], errors=False))
|
|
335
|
+
|
|
336
|
+
if find_incidental:
|
|
337
|
+
incidental_paths = set(target.path for target in filter_targets(targets, find_incidental, errors=False))
|
|
338
|
+
else:
|
|
339
|
+
incidental_paths = set()
|
|
340
|
+
|
|
341
|
+
unassigned_paths = all_paths - supported_paths - unsupported_paths - incidental_paths
|
|
342
|
+
conflicting_paths = supported_paths & unsupported_paths
|
|
343
|
+
|
|
344
|
+
unassigned_message = 'missing alias `%s` or `%s`' % (find.strip('/'), self.UNSUPPORTED.strip('/'))
|
|
345
|
+
conflicting_message = 'conflicting alias `%s` and `%s`' % (find.strip('/'), self.UNSUPPORTED.strip('/'))
|
|
346
|
+
|
|
347
|
+
messages = []
|
|
348
|
+
|
|
349
|
+
for path in unassigned_paths:
|
|
350
|
+
if path == 'test/integration/targets/ansible-test-container':
|
|
351
|
+
continue # special test target which uses group 6 -- nothing else should be in that group
|
|
352
|
+
|
|
353
|
+
messages.append(SanityMessage(unassigned_message, '%s/aliases' % path))
|
|
354
|
+
|
|
355
|
+
for path in conflicting_paths:
|
|
356
|
+
messages.append(SanityMessage(conflicting_message, '%s/aliases' % path))
|
|
357
|
+
|
|
358
|
+
return messages
|
|
359
|
+
|
|
360
|
+
def check_changes(self, args: SanityConfig, results: Results) -> None:
|
|
361
|
+
"""Check changes and store results in the provided result dictionary."""
|
|
362
|
+
integration_targets = list(walk_integration_targets())
|
|
363
|
+
module_targets = list(walk_module_targets())
|
|
364
|
+
|
|
365
|
+
integration_targets_by_name = dict((target.name, target) for target in integration_targets)
|
|
366
|
+
module_names_by_path = dict((target.path, target.module) for target in module_targets)
|
|
367
|
+
|
|
368
|
+
disabled_targets = []
|
|
369
|
+
unstable_targets = []
|
|
370
|
+
unsupported_targets = []
|
|
371
|
+
|
|
372
|
+
for command in [command for command in args.metadata.change_description.focused_command_targets if 'integration' in command]:
|
|
373
|
+
for target in args.metadata.change_description.focused_command_targets[command]:
|
|
374
|
+
if self.DISABLED in integration_targets_by_name[target].aliases:
|
|
375
|
+
disabled_targets.append(target)
|
|
376
|
+
elif self.UNSTABLE in integration_targets_by_name[target].aliases:
|
|
377
|
+
unstable_targets.append(target)
|
|
378
|
+
elif self.UNSUPPORTED in integration_targets_by_name[target].aliases:
|
|
379
|
+
unsupported_targets.append(target)
|
|
380
|
+
|
|
381
|
+
untested_modules = []
|
|
382
|
+
|
|
383
|
+
for path in args.metadata.change_description.no_integration_paths:
|
|
384
|
+
module = module_names_by_path.get(path)
|
|
385
|
+
|
|
386
|
+
if module:
|
|
387
|
+
untested_modules.append(module)
|
|
388
|
+
|
|
389
|
+
comments = [
|
|
390
|
+
self.format_comment(self.TEMPLATE_DISABLED, disabled_targets),
|
|
391
|
+
self.format_comment(self.TEMPLATE_UNSTABLE, unstable_targets),
|
|
392
|
+
self.format_comment(self.TEMPLATE_UNSUPPORTED, unsupported_targets),
|
|
393
|
+
self.format_comment(self.TEMPLATE_UNTESTED, untested_modules),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
comments = [comment for comment in comments if comment]
|
|
397
|
+
|
|
398
|
+
labels = dict(
|
|
399
|
+
needs_tests=bool(untested_modules),
|
|
400
|
+
disabled_tests=bool(disabled_targets),
|
|
401
|
+
unstable_tests=bool(unstable_targets),
|
|
402
|
+
unsupported_tests=bool(unsupported_targets),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
results.comments += comments
|
|
406
|
+
results.labels.update(labels)
|
|
407
|
+
|
|
408
|
+
def format_comment(self, template: str, targets: list[str]) -> t.Optional[str]:
|
|
409
|
+
"""Format and return a comment based on the given template and targets, or None if there are no targets."""
|
|
410
|
+
if not targets:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
tests = '\n'.join('- %s' % target for target in targets)
|
|
414
|
+
|
|
415
|
+
data = dict(
|
|
416
|
+
explain_url=self.EXPLAIN_URL,
|
|
417
|
+
tests=tests,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
message = textwrap.dedent(template).strip().format(**data)
|
|
421
|
+
|
|
422
|
+
return message
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@dataclasses.dataclass
|
|
426
|
+
class Results:
|
|
427
|
+
"""Check results."""
|
|
428
|
+
|
|
429
|
+
comments: list[str]
|
|
430
|
+
labels: dict[str, bool]
|
|
@@ -73,6 +73,7 @@ class MypyTest(SanityMultipleVersion):
|
|
|
73
73
|
"""Return the given list of test targets, filtered to include only those relevant for the test."""
|
|
74
74
|
return [target for target in targets if os.path.splitext(target.path)[1] == '.py' and target.path not in self.vendored_paths and (
|
|
75
75
|
target.path.startswith('lib/ansible/') or target.path.startswith('test/lib/ansible_test/_internal/')
|
|
76
|
+
or target.path.startswith('packaging/cli-doc/')
|
|
76
77
|
or target.path.startswith('test/lib/ansible_test/_util/target/sanity/import/'))]
|
|
77
78
|
|
|
78
79
|
@property
|
|
@@ -116,6 +117,7 @@ class MypyTest(SanityMultipleVersion):
|
|
|
116
117
|
MyPyContext('ansible-test', ['test/lib/ansible_test/_internal/'], controller_python_versions),
|
|
117
118
|
MyPyContext('ansible-core', ['lib/ansible/'], controller_python_versions),
|
|
118
119
|
MyPyContext('modules', ['lib/ansible/modules/', 'lib/ansible/module_utils/'], remote_only_python_versions),
|
|
120
|
+
MyPyContext('packaging', ['packaging/cli-doc/'], controller_python_versions),
|
|
119
121
|
)
|
|
120
122
|
|
|
121
123
|
unfiltered_messages: list[SanityMessage] = []
|
|
@@ -168,6 +170,9 @@ class MypyTest(SanityMultipleVersion):
|
|
|
168
170
|
# However, it will also report issues on those files, which is not the desired behavior.
|
|
169
171
|
messages = [message for message in messages if message.path in paths_set]
|
|
170
172
|
|
|
173
|
+
if args.explain:
|
|
174
|
+
return SanitySuccess(self.name, python_version=python.version)
|
|
175
|
+
|
|
171
176
|
results = settings.process_errors(messages, paths)
|
|
172
177
|
|
|
173
178
|
if results:
|
|
@@ -250,7 +255,7 @@ class MypyTest(SanityMultipleVersion):
|
|
|
250
255
|
|
|
251
256
|
pattern = r'^(?P<path>[^:]*):(?P<line>[0-9]+):((?P<column>[0-9]+):)? (?P<level>[^:]+): (?P<message>.*)$'
|
|
252
257
|
|
|
253
|
-
parsed = parse_to_list_of_dict(pattern, stdout)
|
|
258
|
+
parsed = parse_to_list_of_dict(pattern, stdout or '')
|
|
254
259
|
|
|
255
260
|
messages = [SanityMessage(
|
|
256
261
|
level=r['level'],
|
|
@@ -20,7 +20,7 @@ class AnsibleLayout(LayoutProvider):
|
|
|
20
20
|
@staticmethod
|
|
21
21
|
def is_content_root(path: 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: str, paths: list[str]) -> ContentLayout:
|
|
26
26
|
"""Create a Layout using the given root and paths."""
|
|
@@ -251,6 +251,13 @@ def collect_requirements(
|
|
|
251
251
|
# installed packages may have run-time dependencies on setuptools
|
|
252
252
|
uninstall_packages.remove('setuptools')
|
|
253
253
|
|
|
254
|
+
# hack to allow the package-data sanity test to keep wheel in the venv
|
|
255
|
+
install_commands = [command for command in commands if isinstance(command, PipInstall)]
|
|
256
|
+
install_wheel = any(install.has_package('wheel') for install in install_commands)
|
|
257
|
+
|
|
258
|
+
if install_wheel:
|
|
259
|
+
uninstall_packages.remove('wheel')
|
|
260
|
+
|
|
254
261
|
commands.extend(collect_uninstall(packages=uninstall_packages))
|
|
255
262
|
|
|
256
263
|
return commands
|
ansible_test/_internal/util.py
CHANGED
|
@@ -438,7 +438,7 @@ def raw_command(
|
|
|
438
438
|
display.info(f'{description}: {escaped_cmd}', verbosity=cmd_verbosity, truncate=True)
|
|
439
439
|
display.info('Working directory: %s' % cwd, verbosity=2)
|
|
440
440
|
|
|
441
|
-
program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required=
|
|
441
|
+
program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required=False)
|
|
442
442
|
|
|
443
443
|
if program:
|
|
444
444
|
display.info('Program found: %s' % program, verbosity=2)
|
|
@@ -498,9 +498,14 @@ def run_command(
|
|
|
498
498
|
)
|
|
499
499
|
|
|
500
500
|
|
|
501
|
-
def yamlcheck(python: PythonConfig) -> t.Optional[bool]:
|
|
501
|
+
def yamlcheck(python: PythonConfig, explain: bool = False) -> t.Optional[bool]:
|
|
502
502
|
"""Return True if PyYAML has libyaml support, False if it does not and None if it was not found."""
|
|
503
|
-
|
|
503
|
+
stdout = raw_command([python.path, os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'yamlcheck.py')], capture=True, explain=explain)[0]
|
|
504
|
+
|
|
505
|
+
if explain:
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
result = json.loads(stdout)
|
|
504
509
|
|
|
505
510
|
if not result['yaml']:
|
|
506
511
|
return None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# IMPORTANT
|
|
2
|
+
# Set "ignore_missing_imports" per package below, rather than globally.
|
|
3
|
+
# That will help identify missing type stubs that should be added to the sanity test environment.
|
|
4
|
+
|
|
5
|
+
[mypy]
|
|
6
|
+
|
|
7
|
+
[mypy-docutils]
|
|
8
|
+
ignore_missing_imports = True
|
|
9
|
+
|
|
10
|
+
[mypy-docutils.core]
|
|
11
|
+
ignore_missing_imports = True
|
|
12
|
+
|
|
13
|
+
[mypy-docutils.writers]
|
|
14
|
+
ignore_missing_imports = True
|
|
15
|
+
|
|
16
|
+
[mypy-docutils.writers.manpage]
|
|
17
|
+
ignore_missing_imports = True
|
|
18
|
+
|
|
19
|
+
[mypy-argcomplete]
|
|
20
|
+
ignore_missing_imports = True
|
|
@@ -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)
|