Skip to content
Bitcoin Roots

v30.3-roots.1 · Commit 22: [PATCH 22/68] ci: consolidate sanitizer and apt workflows

ci/test/test_sanitizer_workflows.py

Before: /dev/null · After: ci/test/test_sanitizer_workflows.py

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..c6f659b0d22a994f98c4f24cb3ed83db15716c68

Download original patch · Release notes

Diff rows 1–99 of 99. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

ci/test/test_sanitizer_workflows.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Regression contracts for shared PR and nightly sanitizer execution."""
3
+
4
+ import importlib.util
5
+ import json
6
+ from pathlib import Path
7
+ import re
8
+ import unittest
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+ CI_WORKFLOW = ROOT / ".github/workflows/ci.yml"
13
+ NIGHTLY_WORKFLOW = ROOT / ".github/workflows/nightly.yml"
14
+ REUSABLE_WORKFLOW = ROOT / ".github/workflows/reusable-sanitizer.yml"
15
+ CLASSIFIER = ROOT / "ci/change-classifier.py"
16
+ POLICY = ROOT / "ci/change-classifier-policy.json"
17
+
18
+
19
+ def job(workflow, name):
20
+ text = workflow.read_text(encoding="utf-8")
21
+ match = re.search(rf"(?ms)^ {re.escape(name)}:\n(.*?)(?=^ [a-z0-9_-]+:\n|\Z)", text)
22
+ if match is None:
23
+ raise AssertionError(f"Job {name!r} does not exist in {workflow}")
24
+ return match.group(1)
25
+
26
+
27
+ class SanitizerWorkflowTest(unittest.TestCase):
28
+ def test_reusable_workflow_owns_the_sanitizer_runner(self):
29
+ workflow = REUSABLE_WORKFLOW.read_text(encoding="utf-8")
30
+ for input_name in ("ref", "name", "file-env", "timeout", "cache-save", "enable-usdt", "configure-aslr"):
31
+ self.assertIn(f" {input_name}:", workflow)
32
+ self.assertIn("permissions:\n actions: read\n contents: read", workflow)
33
+ self.assertIn("persist-credentials: false", workflow)
34
+ self.assertIn("save: ${{ inputs.cache-save }}", workflow)
35
+ self.assertIn("if: inputs.enable-usdt", workflow)
36
+ self.assertIn("if: inputs.configure-aslr", workflow)
37
+ for shared_step in (
38
+ "Configure environment",
39
+ "Restore compiler cache",
40
+ "Restore caches",
41
+ "Configure Docker",
42
+ "Run sanitizer coverage",
43
+ ):
44
+ self.assertEqual(workflow.count(shared_step), 1)
45
+
46
+ def test_pr_asan_remains_selected_and_untrusted(self):
47
+ workflow = CI_WORKFLOW.read_text(encoding="utf-8")
48
+ self.assertIn("permissions:\n actions: read\n contents: read\n pull-requests: read", workflow)
49
+ sanitizers = job(CI_WORKFLOW, "sanitizers")
50
+ self.assertIn("needs.classify.outputs.sanitizers == 'true'", sanitizers)
51
+ self.assertIn("uses: ./.github/workflows/reusable-sanitizer.yml", sanitizers)
52
+ self.assertIn("ref: ${{ github.event.pull_request.head.sha }}", sanitizers)
53
+ self.assertIn("name: ASan, LSan, UBSan, integer, wallet, GUI, and USDT", sanitizers)
54
+ self.assertIn("file-env: ./ci/test/00_setup_env_native_asan.sh", sanitizers)
55
+ self.assertIn("timeout: 120", sanitizers)
56
+ self.assertIn("cache-save: false", sanitizers)
57
+ self.assertIn("enable-usdt: true", sanitizers)
58
+ self.assertIn("configure-aslr: false", sanitizers)
59
+ self.assertNotIn("Restore compiler cache", sanitizers)
60
+ self.assertNotIn("Enable USDT coverage", sanitizers)
61
+
62
+ def test_nightly_sanitizer_matrices_and_failure_aggregation_remain_exact(self):
63
+ workflow = NIGHTLY_WORKFLOW.read_text(encoding="utf-8")
64
+ for expected in (
65
+ "all) value=\"${all}\"; sanitizer_value=\"${sanitizers}\"",
66
+ "tsan) value='{\"include\":[]}'; sanitizer_value='{\"include\":[{\"name\":\"ThreadSanitizer\",\"file-env\":\"./ci/test/00_setup_env_native_tsan.sh\",\"timeout\":240,\"configure-aslr\":true}]}'",
67
+ "msan) value='{\"include\":[]}'; sanitizer_value='{\"include\":[{\"name\":\"MemorySanitizer\",\"file-env\":\"./ci/test/00_setup_env_native_msan.sh\",\"timeout\":240,\"configure-aslr\":true}]}'",
68
+ "sanitizers) value='{\"include\":[]}'; sanitizer_value=\"${sanitizers}\"",
69
+ ):
70
+ self.assertIn(expected, workflow)
71
+
72
+ sanitizers = job(NIGHTLY_WORKFLOW, "sanitizers")
73
+ self.assertIn("strategy:\n fail-fast: false", sanitizers)
74
+ self.assertIn("matrix: ${{ fromJSON(needs.gate.outputs.sanitizer-matrix) }}", sanitizers)
75
+ self.assertIn("uses: ./.github/workflows/reusable-sanitizer.yml", sanitizers)
76
+ self.assertIn("enable-usdt: false", sanitizers)
77
+ self.assertIn("configure-aslr: ${{ matrix.configure-aslr }}", sanitizers)
78
+
79
+ required_result = job(CI_WORKFLOW, "required-result")
80
+ self.assertIn("sanitizers", required_result.split("needs:", 1)[1].split("runs-on:", 1)[0])
81
+ self.assertIn("SANITIZERS: ${{ needs.sanitizers.result }}", required_result)
82
+ self.assertIn("check_selected \"$SANITIZERS_SELECTED\" \"$SANITIZERS\" sanitizers", required_result)
83
+
84
+ def test_cpp_and_ci_sanitizer_selection_contracts_are_preserved(self):
85
+ spec = importlib.util.spec_from_file_location("change_classifier", CLASSIFIER)
86
+ module = importlib.util.module_from_spec(spec)
87
+ spec.loader.exec_module(module)
88
+ policy = json.loads(POLICY.read_text(encoding="utf-8"))
89
+
90
+ cpp_result = module.result_for(["src/wallet/wallet.cpp"], [], False, False, policy)
91
+ self.assertTrue(cpp_result["selected"]["sanitizers"])
92
+
93
+ label_result = module.result_for(["README.md"], ["ci:sanitizers"], False, False, policy)
94
+ self.assertTrue(label_result["selected"]["sanitizers"])
95
+ self.assertTrue(label_result["selected"]["nightly_sanitizers"])
96
+
97
+
98
+ if __name__ == "__main__":
99
+ unittest.main()