LATER packet-kerberos: ticket_checksum tmpvtb...
[wireshark-sm.git] / test / fixtures_ws.py
blobd5c1a8c23a46f317b2f183a790428a28b3e4a9fb
2 # Wireshark tests
4 # Copyright (c) 2018 Peter Wu <peter@lekensteyn.nl>
6 # SPDX-License-Identifier: GPL-2.0-or-later
8 '''Fixtures that are specific to Wireshark.'''
10 from contextlib import contextmanager
11 import os
12 import re
13 import subprocess
14 import sys
15 import types
16 import pytest
17 import shutil
19 @pytest.fixture(scope='session')
20 def capture_interface(request, cmd_dumpcap):
21 '''
22 Name of capture interface. Tests will be skipped if dumpcap is not
23 available or no Loopback interface is available.
24 '''
25 disabled = request.config.getoption('--disable-capture', default=False)
26 if disabled:
27 pytest.skip('Capture tests are disabled via --disable-capture')
28 proc = subprocess.Popen((cmd_dumpcap, '-D'), stdout=subprocess.PIPE,
29 stderr=subprocess.PIPE, universal_newlines=True)
30 outs, errs = proc.communicate()
31 if proc.returncode != 0:
32 print('"dumpcap -D" exited with %d. stderr:\n%s' %
33 (proc.returncode, errs))
34 pytest.skip('Test requires capture privileges and an interface.')
35 # Matches: "lo (Loopback)" (Linux), "lo0 (Loopback)" (macOS) or
36 # "\Device\NPF_{...} (Npcap Loopback Adapter)" (Windows)
37 print('"dumpcap -D" output:\n%s' % (outs,))
38 m = re.search(r'^(\d+)\. .*\(.*Loopback.*\)', outs, re.MULTILINE|re.IGNORECASE)
39 if not m:
40 pytest.skip('Test requires a capture interface.')
41 iface = m.group(1)
42 # Interface found, check for capture privileges (needed for Linux).
43 try:
44 subprocess.check_output((cmd_dumpcap, '-L', '-i', iface),
45 stderr=subprocess.STDOUT,
46 universal_newlines=True)
47 return iface
48 except subprocess.CalledProcessError as e:
49 print('"dumpcap -L -i %s" exited with %d. Output:\n%s' % (iface,
50 e.returncode,
51 e.output))
52 pytest.skip('Test requires capture privileges.')
55 @pytest.fixture(scope='session')
56 def program_path(request):
57 '''
58 Path to the Wireshark binaries as set by the --program-path option, the
59 WS_BIN_PATH environment variable or (curdir)/run.
60 '''
61 curdir_run = os.path.join(os.curdir, 'run')
62 if sys.platform == 'win32':
63 build_type = request.config.getoption('--build-type')
64 curdir_run_config = os.path.join(curdir_run, build_type)
65 if os.path.exists(curdir_run_config):
66 curdir_run = curdir_run_config
67 paths = (
68 request.config.getoption('--program-path', default=None),
69 os.environ.get('WS_BIN_PATH'),
70 curdir_run,
72 for path in paths:
73 if type(path) == str and os.path.isdir(path):
74 return path
75 raise AssertionError('Missing directory with Wireshark binaries')
78 @pytest.fixture(scope='session')
79 def program(program_path, request):
80 skip_if_missing = request.config.getoption('--skip-missing-programs',
81 default='')
82 skip_if_missing = skip_if_missing.split(',') if skip_if_missing else []
83 dotexe = ''
84 if sys.platform.startswith('win32'):
85 dotexe = '.exe'
87 def resolver(name):
88 path = os.path.abspath(os.path.join(program_path, name + dotexe))
89 if not os.access(path, os.X_OK):
90 if skip_if_missing == ['all'] or name in skip_if_missing:
91 pytest.skip('Program %s is not available' % (name,))
92 raise AssertionError('Program %s is not available' % (name,))
93 return path
94 return resolver
97 @pytest.fixture(scope='session')
98 def cmd_capinfos(program):
99 return program('capinfos')
102 @pytest.fixture(scope='session')
103 def cmd_dumpcap(program):
104 return program('dumpcap')
107 @pytest.fixture(scope='session')
108 def cmd_mergecap(program):
109 return program('mergecap')
112 @pytest.fixture(scope='session')
113 def cmd_rawshark(program):
114 return program('rawshark')
117 @pytest.fixture(scope='session')
118 def cmd_tshark(program):
119 return program('tshark')
122 @pytest.fixture(scope='session')
123 def cmd_text2pcap(program):
124 return program('text2pcap')
127 @pytest.fixture(scope='session')
128 def cmd_editcap(program):
129 return program('editcap')
132 @pytest.fixture(scope='session')
133 def cmd_wireshark(program):
134 return program('wireshark')
137 @pytest.fixture(scope='session')
138 def wireshark_command(cmd_wireshark):
139 # Windows can always display the GUI and macOS can if we're in a login session.
140 # On Linux, headless mode is used, see QT_QPA_PLATFORM in the 'test_env' fixture.
141 if sys.platform == 'darwin' and 'SECURITYSESSIONID' not in os.environ:
142 pytest.skip('Wireshark GUI tests require loginwindow session')
143 if sys.platform not in ('win32', 'darwin', 'linux'):
144 if 'DISPLAY' not in os.environ:
145 pytest.skip('Wireshark GUI tests require DISPLAY')
146 return (cmd_wireshark, '-ogui.update.enabled:FALSE')
149 @pytest.fixture(scope='session')
150 def cmd_extcap(program):
151 def extcap_name(name, stratoshark_extcap=False):
152 if stratoshark_extcap:
153 if sys.platform == 'darwin':
154 return program(os.path.join('Stratoshark.app/Contents/MacOS/extcap', name))
155 else:
156 return program(os.path.join('extcap/stratoshark', name))
157 else:
158 if sys.platform == 'darwin':
159 return program(os.path.join('Wireshark.app/Contents/MacOS/extcap', name))
160 else:
161 return program(os.path.join('extcap/wireshark', name))
162 return extcap_name
165 @pytest.fixture(scope='session')
166 def features(cmd_tshark, make_env):
167 '''Returns an object describing available features in tshark.'''
168 try:
169 tshark_v = subprocess.check_output(
170 (cmd_tshark, '--version'),
171 stderr=subprocess.PIPE,
172 universal_newlines=True,
173 env=make_env()
175 tshark_v = re.sub(r'\s+', ' ', tshark_v)
176 except subprocess.CalledProcessError as ex:
177 print('Failed to detect tshark features: %s' % (ex,))
178 tshark_v = ''
179 gcry_m = re.search(r'\+Gcrypt +([0-9]+)\.([0-9]+)', tshark_v)
180 gcry_ver = (int(gcry_m.group(1)),int(gcry_m.group(2)))
181 return types.SimpleNamespace(
182 have_x64='Compiler info: 64-bit' in tshark_v,
183 have_lua='+Lua' in tshark_v,
184 have_lua_unicode='(with UfW patches)' in tshark_v,
185 have_nghttp2='+nghttp2' in tshark_v,
186 have_nghttp3='+nghttp3' in tshark_v,
187 have_kerberos='+Kerberos' in tshark_v,
188 have_gnutls='+GnuTLS' in tshark_v,
189 have_pkcs11='PKCS#11' in tshark_v,
190 have_brotli='+brotli' in tshark_v,
191 have_zstd='+Zstandard' in tshark_v,
192 have_plugins='Plugins: supported' in tshark_v,
196 @pytest.fixture(scope='session')
197 def dirs():
198 '''Returns fixed directories containing test input.'''
199 this_dir = os.path.dirname(__file__)
200 return types.SimpleNamespace(
201 baseline_dir=os.path.join(this_dir, 'baseline'),
202 capture_dir=os.path.join(this_dir, 'captures'),
203 config_dir=os.path.join(this_dir, 'config'),
204 key_dir=os.path.join(this_dir, 'keys'),
205 lua_dir=os.path.join(this_dir, 'lua'),
206 protobuf_lang_files_dir=os.path.join(this_dir, 'protobuf_lang_files'),
207 tools_dir=os.path.join(this_dir, '..', 'tools'),
208 dfilter_dir=os.path.join(this_dir, 'suite_dfilter'),
212 @pytest.fixture(scope='session')
213 def capture_file(dirs):
214 '''Returns the path to a capture file.'''
215 def resolver(filename):
216 return os.path.join(dirs.capture_dir, filename)
217 return resolver
219 @pytest.fixture
220 def result_file(tmp_path):
221 '''Returns the path to a temporary file.'''
222 def result_file_real(filename):
223 return str(tmp_path / filename)
224 return result_file_real
226 @pytest.fixture
227 def home_path(tmp_path):
228 '''Per-test home directory.'''
229 return str(tmp_path / 'test-home')
231 @pytest.fixture
232 def conf_path(home_path):
233 '''Path to the Wireshark configuration directory.'''
234 if sys.platform.startswith('win32'):
235 conf_path = os.path.join(home_path, 'Wireshark')
236 else:
237 conf_path = os.path.join(home_path, '.config', 'wireshark')
238 os.makedirs(conf_path)
239 return conf_path
242 @pytest.fixture(scope='session')
243 def make_env():
244 """A factory for a modified environment to ensure reproducible tests."""
245 def make_env_real(home=None):
246 env = os.environ.copy()
247 env['TZ'] = 'UTC'
248 env['WIRESHARK_ABORT_ON_DISSECTOR_BUG'] = '1'
249 home_env = 'APPDATA' if sys.platform.startswith('win32') else 'HOME'
250 if home:
251 env[home_env] = home
252 else:
253 # This directory is supposed not to be written and is used by
254 # "readonly" tests that do not read any other preferences.
255 env[home_env] = "/wireshark-tests-unused"
256 # XDG_CONFIG_HOME takes precedence over HOME, which we don't want.
257 try:
258 del env['XDG_CONFIG_HOME']
259 except KeyError:
260 pass
261 return env
262 return make_env_real
265 @pytest.fixture
266 def base_env(home_path, make_env, request):
267 """A modified environment to ensure reproducible tests. Tests can modify
268 this environment as they see fit."""
269 env = make_env(home=home_path)
271 return env
274 @pytest.fixture
275 def test_env(base_env, conf_path, request, dirs):
276 '''A process environment with a populated configuration directory.'''
277 # Populate our UAT files
278 uat_files = [
279 '80211_keys',
280 'dtlsdecrypttablefile',
281 'esp_sa',
282 'ssl_keys',
283 'c1222_decryption_table',
284 'ikev1_decryption_table',
285 'ikev2_decryption_table',
287 # uat.c replaces backslashes...
288 key_dir_path = os.path.join(dirs.key_dir, '').replace('\\', '\\x5c')
289 for uat in uat_files:
290 template_file = os.path.join(dirs.config_dir, uat + '.tmpl')
291 out_file = os.path.join(conf_path, uat)
292 with open(template_file, 'r') as f:
293 template_contents = f.read()
294 cf_contents = template_contents.replace('TEST_KEYS_DIR', key_dir_path)
295 with open(out_file, 'w') as f:
296 f.write(cf_contents)
298 env = base_env
299 env['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1'
300 env['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1'
302 # Allow GUI tests to be run without opening windows nor requiring a Xserver.
303 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file
304 # in the current directory, output0000.png, output0001.png, etc. Note that
305 # this will overwrite existing files.
306 if sys.platform == 'linux':
307 # This option was verified working on Arch Linux with Qt 5.12.0-2 and
308 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and
309 # Windows it unfortunately crashes (Qt 5.12.0).
310 env['QT_QPA_PLATFORM'] = 'minimal'
312 return env
315 @pytest.fixture
316 def test_env_80211_user_tk(base_env, conf_path, request, dirs):
317 '''A process environment with a populated configuration directory.'''
318 # Populate our UAT files
319 uat_files = [
320 '80211_keys',
322 # uat.c replaces backslashes...
323 key_dir_path = os.path.join(dirs.key_dir, '').replace('\\', '\\x5c')
324 for uat in uat_files:
325 template_file = os.path.join(dirs.config_dir, uat + '.user_tk_tmpl')
326 out_file = os.path.join(conf_path, uat)
327 with open(template_file, 'r') as f:
328 template_contents = f.read()
329 cf_contents = template_contents.replace('TEST_KEYS_DIR', key_dir_path)
330 with open(out_file, 'w') as f:
331 f.write(cf_contents)
333 env = base_env
334 env['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1'
335 env['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1'
337 # Allow GUI tests to be run without opening windows nor requiring a Xserver.
338 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file
339 # in the current directory, output0000.png, output0001.png, etc. Note that
340 # this will overwrite existing files.
341 if sys.platform == 'linux':
342 # This option was verified working on Arch Linux with Qt 5.12.0-2 and
343 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and
344 # Windows it unfortunately crashes (Qt 5.12.0).
345 env['QT_QPA_PLATFORM'] = 'minimal'
347 return env
349 @pytest.fixture
350 def dfilter_env(base_env, conf_path, request, dirs):
351 '''A process environment with a populated configuration directory.'''
352 src_macro_path = os.path.join(dirs.dfilter_dir, 'test_dmacros')
353 dst_macro_path = os.path.join(conf_path, 'dmacros')
354 shutil.copy(src_macro_path, dst_macro_path)
356 env = base_env
357 return env
359 @pytest.fixture
360 def unicode_env(home_path, make_env):
361 '''A Wireshark configuration directory with Unicode in its path.'''
362 home_env = 'APPDATA' if sys.platform.startswith('win32') else 'HOME'
363 uni_home = os.path.join(home_path, 'unicode-Ф-€-中-testcases')
364 env = make_env(home=uni_home)
365 if sys.platform == 'win32':
366 pluginsdir = os.path.join(uni_home, 'Wireshark', 'plugins')
367 else:
368 pluginsdir = os.path.join(uni_home, '.local/lib/wireshark/plugins')
369 os.makedirs(pluginsdir)
370 return types.SimpleNamespace(
371 path=lambda *args: os.path.join(uni_home, *args),
372 env=env,
373 pluginsdir=pluginsdir
377 @pytest.fixture(scope='session')
378 def make_screenshot():
379 '''Creates a screenshot and save it to a file. Intended for CI purposes.'''
380 def make_screenshot_real(filename):
381 try:
382 if sys.platform == 'darwin':
383 subprocess.check_call(['screencapture', filename])
384 else:
385 print("Creating a screenshot on this platform is not supported")
386 return
387 size = os.path.getsize(filename)
388 print("Created screenshot %s (%d bytes)" % (filename, size))
389 except (subprocess.CalledProcessError, OSError) as e:
390 print("Failed to take screenshot:", e)
391 return make_screenshot_real
394 @pytest.fixture
395 def make_screenshot_on_error(request, make_screenshot, result_file):
396 '''Writes a screenshot when a process times out.'''
397 @contextmanager
398 def make_screenshot_on_error_real():
399 try:
400 yield
401 except subprocess.TimeoutExpired:
402 filename = result_file('screenshot.png')
403 make_screenshot(filename)
404 raise
405 return make_screenshot_on_error_real