Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 16: [PATCH 16/17] fix(build): classify Homebrew libevent headers as system

ci/test/test_homebrew_libevent_headers.py

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

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..4c9688d2eba63ed1699e1f0076904989dfe73161

Download original patch · Release notes

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

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.

ci/test/test_homebrew_libevent_headers.py ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026-present The Bitcoin Roots developers
3
+ # Distributed under the MIT software license, see the accompanying
4
+ # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
+
6
+ from pathlib import Path
7
+ import json
8
+ import os
9
+ import shlex
10
+ import shutil
11
+ import subprocess
12
+ import tempfile
13
+ import unittest
14
+
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ CLANG = shutil.which('clang++')
17
+
18
+
19
+ @unittest.skipUnless(CLANG and shutil.which('cmake') and shutil.which('pkg-config'),
20
+ 'CMake, Clang and pkg-config are required')
21
+ class HomebrewLibeventHeadersTest(unittest.TestCase):
22
+ def build(self, discovery, *, apple=True, mismatch=False, implicit=True,
23
+ project_warning=False, brew_success=True, alias=True, extra_mismatch=False):
24
+ with tempfile.TemporaryDirectory(prefix='roots-homebrew-libevent-') as directory:
25
+ work = Path(directory)
26
+ cellar = work / 'Cellar/libevent/2.1.13'
27
+ prefix = work / 'homebrew prefix'
28
+ headers = cellar / 'include/event2'
29
+ headers.mkdir(parents=True)
30
+ # Reproduce the native libevent documentation diagnostic, without
31
+ # changing or copying third-party source into the repository.
32
+ (headers / 'event.h').write_text(
33
+ '/** @param missing Not a parameter. */\n'
34
+ 'void event_probe(int actual);\n')
35
+ (headers / 'http.h').write_text(
36
+ '#include <cstdint>\nstruct evhttp_connection {};\n'
37
+ 'inline void evhttp_connection_get_peer(evhttp_connection*, '
38
+ 'const char**, uint16_t*) {}\n')
39
+ (prefix / 'include').mkdir(parents=True)
40
+ if alias:
41
+ if mismatch:
42
+ shutil.copytree(headers, prefix / 'include/event2')
43
+ else:
44
+ (prefix / 'include/event2').symlink_to(headers, target_is_directory=True)
45
+ other = work / 'other-libevent'
46
+ if extra_mismatch:
47
+ shutil.copytree(headers, other / 'include/event2')
48
+ tools = work / 'tools'
49
+ tools.mkdir()
50
+ brew = tools / 'brew'
51
+ brew.write_text('#!/bin/sh\n' + (
52
+ 'case "$*" in\n'
53
+ f' "--prefix") printf "%s\\n" {shlex.quote(str(prefix))} ;;\n'
54
+ ' *) exit 1 ;;\nesac\n' if brew_success else 'exit 1\n'))
55
+ brew.chmod(0o755)
56
+ config = work / 'config'
57
+ config.mkdir()
58
+ pc = work / 'pkgconfig'
59
+ pc.mkdir()
60
+ configs = ['set(Libevent_VERSION "2.1.13")\n']
61
+ for component in ('core', 'extra', 'pthreads'):
62
+ selected = other if extra_mismatch and component == 'extra' else cellar
63
+ (selected / 'lib').mkdir(exist_ok=True)
64
+ configs.append(
65
+ f'if(NOT TARGET libevent::{component})\n'
66
+ f' add_library(libevent::{component} INTERFACE IMPORTED)\n'
67
+ f' set_target_properties(libevent::{component} PROPERTIES\n'
68
+ f' INTERFACE_INCLUDE_DIRECTORIES "{selected / "include"}")\n'
69
+ 'endif()\n')
70
+ (pc / f'libevent_{component}.pc').write_text(
71
+ f'prefix={selected}\nlibdir=${{prefix}}/lib\nincludedir=${{prefix}}/include\n'
72
+ f'Name: libevent_{component}\nDescription: Test fixture\nVersion: 2.1.13\n'
73
+ 'Libs: -L${libdir}\nCflags: -I${includedir}\n')
74
+ (config / 'LibeventConfig.cmake').write_text(''.join(configs))
75
+ (config / 'LibeventConfigVersion.cmake').write_text(
76
+ 'set(PACKAGE_VERSION "2.1.13")\nset(PACKAGE_VERSION_COMPATIBLE TRUE)\n')
77
+ source = work / 'source'
78
+ source.mkdir()
79
+ (source / 'probe.cpp').write_text(
80
+ '#include <event2/event.h>\n' + (
81
+ '/** @param wrong Not a parameter. */\nvoid project_probe(int actual);\n'
82
+ if project_warning else ''))
83
+ implicit_include = (
84
+ f'list(APPEND CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "{prefix / "include"}")\n'
85
+ if implicit else '')
86
+ (source / 'CMakeLists.txt').write_text(
87
+ 'cmake_minimum_required(VERSION 3.22)\n'
88
+ 'project(HomebrewLibeventHeaders LANGUAGES CXX)\n'
89
+ f'set(CMAKE_HOST_APPLE {"TRUE" if apple else "FALSE"})\n'
90
+ f'{implicit_include}'
91
+ f'list(PREPEND CMAKE_MODULE_PATH "{ROOT / "cmake/module"}")\n'
92
+ 'find_package(Libevent 2.1.8 MODULE REQUIRED)\n'
93
+ 'foreach(component core extra pthreads)\n'
94
+ ' add_library(probe_${component} OBJECT probe.cpp)\n'
95
+ ' target_link_libraries(probe_${component} PRIVATE libevent::${component})\n'
96
+ ' target_compile_options(probe_${component} PRIVATE -Wdocumentation -Werror)\n'
97
+ 'endforeach()\n')
98
+ environment = {**os.environ, 'PATH': f'{tools}:{os.environ["PATH"]}',
99
+ 'PKG_CONFIG_LIBDIR': str(pc), 'PKG_CONFIG_PATH': ''}
100
+ environment.pop('CPATH', None)
101
+ build = work / 'build'
102
+ configure = subprocess.run(
103
+ ['cmake', '-S', str(source), '-B', str(build),
104
+ f'-DCMAKE_CXX_COMPILER={CLANG}', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
105
+ f'-DLibevent_DIR={config if discovery == "config" else work / "absent"}'],
106
+ capture_output=True, text=True, env=environment)
107
+ self.assertEqual(configure.returncode, 0, configure.stdout + configure.stderr)
108
+ if discovery == 'pkgconfig':
109
+ self.assertIn('Found PkgConfig:', configure.stdout)
110
+ else:
111
+ self.assertIn(f'Found Libevent: {config}', configure.stdout)
112
+ commands = json.loads((build / 'compile_commands.json').read_text())
113
+ self.assertEqual(len(commands), 3)
114
+ result = subprocess.run(
115
+ ['cmake', '--build', str(build), '--parallel', '1'],
116
+ capture_output=True, text=True,
117
+ env={**environment, 'CPATH': str(prefix / 'include')})
118
+ for entry in commands:
119
+ argv = shlex.split(entry['command'])
120
+ system_dirs = [argv[i + 1] for i, arg in enumerate(argv[:-1]) if arg == '-isystem']
121
+ expected = apple and not mismatch and brew_success and alias
122
+ if extra_mismatch and 'probe_extra.dir' in entry['command']:
123
+ expected = False
124
+ self.assertEqual(str(prefix / 'include') in system_dirs, expected,
125
+ entry['command'] + result.stderr)
126
+ self.assertIn('-Werror', argv)
127
+ self.assertIn('-Wdocumentation', argv)
128
+ return result
129
+
130
+ def both(self, **kwargs):
131
+ results = []
132
+ for discovery in ('config', 'pkgconfig'):
133
+ with self.subTest(discovery=discovery):
134
+ results.append(self.build(discovery, **kwargs))
135
+ return results
136
+
137
+ def test_implicit_matching_alias_emits_flag_and_compiles(self):
138
+ for result in self.both():
139
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
140
+
141
+ def test_explicit_matching_alias_compiles(self):
142
+ for result in self.both(implicit=False):
143
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
144
+
145
+ def test_project_documentation_warnings_still_fail(self):
146
+ for result in self.both(project_warning=True):
147
+ self.assertNotEqual(result.returncode, 0)
148
+ self.assertIn('source/probe.cpp:', result.stderr)
149
+ self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
150
+
151
+ def test_mismatched_installation_is_not_hidden(self):
152
+ for result in self.both(mismatch=True):
153
+ self.assertNotEqual(result.returncode, 0)
154
+ self.assertIn('homebrew prefix/include/event2/event.h:', result.stderr)
155
+
156
+ def test_mismatched_component_is_not_hidden(self):
157
+ for result in self.both(extra_mismatch=True):
158
+ self.assertNotEqual(result.returncode, 0)
159
+ self.assertIn('homebrew prefix/include/event2/event.h:', result.stderr)
160
+
161
+ def test_non_apple_host_is_unchanged(self):
162
+ for result in self.both(apple=False):
163
+ self.assertNotEqual(result.returncode, 0)
164
+ self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
165
+
166
+ def test_failed_brew_query_is_unchanged(self):
167
+ for result in self.both(brew_success=False):
168
+ self.assertNotEqual(result.returncode, 0)
169
+ self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
170
+
171
+ def test_missing_alias_uses_selected_system_headers(self):
172
+ for result in self.both(alias=False):
173
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
174
+
175
+
176
+ if __name__ == '__main__':
177
+ unittest.main()