Matrix Docker Ansible eploy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

247 lines
11 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. """Regression tests for the Molecule CI matrix; no Docker or Python packages needed."""
  5. import contextlib
  6. import importlib.util
  7. import io
  8. import json
  9. import os
  10. from pathlib import Path
  11. import subprocess
  12. import sys
  13. import tempfile
  14. import unittest
  15. # Importing the script should not leave __pycache__ in the repository.
  16. sys.dont_write_bytecode = True
  17. SCRIPT = Path(__file__).with_name("molecule-select-roles.py")
  18. SPEC = importlib.util.spec_from_file_location("selector", SCRIPT)
  19. selector = importlib.util.module_from_spec(SPEC)
  20. SPEC.loader.exec_module(selector)
  21. class SelectionTests(unittest.TestCase):
  22. def setUp(self):
  23. self.temp = tempfile.TemporaryDirectory()
  24. self.addCleanup(self.temp.cleanup)
  25. self.root = Path(self.temp.name)
  26. self.git = selector.Git(self.root)
  27. self.git.run("init", "--quiet", "--initial-branch=main")
  28. self.git.run("config", "user.name", "Molecule selector test")
  29. self.git.run("config", "user.email", "test@example.com")
  30. self.git.run("config", "commit.gpgsign", "false")
  31. self.roles = ["matrix-database", "matrix-livekit", "matrix-web"]
  32. self.write(selector.IMAGE_VARS, '\n'.join([
  33. '---',
  34. 'molecule_shared_image_curl: "curl:1"',
  35. 'molecule_shared_image_postgres: "postgres:1"',
  36. 'molecule_shared_image_livekit: "livekit:1"',
  37. ]) + '\n')
  38. self.write("molecule-shared/requirements.yml", "roles: []\n")
  39. self.write("molecule-shared/tasks/postgres.yml", '{{ molecule_shared_image_postgres }}\n')
  40. self.write("molecule-shared/tasks/probe.yml", "molecule-shared/probe.py\n")
  41. self.write("molecule-shared/probe.py", "print('probe')\n")
  42. for role in self.roles:
  43. self.write(f"roles/custom/{role}/molecule/default/molecule.yml", "---\n")
  44. self.scenario(role, "verify.yml", '{{ molecule_shared_image_curl }}\n')
  45. self.scenario(role, "prepare.yml", "molecule-shared/vars.yml\n")
  46. self.path(f"roles/custom/{role}/molecule/default/requirements.yml").symlink_to(
  47. "../../../../../molecule-shared/requirements.yml"
  48. )
  49. self.scenario("matrix-database", "prepare.yml", "molecule-shared/vars.yml\nmolecule-shared/tasks/postgres.yml\n")
  50. self.scenario("matrix-livekit", "tasks/sfu.yml", '{{ molecule_shared_image_livekit }}\n')
  51. self.scenario("matrix-web", "tasks/probe.yml", "molecule-shared/tasks/probe.yml\n")
  52. self.base = self.commit()
  53. self.git.run("update-ref", "refs/remotes/origin/main", self.base)
  54. def path(self, name):
  55. return self.root / name
  56. def write(self, name, content):
  57. path = self.path(name)
  58. path.parent.mkdir(parents=True, exist_ok=True)
  59. path.write_text(content)
  60. def scenario(self, role, name, content):
  61. self.write(f"roles/custom/{role}/molecule/default/{name}", content)
  62. def commit(self):
  63. self.git.run("add", "--all")
  64. self.git.run("commit", "--quiet", "--allow-empty", "-m", "Test fixture")
  65. return self.git.commit("HEAD")
  66. def bump(self, image):
  67. path = self.path(selector.IMAGE_VARS)
  68. path.write_text(path.read_text().replace(f'"{image}:1"', f'"{image}:2"'))
  69. def selected(self, base=None):
  70. self.commit()
  71. with contextlib.redirect_stderr(io.StringIO()):
  72. return selector.select_roles(self.git, base or self.base)
  73. def test_livekit_bump_only_runs_its_consumer(self):
  74. self.bump("livekit")
  75. self.assertEqual(self.selected(), ["matrix-livekit"])
  76. def test_postgres_bump_follows_shared_task(self):
  77. self.bump("postgres")
  78. self.assertEqual(self.selected(), ["matrix-database"])
  79. def test_curl_bump_runs_every_consumer(self):
  80. self.bump("curl")
  81. self.assertEqual(self.selected(), self.roles)
  82. def test_multiple_image_bumps_union_with_direct_role_changes(self):
  83. self.bump("livekit")
  84. self.bump("postgres")
  85. self.write("roles/custom/matrix-web/defaults/main.yml", "version: 2\n")
  86. self.assertEqual(self.selected(), self.roles)
  87. def test_direct_changes_ignore_roles_without_scenarios(self):
  88. self.write("roles/custom/matrix-web/defaults/main.yml", "version: 2\n")
  89. self.write("roles/custom/matrix-untested/defaults/main.yml", "version: 2\n")
  90. self.assertEqual(self.selected(), ["matrix-web"])
  91. def test_documentation_change_runs_nothing(self):
  92. self.write("docs/example.md", "Documentation\n")
  93. self.assertEqual(self.selected(), [])
  94. def test_image_pin_comments_and_quoting_do_not_run_scenarios(self):
  95. path = self.path(selector.IMAGE_VARS)
  96. path.write_text("# New comment\n" + path.read_text().replace('"', "'"))
  97. self.assertEqual(self.selected(), [])
  98. def test_shared_task_change_only_runs_consumers(self):
  99. self.write("molecule-shared/tasks/postgres.yml", '{{ molecule_shared_image_postgres }}\n# Changed\n')
  100. self.assertEqual(self.selected(), ["matrix-database"])
  101. def test_shared_fixture_change_follows_nested_references(self):
  102. self.write("molecule-shared/probe.py", "print('updated probe')\n")
  103. self.assertEqual(self.selected(), ["matrix-web"])
  104. def test_deleted_helper_uses_previous_consumers(self):
  105. self.path("molecule-shared/probe.py").unlink()
  106. self.write("molecule-shared/tasks/probe.yml", "# Probe removed\n")
  107. self.assertEqual(self.selected(), ["matrix-web"])
  108. def test_renamed_helper_unions_previous_and_current_consumers(self):
  109. self.path("molecule-shared/probe.py").rename(self.path("molecule-shared/new-probe.py"))
  110. self.write("molecule-shared/tasks/probe.yml", "# Probe moved\n")
  111. self.scenario("matrix-livekit", "tasks/new-probe.yml", "molecule-shared/new-probe.py\n")
  112. self.assertEqual(self.selected(), ["matrix-livekit", "matrix-web"])
  113. def test_deleted_role_is_not_selected(self):
  114. self.path("roles/custom/matrix-livekit/molecule/default/molecule.yml").unlink()
  115. self.assertEqual(self.selected(), [])
  116. def test_new_scenario_is_selected(self):
  117. self.write("roles/custom/matrix-new/molecule/default/molecule.yml", "---\n")
  118. self.assertEqual(self.selected(), ["matrix-new"])
  119. def test_global_changes_run_all(self):
  120. for name in sorted(selector.GLOBAL_FILES):
  121. with self.subTest(name=name):
  122. self.git.run("reset", "--hard", self.base)
  123. self.write(name, "# Infrastructure changed\n")
  124. self.assertEqual(self.selected(), self.roles)
  125. def test_unknown_shared_file_runs_all(self):
  126. self.write("molecule-shared/new-helper.yml", "---\n")
  127. self.assertEqual(self.selected(), self.roles)
  128. def test_unconsumed_image_bump_runs_all(self):
  129. self.scenario("matrix-livekit", "tasks/sfu.yml", "# No image reference\n")
  130. self.base = self.commit()
  131. self.bump("livekit")
  132. self.assertEqual(self.selected(), self.roles)
  133. def test_unsupported_pins_run_all(self):
  134. original = self.path(selector.IMAGE_VARS).read_text()
  135. for content in [
  136. '---\n' + original,
  137. original + 'other_setting: true\n',
  138. original + 'molecule_shared_image_new: "new:1"\n',
  139. original.replace('molecule_shared_image_livekit: "livekit:1"\n', ''),
  140. original + 'molecule_shared_image_curl: "curl:2"\n',
  141. original.replace('"livekit:1"', '"{{ another_variable }}"'),
  142. ]:
  143. with self.subTest(content=content):
  144. self.write(selector.IMAGE_VARS, content)
  145. self.assertEqual(self.selected(), self.roles)
  146. def test_missing_pins_run_all(self):
  147. self.path(selector.IMAGE_VARS).unlink()
  148. self.assertEqual(self.selected(), self.roles)
  149. def test_unresolved_references_run_all(self):
  150. for reference in [
  151. 'molecule-shared/tasks/{{ helper }}.yml',
  152. 'molecule-shared/{{ helper }}.yml',
  153. 'molecule-shared/missing.yml',
  154. "{{ lookup('vars', 'molecule_shared_image_' + name) }}",
  155. ]:
  156. with self.subTest(reference=reference):
  157. self.scenario("matrix-web", "tasks/dynamic.yml", reference)
  158. self.bump("livekit")
  159. self.assertEqual(self.selected(), self.roles)
  160. def test_shared_reference_cycles_terminate(self):
  161. self.write("molecule-shared/tasks/postgres.yml", 'molecule-shared/tasks/postgres.yml\n{{ molecule_shared_image_postgres }}\n')
  162. self.assertEqual(self.selected(), ["matrix-database"])
  163. def test_comments_do_not_introduce_unresolved_references(self):
  164. self.scenario("matrix-web", "tasks/comments.yml", "# See molecule-shared/probe.py.\n# Helpers live in molecule-shared/.\n")
  165. self.base = self.commit()
  166. self.bump("livekit")
  167. self.assertEqual(self.selected(), ["matrix-livekit"])
  168. def test_symlinked_shared_helper_is_followed(self):
  169. self.path("molecule-shared/tasks/probe.yml").unlink()
  170. self.path("molecule-shared/tasks/probe.yml").symlink_to("../probe.py")
  171. self.base = self.commit()
  172. self.write("molecule-shared/probe.py", "print('changed')\n")
  173. self.assertEqual(self.selected(), ["matrix-web"])
  174. def test_invalid_comparison_runs_all(self):
  175. self.assertEqual(self.selected(base="missing-commit"), self.roles)
  176. def test_manual_dispatch_all_or_one_role(self):
  177. with contextlib.redirect_stderr(io.StringIO()):
  178. self.assertEqual(selector.select_roles(self.git, None), self.roles)
  179. self.assertEqual(selector.select_roles(self.git, None, role="matrix-web"), ["matrix-web"])
  180. with self.assertRaises(ValueError):
  181. selector.select_roles(self.git, None, role="../outside")
  182. def test_event_comparison_bases(self):
  183. self.bump("livekit")
  184. head = self.commit()
  185. push = {"EVENT_NAME": "push", "DEFAULT_BRANCH": "main", "GITHUB_REF": "refs/heads/renovate/test"}
  186. for before in [self.base, "0" * 40, "unavailable", ""]:
  187. with self.subTest(before=before):
  188. self.assertEqual(selector.comparison_base(self.git, head, dict(push, BEFORE_SHA=before)), self.base)
  189. self.assertEqual(selector.comparison_base(self.git, head, {"EVENT_NAME": "pull_request", "BASE_SHA": self.base}), self.base)
  190. self.assertIsNone(selector.comparison_base(self.git, head, {"EVENT_NAME": "workflow_dispatch"}))
  191. self.assertIsNone(selector.comparison_base(self.git, head, dict(push, GITHUB_REF="refs/heads/main")))
  192. self.assertIsNone(selector.comparison_base(self.git, head, dict(push, DEFAULT_BRANCH="missing")))
  193. self.assertIsNone(selector.comparison_base(self.git, head, {"EVENT_NAME": "pull_request", "BASE_SHA": "missing"}))
  194. def test_cli_writes_github_output(self):
  195. self.bump("livekit")
  196. self.commit()
  197. output = self.path("github-output")
  198. output.write_text("previous=value\n")
  199. env = dict(os.environ, INPUT_ROLE="", GITHUB_OUTPUT=str(output))
  200. result = subprocess.run(
  201. [sys.executable, str(SCRIPT), "--base", self.base], cwd=self.root,
  202. env=env, capture_output=True, text=True, check=True,
  203. )
  204. self.assertEqual(json.loads(result.stdout), ["matrix-livekit"])
  205. self.assertEqual(output.read_text(), 'previous=value\nroles=["matrix-livekit"]\n')
  206. if __name__ == "__main__":
  207. unittest.main()