antsibull-nox 0.1.0__py3-none-any.whl → 0.2.0__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.
antsibull_nox/sessions.py CHANGED
@@ -10,16 +10,24 @@ Create nox sessions.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import json
13
14
  import os
14
15
  import shlex
15
16
  import subprocess
16
17
  import sys
17
18
  import typing as t
19
+ from contextlib import contextmanager
18
20
  from dataclasses import asdict, dataclass
19
21
  from pathlib import Path
20
22
 
21
23
  import nox
22
24
 
25
+ from .ansible import (
26
+ AnsibleCoreVersion,
27
+ get_ansible_core_info,
28
+ get_ansible_core_package_name,
29
+ get_supported_core_versions,
30
+ )
23
31
  from .collection import (
24
32
  CollectionData,
25
33
  force_collection_version,
@@ -30,17 +38,21 @@ from .collection import (
30
38
  from .data_util import prepare_data_script
31
39
  from .paths import (
32
40
  copy_collection,
41
+ copy_directory_tree_into,
33
42
  create_temp_directory,
34
43
  filter_paths,
35
44
  find_data_directory,
36
45
  list_all_files,
37
46
  remove_path,
38
47
  )
48
+ from .python import get_installed_python_versions
49
+ from .utils import Version
39
50
 
40
51
  # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
41
52
  # https://docs.gitlab.com/ci/variables/predefined_variables/#predefined-variables
42
53
  # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
43
54
  IN_CI = os.environ.get("CI") == "true"
55
+ IN_GITHUB_ACTIONS = bool(os.environ.get("GITHUB_ACTION"))
44
56
  ALLOW_EDITABLE = os.environ.get("ALLOW_EDITABLE", str(not IN_CI)).lower() in (
45
57
  "1",
46
58
  "true",
@@ -60,6 +72,28 @@ MODULE_PATHS = [
60
72
  "tests/unit/plugins/module_utils/",
61
73
  ]
62
74
 
75
+ _SESSIONS: dict[str, list[dict[str, t.Any]]] = {}
76
+
77
+
78
+ @contextmanager
79
+ def _ci_group(name: str) -> t.Iterator[None]:
80
+ """
81
+ Try to ensure that the output inside the context is printed in a collapsable group.
82
+
83
+ This is highly CI system dependent, and currently only works for GitHub Actions.
84
+ """
85
+ if IN_GITHUB_ACTIONS:
86
+ print(f"::group::{name}")
87
+ yield
88
+ if IN_GITHUB_ACTIONS:
89
+ print("::endgroup::")
90
+
91
+
92
+ def _register(name: str, data: dict[str, t.Any]) -> None:
93
+ if name not in _SESSIONS:
94
+ _SESSIONS[name] = []
95
+ _SESSIONS[name].append(data)
96
+
63
97
 
64
98
  def install(session: nox.Session, *args: str, editable: bool = False, **kwargs):
65
99
  """
@@ -167,6 +201,7 @@ def prepare_collections(
167
201
  extra_deps_files=extra_deps_files,
168
202
  extra_collections=extra_collections,
169
203
  with_current=False,
204
+ global_cache_dir=session.cache_dir,
170
205
  )
171
206
  current_setup = setup_current_tree(place, setup.current_collection)
172
207
  return CollectionSetup(
@@ -179,26 +214,84 @@ def prepare_collections(
179
214
 
180
215
 
181
216
  def _run_bare_script(
182
- session: nox.Session, /, name: str, *, extra_data: dict[str, t.Any] | None = None
217
+ session: nox.Session,
218
+ /,
219
+ name: str,
220
+ *,
221
+ use_session_python: bool = False,
222
+ files: list[Path] | None = None,
223
+ extra_data: dict[str, t.Any] | None = None,
183
224
  ) -> None:
184
- files = list_all_files()
225
+ if files is None:
226
+ files = list_all_files()
185
227
  data = prepare_data_script(
186
228
  session,
187
229
  base_name=name,
188
230
  paths=files,
189
231
  extra_data=extra_data,
190
232
  )
233
+ python = sys.executable
234
+ env = {}
235
+ if use_session_python:
236
+ python = "python"
237
+ env["PYTHONPATH"] = str(find_data_directory())
191
238
  session.run(
192
- sys.executable,
239
+ python,
193
240
  find_data_directory() / f"{name}.py",
194
241
  "--data",
195
242
  data,
196
243
  external=True,
244
+ env=env,
197
245
  )
198
246
 
199
247
 
248
+ def _compose_description(
249
+ *,
250
+ prefix: str | dict[t.Literal["one", "other"], str] | None = None,
251
+ programs: dict[str, str | bool | None],
252
+ ) -> str:
253
+ parts: list[str] = []
254
+
255
+ def add(text: str, *, comma: bool = False) -> None:
256
+ if parts:
257
+ if comma:
258
+ parts.append(", ")
259
+ else:
260
+ parts.append(" ")
261
+ parts.append(text)
262
+
263
+ active_programs = [
264
+ (program, value if isinstance(value, str) else None)
265
+ for program, value in programs.items()
266
+ if value not in (False, None)
267
+ ]
268
+
269
+ if prefix:
270
+ if isinstance(prefix, dict):
271
+ if len(active_programs) == 1 and "one" in prefix:
272
+ add(prefix["one"])
273
+ else:
274
+ add(prefix["other"])
275
+ else:
276
+ add(prefix)
277
+
278
+ for index, (program, value) in enumerate(active_programs):
279
+ if index + 1 == len(active_programs) and index > 0:
280
+ add("and", comma=index > 1)
281
+ add(program, comma=index > 0 and index + 1 < len(active_programs))
282
+ if value is not None:
283
+ add(f"({value})")
284
+
285
+ return "".join(parts)
286
+
287
+
200
288
  def add_lint(
201
- *, make_lint_default: bool, has_formatters: bool, has_codeqa: bool, has_typing: bool
289
+ *,
290
+ make_lint_default: bool,
291
+ has_formatters: bool,
292
+ has_codeqa: bool,
293
+ has_yamllint: bool,
294
+ has_typing: bool,
202
295
  ) -> None:
203
296
  """
204
297
  Add nox meta session for linting.
@@ -212,11 +305,28 @@ def add_lint(
212
305
  dependent_sessions.append("formatters")
213
306
  if has_codeqa:
214
307
  dependent_sessions.append("codeqa")
308
+ if has_yamllint:
309
+ dependent_sessions.append("yamllint")
215
310
  if has_typing:
216
311
  dependent_sessions.append("typing")
217
- nox.session( # type: ignore
218
- lint, name="lint", default=make_lint_default, requires=dependent_sessions
312
+
313
+ lint.__doc__ = _compose_description(
314
+ prefix={
315
+ "one": "Meta session for triggering the following session:",
316
+ "other": "Meta session for triggering the following sessions:",
317
+ },
318
+ programs={
319
+ "formatters": has_formatters,
320
+ "codeqa": has_codeqa,
321
+ "yamllint": has_yamllint,
322
+ "typing": has_typing,
323
+ },
219
324
  )
325
+ nox.session(
326
+ name="lint",
327
+ default=make_lint_default,
328
+ requires=dependent_sessions,
329
+ )(lint)
220
330
 
221
331
 
222
332
  def add_formatters(
@@ -299,7 +409,17 @@ def add_formatters(
299
409
  if run_black or run_black_modules:
300
410
  execute_black(session)
301
411
 
302
- nox.session(formatters, name="formatters", default=False) # type: ignore
412
+ formatters.__doc__ = _compose_description(
413
+ prefix={
414
+ "one": "Run code formatter:",
415
+ "other": "Run code formatters:",
416
+ },
417
+ programs={
418
+ "isort": run_isort,
419
+ "black": run_black,
420
+ },
421
+ )
422
+ nox.session(name="formatters", default=False)(formatters)
303
423
 
304
424
 
305
425
  def add_codeqa( # noqa: C901
@@ -412,7 +532,120 @@ def add_codeqa( # noqa: C901
412
532
  if run_pylint and prepared_collections:
413
533
  execute_pylint(session, prepared_collections)
414
534
 
415
- nox.session(codeqa, name="codeqa", default=False) # type: ignore
535
+ codeqa.__doc__ = _compose_description(
536
+ prefix={
537
+ "other": "Run code QA:",
538
+ },
539
+ programs={
540
+ "flake8": run_flake8,
541
+ "pylint": run_pylint,
542
+ },
543
+ )
544
+ nox.session(name="codeqa", default=False)(codeqa)
545
+
546
+
547
+ def add_yamllint(
548
+ *,
549
+ run_yamllint: bool,
550
+ yamllint_config: str | os.PathLike | None,
551
+ yamllint_config_plugins: str | os.PathLike | None,
552
+ yamllint_config_plugins_examples: str | os.PathLike | None,
553
+ yamllint_package: str,
554
+ ) -> None:
555
+ """
556
+ Add yamllint session for linting YAML files and plugin/module docs.
557
+ """
558
+
559
+ def compose_dependencies() -> list[str]:
560
+ deps = []
561
+ if run_yamllint:
562
+ deps.append(yamllint_package)
563
+ return deps
564
+
565
+ def to_str(config: str | os.PathLike | None) -> str | None:
566
+ return str(config) if config else None
567
+
568
+ def execute_yamllint(session: nox.Session) -> None:
569
+ # Run yamllint
570
+ all_files = list_all_files()
571
+ cwd = Path.cwd()
572
+ all_yaml_filenames = [
573
+ str(file.relative_to(cwd))
574
+ for file in all_files
575
+ if file.name.lower().endswith((".yml", ".yaml"))
576
+ ]
577
+ if not all_yaml_filenames:
578
+ session.warn("Skipping yamllint since no YAML file was found...")
579
+ return
580
+
581
+ command = ["yamllint"]
582
+ if yamllint_config is not None:
583
+ command.extend(
584
+ [
585
+ "-c",
586
+ str(yamllint_config),
587
+ ]
588
+ )
589
+ command.append("--strict")
590
+ command.append("--")
591
+ command.extend(all_yaml_filenames)
592
+ command.extend(session.posargs)
593
+ session.run(*command)
594
+
595
+ def execute_plugin_yamllint(session: nox.Session) -> None:
596
+ # Run yamllint
597
+ all_files = list_all_files()
598
+ cwd = Path.cwd()
599
+ plugins_dir = cwd / "plugins"
600
+ ignore_dirs = [
601
+ plugins_dir / "action",
602
+ plugins_dir / "module_utils",
603
+ plugins_dir / "plugin_utils",
604
+ ]
605
+ all_plugin_files = [
606
+ file
607
+ for file in all_files
608
+ if file.is_relative_to(plugins_dir)
609
+ and file.name.lower().endswith((".py", ".yml", ".yaml"))
610
+ and not any(file.is_relative_to(dir) for dir in ignore_dirs)
611
+ ]
612
+ if not all_plugin_files:
613
+ session.warn(
614
+ "Skipping yamllint for modules/plugins since"
615
+ " no appropriate Python file was found..."
616
+ )
617
+ return
618
+ _run_bare_script(
619
+ session,
620
+ "plugin-yamllint",
621
+ use_session_python=True,
622
+ files=all_plugin_files,
623
+ extra_data={
624
+ "config": to_str(yamllint_config_plugins or yamllint_config),
625
+ "config_examples": to_str(
626
+ yamllint_config_plugins_examples
627
+ or yamllint_config_plugins
628
+ or yamllint_config
629
+ ),
630
+ },
631
+ )
632
+
633
+ def yamllint(session: nox.Session) -> None:
634
+ install(session, *compose_dependencies())
635
+ if run_yamllint:
636
+ execute_yamllint(session)
637
+ execute_plugin_yamllint(session)
638
+
639
+ yamllint.__doc__ = _compose_description(
640
+ prefix={
641
+ "one": "Run YAML checker:",
642
+ "other": "Run YAML checkers:",
643
+ },
644
+ programs={
645
+ "yamllint": run_yamllint,
646
+ },
647
+ )
648
+ nox.session(name="yamllint", default=False)(yamllint)
416
649
 
417
650
 
418
651
  def add_typing(
@@ -479,7 +712,16 @@ def add_typing(
479
712
  if run_mypy and prepared_collections:
480
713
  execute_mypy(session, prepared_collections)
481
714
 
482
- nox.session(typing, name="typing", default=False) # type: ignore
715
+ typing.__doc__ = _compose_description(
716
+ prefix={
717
+ "one": "Run type checker:",
718
+ "other": "Run type checkers:",
719
+ },
720
+ programs={
721
+ "mypy": run_mypy,
722
+ },
723
+ )
724
+ nox.session(name="typing", default=False)(typing)
483
725
 
484
726
 
485
727
  def add_lint_sessions(
@@ -506,6 +748,12 @@ def add_lint_sessions(
506
748
  pylint_package: str = "pylint",
507
749
  pylint_ansible_core_package: str | None = "ansible-core",
508
750
  pylint_extra_deps: list[str] | None = None,
751
+ # yamllint:
752
+ run_yamllint: bool = False,
753
+ yamllint_config: str | os.PathLike | None = None,
754
+ yamllint_config_plugins: str | os.PathLike | None = None,
755
+ yamllint_config_plugins_examples: str | os.PathLike | None = None,
756
+ yamllint_package: str = "yamllint",
509
757
  # mypy:
510
758
  run_mypy: bool = True,
511
759
  mypy_config: str | os.PathLike | None = None,
@@ -518,11 +766,13 @@ def add_lint_sessions(
518
766
  """
519
767
  has_formatters = run_isort or run_black or run_black_modules or False
520
768
  has_codeqa = run_flake8 or run_pylint
769
+ has_yamllint = run_yamllint
521
770
  has_typing = run_mypy
522
771
 
523
772
  add_lint(
524
773
  has_formatters=has_formatters,
525
774
  has_codeqa=has_codeqa,
775
+ has_yamllint=has_yamllint,
526
776
  has_typing=has_typing,
527
777
  make_lint_default=make_lint_default,
528
778
  )
@@ -553,6 +803,15 @@ def add_lint_sessions(
553
803
  pylint_extra_deps=pylint_extra_deps or [],
554
804
  )
555
805
 
806
+ if has_yamllint:
807
+ add_yamllint(
808
+ run_yamllint=run_yamllint,
809
+ yamllint_config=yamllint_config,
810
+ yamllint_config_plugins=yamllint_config_plugins,
811
+ yamllint_config_plugins_examples=yamllint_config_plugins_examples,
812
+ yamllint_package=yamllint_package,
813
+ )
814
+
556
815
  if has_typing:
557
816
  add_typing(
558
817
  extra_code_files=extra_code_files or [],
@@ -571,7 +830,7 @@ def add_docs_check(
571
830
  ansible_core_package: str = "ansible-core",
572
831
  validate_collection_refs: t.Literal["self", "dependent", "all"] | None = None,
573
832
  extra_collections: list[str] | None = None,
574
- ):
833
+ ) -> None:
575
834
  """
576
835
  Add docs-check session for linting.
577
836
  """
@@ -609,9 +868,11 @@ def add_docs_check(
609
868
  if prepared_collections:
610
869
  execute_antsibull_docs(session, prepared_collections)
611
870
 
612
- nox.session( # type: ignore
613
- docs_check, name="docs-check", default=make_docs_check_default
614
- )
871
+ docs_check.__doc__ = "Run 'antsibull-docs lint-collection-docs'"
872
+ nox.session(
873
+ name="docs-check",
874
+ default=make_docs_check_default,
875
+ )(docs_check)
615
876
 
616
877
 
617
878
  def add_license_check(
@@ -621,7 +882,7 @@ def add_license_check(
621
882
  reuse_package: str = "reuse",
622
883
  run_license_check: bool = True,
623
884
  license_check_extra_ignore_paths: list[str] | None = None,
624
- ):
885
+ ) -> None:
625
886
  """
626
887
  Add license-check session for license checks.
627
888
  """
@@ -645,9 +906,22 @@ def add_license_check(
645
906
  },
646
907
  )
647
908
 
648
- nox.session( # type: ignore
649
- license_check, name="license-check", default=make_license_check_default
909
+ license_check.__doc__ = _compose_description(
910
+ prefix={
911
+ "one": "Run license checker:",
912
+ "other": "Run license checkers:",
913
+ },
914
+ programs={
915
+ "reuse": run_reuse,
916
+ "license-check": (
917
+ "ensure GPLv3+ for plugins" if run_license_check else False
918
+ ),
919
+ },
650
920
  )
921
+ nox.session(
922
+ name="license-check",
923
+ default=make_license_check_default,
924
+ )(license_check)
651
925
 
652
926
 
653
927
  @dataclass
@@ -688,7 +962,7 @@ def add_extra_checks(
688
962
  # action-groups:
689
963
  run_action_groups: bool = False,
690
964
  action_groups_config: list[ActionGroup] | None = None,
691
- ):
965
+ ) -> None:
692
966
  """
693
967
  Add extra-checks session for extra checks.
694
968
  """
@@ -730,12 +1004,25 @@ def add_extra_checks(
730
1004
  if run_action_groups:
731
1005
  execute_action_groups(session)
732
1006
 
733
- nox.session( # type: ignore
734
- extra_checks,
1007
+ extra_checks.__doc__ = _compose_description(
1008
+ prefix={
1009
+ "one": "Run extra checker:",
1010
+ "other": "Run extra checkers:",
1011
+ },
1012
+ programs={
1013
+ "no-unwanted-files": (
1014
+ "checks for unwanted files in plugins/"
1015
+ if run_no_unwanted_files
1016
+ else False
1017
+ ),
1018
+ "action-groups": "validate action groups" if run_action_groups else False,
1019
+ },
1020
+ )
1021
+ nox.session(
735
1022
  name="extra-checks",
736
1023
  python=False,
737
1024
  default=make_extra_checks_default,
738
- )
1025
+ )(extra_checks)
739
1026
 
740
1027
 
741
1028
  def add_build_import_check(
@@ -745,9 +1032,9 @@ def add_build_import_check(
745
1032
  run_galaxy_importer: bool = True,
746
1033
  galaxy_importer_package: str = "galaxy-importer",
747
1034
  galaxy_importer_config_path: (
748
- str | None
1035
+ str | os.PathLike | None
749
1036
  ) = None, # https://github.com/ansible/galaxy-importer#configuration
750
- ):
1037
+ ) -> None:
751
1038
  """
752
1039
  Add license-check session for license checks.
753
1040
  """
@@ -793,7 +1080,7 @@ def add_build_import_check(
793
1080
  env = {}
794
1081
  if galaxy_importer_config_path:
795
1082
  env["GALAXY_IMPORTER_CONFIG"] = str(
796
- Path.cwd() / galaxy_importer_config_path
1083
+ Path(galaxy_importer_config_path).absolute()
797
1084
  )
798
1085
  with session.chdir(collection_dir):
799
1086
  import_log = (
@@ -808,7 +1095,8 @@ def add_build_import_check(
808
1095
  or ""
809
1096
  )
810
1097
  if import_log:
811
- print(import_log)
1098
+ with _ci_group("Run Galaxy importer"):
1099
+ print(import_log)
812
1100
  error_prefix = "ERROR:"
813
1101
  errors = []
814
1102
  for line in import_log.splitlines():
@@ -821,13 +1109,589 @@ def add_build_import_check(
821
1109
  f" error{'' if len(errors) == 1 else 's'}:\n{messages}"
822
1110
  )
823
1111
 
824
- nox.session( # type: ignore
825
- build_import_check,
1112
+ build_import_check.__doc__ = _compose_description(
1113
+ prefix={
1114
+ "one": "Run build and import checker:",
1115
+ "other": "Run build and import checkers:",
1116
+ },
1117
+ programs={
1118
+ "build-collection": True,
1119
+ "galaxy-importer": (
1120
+ "test whether Galaxy will import built collection"
1121
+ if run_galaxy_importer
1122
+ else False
1123
+ ),
1124
+ },
1125
+ )
1126
+ nox.session(
826
1127
  name="build-import-check",
827
1128
  default=make_build_import_check_default,
1129
+ )(build_import_check)
1130
+
1131
+
1132
+ def _parse_ansible_core_version(
1133
+ version: str | AnsibleCoreVersion,
1134
+ ) -> AnsibleCoreVersion:
1135
+ if version in ("devel", "milestone"):
1136
+ # For some reason mypy doesn't notice that
1137
+ return t.cast(AnsibleCoreVersion, version)
1138
+ if isinstance(version, Version):
1139
+ return version
1140
+ return Version.parse(version)
1141
+
1142
+
1143
+ def add_ansible_test_session(
1144
+ *,
1145
+ name: str,
1146
+ description: str | None,
1147
+ extra_deps_files: list[str | os.PathLike] | None = None,
1148
+ ansible_test_params: list[str],
1149
+ add_posargs: bool = True,
1150
+ default: bool,
1151
+ ansible_core_version: str | AnsibleCoreVersion,
1152
+ ansible_core_source: t.Literal["git", "pypi"] = "git",
1153
+ ansible_core_repo_name: str | None = None,
1154
+ ansible_core_branch_name: str | None = None,
1155
+ handle_coverage: t.Literal["never", "always", "auto"] = "auto",
1156
+ register_name: str | None = None,
1157
+ register_extra_data: dict[str, t.Any] | None = None,
1158
+ ) -> None:
1159
+ """
1160
+ Add generic ansible-test session.
1161
+
1162
+ Returns a list of Python versions set for this session.
1163
+ """
1164
+ parsed_ansible_core_version = _parse_ansible_core_version(ansible_core_version)
1165
+
1166
+ def compose_dependencies() -> list[str]:
1167
+ deps = [
1168
+ get_ansible_core_package_name(
1169
+ parsed_ansible_core_version,
1170
+ source=ansible_core_source,
1171
+ ansible_repo=ansible_core_repo_name,
1172
+ branch_name=ansible_core_branch_name,
1173
+ )
1174
+ ]
1175
+ return deps
1176
+
1177
+ def run_ansible_test(session: nox.Session) -> None:
1178
+ install(session, *compose_dependencies())
1179
+ prepared_collections = prepare_collections(
1180
+ session,
1181
+ install_in_site_packages=False,
1182
+ extra_deps_files=extra_deps_files,
1183
+ install_out_of_tree=True,
1184
+ )
1185
+ if not prepared_collections:
1186
+ session.warn("Skipping ansible-test...")
1187
+ return
1188
+ cwd = Path.cwd()
1189
+ with session.chdir(prepared_collections.current_path):
1190
+ command = ["ansible-test"] + ansible_test_params
1191
+ if add_posargs and session.posargs:
1192
+ command.extend(session.posargs)
1193
+ session.run(*command)
1194
+
1195
+ coverage = (handle_coverage == "auto" and "--coverage" in command) or (
1196
+ handle_coverage == "always"
1197
+ )
1198
+ if coverage:
1199
+ session.run(
1200
+ "ansible-test",
1201
+ "coverage",
1202
+ "xml",
1203
+ "--color",
1204
+ "-v",
1205
+ "--requirements",
1206
+ "--group-by",
1207
+ "command",
1208
+ "--group-by",
1209
+ "version",
1210
+ )
1211
+
1212
+ copy_directory_tree_into(
1213
+ prepared_collections.current_path / "tests" / "output",
1214
+ cwd / "tests" / "output",
1215
+ )
1216
+
1217
+ # Determine Python version(s)
1218
+ core_info = get_ansible_core_info(parsed_ansible_core_version)
1219
+ all_versions = get_installed_python_versions()
1220
+
1221
+ installed_versions = [
1222
+ version
1223
+ for version in core_info.controller_python_versions
1224
+ if version in all_versions
1225
+ ]
1226
+ python = max(installed_versions or core_info.controller_python_versions)
1227
+ python_versions = [python]
1228
+
1229
+ run_ansible_test.__doc__ = description
1230
+ nox.session(
1231
+ name=name,
1232
+ default=default,
1233
+ python=[str(python_version) for python_version in python_versions],
1234
+ )(run_ansible_test)
1235
+
1236
+ if register_name:
1237
+ data = {
1238
+ "name": name,
1239
+ "ansible-core": (
1240
+ str(ansible_core_branch_name)
1241
+ if ansible_core_branch_name is not None
1242
+ else str(parsed_ansible_core_version)
1243
+ ),
1244
+ "python": " ".join(str(python) for python in python_versions),
1245
+ }
1246
+ if register_extra_data:
1247
+ data.update(register_extra_data)
1248
+ _register(register_name, data)
1249
+
1250
+
1251
+ def add_ansible_test_sanity_test_session(
1252
+ *,
1253
+ name: str,
1254
+ description: str | None,
1255
+ default: bool,
1256
+ ansible_core_version: str | AnsibleCoreVersion,
1257
+ ansible_core_source: t.Literal["git", "pypi"] = "git",
1258
+ ansible_core_repo_name: str | None = None,
1259
+ ansible_core_branch_name: str | None = None,
1260
+ ) -> None:
1261
+ """
1262
+ Add generic ansible-test sanity test session.
1263
+ """
1264
+ add_ansible_test_session(
1265
+ name=name,
1266
+ description=description,
1267
+ ansible_test_params=["sanity", "--docker", "-v", "--color"],
1268
+ default=default,
1269
+ ansible_core_version=ansible_core_version,
1270
+ ansible_core_source=ansible_core_source,
1271
+ ansible_core_repo_name=ansible_core_repo_name,
1272
+ ansible_core_branch_name=ansible_core_branch_name,
1273
+ register_name="sanity",
828
1274
  )
829
1275
 
830
1276
 
1277
+ def _parse_min_max_except(
1278
+ min_version: Version | str | None,
1279
+ max_version: Version | str | None,
1280
+ except_versions: list[AnsibleCoreVersion | str] | None,
1281
+ ) -> tuple[Version | None, Version | None, tuple[AnsibleCoreVersion, ...] | None]:
1282
+ if isinstance(min_version, str):
1283
+ min_version = Version.parse(min_version)
1284
+ if isinstance(max_version, str):
1285
+ max_version = Version.parse(max_version)
1286
+ if except_versions is None:
1287
+ return min_version, max_version, None
1288
+ evs = tuple(_parse_ansible_core_version(version) for version in except_versions)
1289
+ return min_version, max_version, evs
1290
+
1291
+
1292
+ def add_all_ansible_test_sanity_test_sessions(
1293
+ *,
1294
+ default: bool = False,
1295
+ include_devel: bool = False,
1296
+ include_milestone: bool = False,
1297
+ add_devel_like_branches: list[tuple[str | None, str]] | None = None,
1298
+ min_version: Version | str | None = None,
1299
+ max_version: Version | str | None = None,
1300
+ except_versions: list[AnsibleCoreVersion | str] | None = None,
1301
+ ) -> None:
1302
+ """
1303
+ Add ansible-test sanity test meta session that runs ansible-test sanity
1304
+ for all supported ansible-core versions.
1305
+ """
1306
+ parsed_min_version, parsed_max_version, parsed_except_versions = (
1307
+ _parse_min_max_except(min_version, max_version, except_versions)
1308
+ )
1309
+
1310
+ sanity_sessions = []
1311
+ for ansible_core_version in get_supported_core_versions(
1312
+ include_devel=include_devel,
1313
+ include_milestone=include_milestone,
1314
+ min_version=parsed_min_version,
1315
+ max_version=parsed_max_version,
1316
+ except_versions=parsed_except_versions,
1317
+ ):
1318
+ name = f"ansible-test-sanity-{ansible_core_version}"
1319
+ add_ansible_test_sanity_test_session(
1320
+ name=name,
1321
+ description=f"Run sanity tests from ansible-core {ansible_core_version}'s ansible-test",
1322
+ ansible_core_version=ansible_core_version,
1323
+ default=False,
1324
+ )
1325
+ sanity_sessions.append(name)
1326
+ if add_devel_like_branches:
1327
+ for repo_name, branch_name in add_devel_like_branches:
1328
+ repo_prefix = (
1329
+ f"{repo_name.replace('/', '-')}-" if repo_name is not None else ""
1330
+ )
1331
+ repo_postfix = f", {repo_name} repository" if repo_name is not None else ""
1332
+ name = f"ansible-test-sanity-{repo_prefix}{branch_name.replace('/', '-')}"
1333
+ add_ansible_test_sanity_test_session(
1334
+ name=name,
1335
+ description=(
1336
+ "Run sanity tests from ansible-test in ansible-core's"
1337
+ f" {branch_name} branch{repo_postfix}"
1338
+ ),
1339
+ ansible_core_version="devel",
1340
+ ansible_core_repo_name=repo_name,
1341
+ ansible_core_branch_name=branch_name,
1342
+ default=False,
1343
+ )
1344
+ sanity_sessions.append(name)
1345
+
1346
+ def run_all_sanity_tests(
1347
+ session: nox.Session, # pylint: disable=unused-argument
1348
+ ) -> None:
1349
+ pass
1350
+
1351
+ run_all_sanity_tests.__doc__ = (
1352
+ "Meta session for running all ansible-test-sanity-* sessions."
1353
+ )
1354
+ nox.session(
1355
+ name="ansible-test-sanity",
1356
+ default=default,
1357
+ requires=sanity_sessions,
1358
+ )(run_all_sanity_tests)
1359
+
1360
+
1361
+ def add_ansible_test_unit_test_session(
1362
+ *,
1363
+ name: str,
1364
+ description: str | None,
1365
+ default: bool,
1366
+ ansible_core_version: str | AnsibleCoreVersion,
1367
+ ansible_core_source: t.Literal["git", "pypi"] = "git",
1368
+ ansible_core_repo_name: str | None = None,
1369
+ ansible_core_branch_name: str | None = None,
1370
+ ) -> None:
1371
+ """
1372
+ Add generic ansible-test unit test session.
1373
+ """
1374
+ add_ansible_test_session(
1375
+ name=name,
1376
+ description=description,
1377
+ ansible_test_params=["units", "--docker", "-v", "--color"],
1378
+ extra_deps_files=["tests/unit/requirements.yml"],
1379
+ default=default,
1380
+ ansible_core_version=ansible_core_version,
1381
+ ansible_core_source=ansible_core_source,
1382
+ ansible_core_repo_name=ansible_core_repo_name,
1383
+ ansible_core_branch_name=ansible_core_branch_name,
1384
+ register_name="units",
1385
+ )
1386
+
1387
+
1388
+ def add_all_ansible_test_unit_test_sessions(
1389
+ *,
1390
+ default: bool = False,
1391
+ include_devel: bool = False,
1392
+ include_milestone: bool = False,
1393
+ add_devel_like_branches: list[tuple[str | None, str]] | None = None,
1394
+ min_version: Version | str | None = None,
1395
+ max_version: Version | str | None = None,
1396
+ except_versions: list[AnsibleCoreVersion | str] | None = None,
1397
+ ) -> None:
1398
+ """
1399
+ Add ansible-test unit test meta session that runs ansible-test units
1400
+ for all supported ansible-core versions.
1401
+ """
1402
+ parsed_min_version, parsed_max_version, parsed_except_versions = (
1403
+ _parse_min_max_except(min_version, max_version, except_versions)
1404
+ )
1405
+
1406
+ units_sessions = []
1407
+ for ansible_core_version in get_supported_core_versions(
1408
+ include_devel=include_devel,
1409
+ include_milestone=include_milestone,
1410
+ min_version=parsed_min_version,
1411
+ max_version=parsed_max_version,
1412
+ except_versions=parsed_except_versions,
1413
+ ):
1414
+ name = f"ansible-test-units-{ansible_core_version}"
1415
+ add_ansible_test_unit_test_session(
1416
+ name=name,
1417
+ description=f"Run unit tests with ansible-core {ansible_core_version}'s ansible-test",
1418
+ ansible_core_version=ansible_core_version,
1419
+ default=False,
1420
+ )
1421
+ units_sessions.append(name)
1422
+ if add_devel_like_branches:
1423
+ for repo_name, branch_name in add_devel_like_branches:
1424
+ repo_prefix = (
1425
+ f"{repo_name.replace('/', '-')}-" if repo_name is not None else ""
1426
+ )
1427
+ repo_postfix = f", {repo_name} repository" if repo_name is not None else ""
1428
+ name = f"ansible-test-units-{repo_prefix}{branch_name.replace('/', '-')}"
1429
+ add_ansible_test_unit_test_session(
1430
+ name=name,
1431
+ description=(
1432
+ "Run unit tests from ansible-test in ansible-core's"
1433
+ f" {branch_name} branch{repo_postfix}"
1434
+ ),
1435
+ ansible_core_version="devel",
1436
+ ansible_core_repo_name=repo_name,
1437
+ ansible_core_branch_name=branch_name,
1438
+ default=False,
1439
+ )
1440
+ units_sessions.append(name)
1441
+
1442
+ def run_all_unit_tests(
1443
+ session: nox.Session, # pylint: disable=unused-argument
1444
+ ) -> None:
1445
+ pass
1446
+
1447
+ run_all_unit_tests.__doc__ = (
1448
+ "Meta session for running all ansible-test-units-* sessions."
1449
+ )
1450
+ nox.session(
1451
+ name="ansible-test-units",
1452
+ default=default,
1453
+ requires=units_sessions,
1454
+ )(run_all_unit_tests)
1455
+
1456
+
1457
+ def add_ansible_test_integration_sessions_default_container(
1458
+ *,
1459
+ include_devel: bool = False,
1460
+ include_milestone: bool = False,
1461
+ add_devel_like_branches: list[tuple[str | None, str]] | None = None,
1462
+ min_version: Version | str | None = None,
1463
+ max_version: Version | str | None = None,
1464
+ except_versions: list[AnsibleCoreVersion | str] | None = None,
1465
+ core_python_versions: (
1466
+ dict[str | AnsibleCoreVersion, list[str | Version]] | None
1467
+ ) = None,
1468
+ controller_python_versions_only: bool = False,
1469
+ default: bool = False,
1470
+ ) -> None:
1471
+ """
1472
+ Add ansible-test integration tests using the default Docker container.
1473
+
1474
+ ``core_python_versions`` can be used to restrict the Python versions
1475
+ to be used for a specific ansible-core version.
1476
+
1477
+ ``controller_python_versions_only`` can be used to only run against
1478
+ controller Python versions.
1479
+ """
1480
+
1481
+ def add_integration_tests(
1482
+ ansible_core_version: AnsibleCoreVersion,
1483
+ repo_name: str | None = None,
1484
+ branch_name: str | None = None,
1485
+ ) -> list[str]:
1486
+ # Determine Python versions to run tests for
1487
+ py_versions = (
1488
+ (core_python_versions.get(branch_name) if branch_name is not None else None)
1489
+ or core_python_versions.get(ansible_core_version)
1490
+ or core_python_versions.get(str(ansible_core_version))
1491
+ if core_python_versions
1492
+ else None
1493
+ )
1494
+ if py_versions is None:
1495
+ core_info = get_ansible_core_info(ansible_core_version)
1496
+ py_versions = list(
1497
+ core_info.controller_python_versions
1498
+ if controller_python_versions_only
1499
+ else core_info.remote_python_versions
1500
+ )
1501
+
1502
+ # Add sessions
1503
+ integration_sessions_core: list[str] = []
1504
+ if branch_name is None:
1505
+ base_name = f"ansible-test-integration-{ansible_core_version}-"
1506
+ else:
1507
+ repo_prefix = (
1508
+ f"{repo_name.replace('/', '-')}-" if repo_name is not None else ""
1509
+ )
1510
+ base_name = f"ansible-test-integration-{repo_prefix}{branch_name.replace('/', '-')}-"
1511
+ for py_version in py_versions:
1512
+ name = f"{base_name}{py_version}"
1513
+ if branch_name is None:
1514
+ description = (
1515
+ f"Run integration tests from ansible-core {ansible_core_version}'s"
1516
+ f" ansible-test with Python {py_version}"
1517
+ )
1518
+ else:
1519
+ repo_postfix = (
1520
+ f", {repo_name} repository" if repo_name is not None else ""
1521
+ )
1522
+ description = (
1523
+ f"Run integration tests from ansible-test in ansible-core's {branch_name}"
1524
+ f" branch{repo_postfix} with Python {py_version}"
1525
+ )
1526
+ add_ansible_test_session(
1527
+ name=name,
1528
+ description=description,
1529
+ ansible_test_params=[
1530
+ "integration",
1531
+ "--docker",
1532
+ "default",
1533
+ "-v",
1534
+ "--color",
1535
+ "--python",
1536
+ str(py_version),
1537
+ ],
1538
+ extra_deps_files=["tests/integration/requirements.yml"],
1539
+ ansible_core_version=ansible_core_version,
1540
+ ansible_core_repo_name=repo_name,
1541
+ ansible_core_branch_name=branch_name,
1542
+ default=False,
1543
+ register_name="integration",
1544
+ register_extra_data={
1545
+ "test-container": "default",
1546
+ "test-python": str(py_version),
1547
+ },
1548
+ )
1549
+ integration_sessions_core.append(name)
1550
+ return integration_sessions_core
1551
+
1552
+ parsed_min_version, parsed_max_version, parsed_except_versions = (
1553
+ _parse_min_max_except(min_version, max_version, except_versions)
1554
+ )
1555
+ integration_sessions: list[str] = []
1556
+ for ansible_core_version in get_supported_core_versions(
1557
+ include_devel=include_devel,
1558
+ include_milestone=include_milestone,
1559
+ min_version=parsed_min_version,
1560
+ max_version=parsed_max_version,
1561
+ except_versions=parsed_except_versions,
1562
+ ):
1563
+ integration_sessions_core = add_integration_tests(ansible_core_version)
1564
+ if integration_sessions_core:
1565
+ name = f"ansible-test-integration-{ansible_core_version}"
1566
+ integration_sessions.append(name)
1567
+
1568
+ def run_integration_tests(
1569
+ session: nox.Session, # pylint: disable=unused-argument
1570
+ ) -> None:
1571
+ pass
1572
+
1573
+ run_integration_tests.__doc__ = (
1574
+ f"Meta session for running all {name}-* sessions."
1575
+ )
1576
+ nox.session(
1577
+ name=name,
1578
+ requires=integration_sessions_core,
1579
+ default=False,
1580
+ )(run_integration_tests)
1581
+ if add_devel_like_branches:
1582
+ for repo_name, branch_name in add_devel_like_branches:
1583
+ integration_sessions_core = add_integration_tests(
1584
+ "devel", repo_name=repo_name, branch_name=branch_name
1585
+ )
1586
+ if integration_sessions_core:
1587
+ repo_prefix = (
1588
+ f"{repo_name.replace('/', '-')}-" if repo_name is not None else ""
1589
+ )
1590
+ name = f"ansible-test-integration-{repo_prefix}{branch_name.replace('/', '-')}"
1591
+ integration_sessions.append(name)
1592
+
1593
+ def run_integration_tests_for_branch(
1594
+ session: nox.Session, # pylint: disable=unused-argument
1595
+ ) -> None:
1596
+ pass
1597
+
1598
+ run_integration_tests_for_branch.__doc__ = (
1599
+ f"Meta session for running all {name}-* sessions."
1600
+ )
1601
+ nox.session(
1602
+ name=name,
1603
+ requires=integration_sessions_core,
1604
+ default=False,
1605
+ )(run_integration_tests_for_branch)
1606
+
1607
+ def ansible_test_integration(
1608
+ session: nox.Session, # pylint: disable=unused-argument
1609
+ ) -> None:
1610
+ pass
1611
+
1612
+ ansible_test_integration.__doc__ = (
1613
+ "Meta session for running all ansible-test-integration-* sessions."
1614
+ )
1615
+ nox.session(
1616
+ name="ansible-test-integration",
1617
+ requires=integration_sessions,
1618
+ default=default,
1619
+ )(ansible_test_integration)
1620
+
1621
+
1622
+ def add_ansible_lint(
1623
+ *,
1624
+ make_ansible_lint_default: bool = True,
1625
+ ansible_lint_package: str = "ansible-lint",
1626
+ strict: bool = False,
1627
+ ) -> None:
1628
+ """
1629
+ Add a session that runs ansible-lint.
1630
+ """
1631
+
1632
+ def compose_dependencies() -> list[str]:
1633
+ return [ansible_lint_package]
1634
+
1635
+ def ansible_lint(session: nox.Session) -> None:
1636
+ install(session, *compose_dependencies())
1637
+ prepared_collections = prepare_collections(
1638
+ session,
1639
+ install_in_site_packages=False,
1640
+ install_out_of_tree=True,
1641
+ extra_deps_files=["tests/integration/requirements.yml"],
1642
+ )
1643
+ if not prepared_collections:
1644
+ session.warn("Skipping ansible-lint...")
1645
+ return
1646
+ env = {"ANSIBLE_COLLECTIONS_PATH": f"{prepared_collections.current_place}"}
1647
+ command = ["ansible-lint", "--offline"]
1648
+ if strict:
1649
+ command.append("--strict")
1650
+ session.run(*command, env=env)
1651
+
1652
+ ansible_lint.__doc__ = "Run ansible-lint."
1653
+ nox.session(
1654
+ name="ansible-lint",
1655
+ default=make_ansible_lint_default,
1656
+ )(ansible_lint)
1657
+
1658
+
1659
+ def add_matrix_generator() -> None:
1660
+ """
1661
+ Add a session that generates matrixes for CI systems.
1662
+ """
1663
+
1664
+ def matrix_generator(
1665
+ session: nox.Session, # pylint: disable=unused-argument
1666
+ ) -> None:
1667
+ json_output = os.environ.get("ANTSIBULL_NOX_MATRIX_JSON")
1668
+ if json_output:
1669
+ print(f"Writing JSON output to {json_output}...")
1670
+ with open(json_output, "wt", encoding="utf-8") as f:
1671
+ f.write(json.dumps(_SESSIONS))
1672
+
1673
+ github_output = os.environ.get("GITHUB_OUTPUT")
1674
+ if github_output:
1675
+ print(f"Writing GitHub output to {github_output}...")
1676
+ with open(github_output, "at", encoding="utf-8") as f:
1677
+ for name, sessions in _SESSIONS.items():
1678
+ f.write(f"{name}={json.dumps(sessions)}\n")
1679
+
1680
+ for name, sessions in sorted(_SESSIONS.items()):
1681
+ print(f"{name} ({len(sessions)}):")
1682
+ for session_data in sessions:
1683
+ data = session_data.copy()
1684
+ session_name = data.pop("name")
1685
+ print(f" {session_name}: {data}")
1686
+
1687
+ matrix_generator.__doc__ = "Generate matrix for CI systems."
1688
+ nox.session(
1689
+ name="matrix-generator",
1690
+ python=False,
1691
+ default=False,
1692
+ )(matrix_generator)
1693
+
1694
+
831
1695
  __all__ = [
832
1696
  "ActionGroup",
833
1697
  "add_build_import_check",
@@ -835,6 +1699,14 @@ __all__ = [
835
1699
  "add_extra_checks",
836
1700
  "add_license_check",
837
1701
  "add_lint_sessions",
1702
+ "add_ansible_test_session",
1703
+ "add_ansible_test_sanity_test_session",
1704
+ "add_all_ansible_test_sanity_test_sessions",
1705
+ "add_ansible_test_unit_test_session",
1706
+ "add_all_ansible_test_unit_test_sessions",
1707
+ "add_ansible_test_integration_sessions_default_container",
1708
+ "add_ansible_lint",
1709
+ "add_matrix_generator",
838
1710
  "install",
839
1711
  "prepare_collections",
840
1712
  ]