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
19 @pytest.fixture(scope
='session')
20 def capture_interface(request
, cmd_dumpcap
):
22 Name of capture interface. Tests will be skipped if dumpcap is not
23 available or no Loopback interface is available.
25 disabled
= request
.config
.getoption('--disable-capture', default
=False)
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
)
40 pytest
.skip('Test requires a capture interface.')
42 # Interface found, check for capture privileges (needed for Linux).
44 subprocess
.check_output((cmd_dumpcap
, '-L', '-i', iface
),
45 stderr
=subprocess
.STDOUT
,
46 universal_newlines
=True)
48 except subprocess
.CalledProcessError
as e
:
49 print('"dumpcap -L -i %s" exited with %d. Output:\n%s' % (iface
,
52 pytest
.skip('Test requires capture privileges.')
55 @pytest.fixture(scope
='session')
56 def program_path(request
):
58 Path to the Wireshark binaries as set by the --program-path option, the
59 WS_BIN_PATH environment variable or (curdir)/run.
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
68 request
.config
.getoption('--program-path', default
=None),
69 os
.environ
.get('WS_BIN_PATH'),
73 if type(path
) == str and os
.path
.isdir(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',
82 skip_if_missing
= skip_if_missing
.split(',') if skip_if_missing
else []
84 if sys
.platform
.startswith('win32'):
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
,))
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
))
156 return program(os
.path
.join('extcap/stratoshark', name
))
158 if sys
.platform
== 'darwin':
159 return program(os
.path
.join('Wireshark.app/Contents/MacOS/extcap', name
))
161 return program(os
.path
.join('extcap/wireshark', name
))
165 @pytest.fixture(scope
='session')
166 def features(cmd_tshark
, make_env
):
167 '''Returns an object describing available features in tshark.'''
169 tshark_v
= subprocess
.check_output(
170 (cmd_tshark
, '--version'),
171 stderr
=subprocess
.PIPE
,
172 universal_newlines
=True,
175 tshark_v
= re
.sub(r
'\s+', ' ', tshark_v
)
176 except subprocess
.CalledProcessError
as ex
:
177 print('Failed to detect tshark features: %s' % (ex
,))
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')
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
)
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
227 def home_path(tmp_path
):
228 '''Per-test home directory.'''
229 return str(tmp_path
/ 'test-home')
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')
237 conf_path
= os
.path
.join(home_path
, '.config', 'wireshark')
238 os
.makedirs(conf_path
)
242 @pytest.fixture(scope
='session')
244 """A factory for a modified environment to ensure reproducible tests."""
245 def make_env_real(home
=None):
246 env
= os
.environ
.copy()
248 home_env
= 'APPDATA' if sys
.platform
.startswith('win32') else 'HOME'
252 # This directory is supposed not to be written and is used by
253 # "readonly" tests that do not read any other preferences.
254 env
[home_env
] = "/wireshark-tests-unused"
255 # XDG_CONFIG_HOME takes precedence over HOME, which we don't want.
257 del env
['XDG_CONFIG_HOME']
265 def base_env(home_path
, make_env
, request
):
266 """A modified environment to ensure reproducible tests. Tests can modify
267 this environment as they see fit."""
268 env
= make_env(home
=home_path
)
274 def test_env(base_env
, conf_path
, request
, dirs
):
275 '''A process environment with a populated configuration directory.'''
276 # Populate our UAT files
279 'dtlsdecrypttablefile',
282 'c1222_decryption_table',
283 'ikev1_decryption_table',
284 'ikev2_decryption_table',
286 # uat.c replaces backslashes...
287 key_dir_path
= os
.path
.join(dirs
.key_dir
, '').replace('\\', '\\x5c')
288 for uat
in uat_files
:
289 template_file
= os
.path
.join(dirs
.config_dir
, uat
+ '.tmpl')
290 out_file
= os
.path
.join(conf_path
, uat
)
291 with
open(template_file
, 'r') as f
:
292 template_contents
= f
.read()
293 cf_contents
= template_contents
.replace('TEST_KEYS_DIR', key_dir_path
)
294 with
open(out_file
, 'w') as f
:
298 env
['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1'
299 env
['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1'
301 # Allow GUI tests to be run without opening windows nor requiring a Xserver.
302 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file
303 # in the current directory, output0000.png, output0001.png, etc. Note that
304 # this will overwrite existing files.
305 if sys
.platform
== 'linux':
306 # This option was verified working on Arch Linux with Qt 5.12.0-2 and
307 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and
308 # Windows it unfortunately crashes (Qt 5.12.0).
309 env
['QT_QPA_PLATFORM'] = 'minimal'
315 def test_env_80211_user_tk(base_env
, conf_path
, request
, dirs
):
316 '''A process environment with a populated configuration directory.'''
317 # Populate our UAT files
321 # uat.c replaces backslashes...
322 key_dir_path
= os
.path
.join(dirs
.key_dir
, '').replace('\\', '\\x5c')
323 for uat
in uat_files
:
324 template_file
= os
.path
.join(dirs
.config_dir
, uat
+ '.user_tk_tmpl')
325 out_file
= os
.path
.join(conf_path
, uat
)
326 with
open(template_file
, 'r') as f
:
327 template_contents
= f
.read()
328 cf_contents
= template_contents
.replace('TEST_KEYS_DIR', key_dir_path
)
329 with
open(out_file
, 'w') as f
:
333 env
['WIRESHARK_RUN_FROM_BUILD_DIRECTORY'] = '1'
334 env
['WIRESHARK_QUIT_AFTER_CAPTURE'] = '1'
336 # Allow GUI tests to be run without opening windows nor requiring a Xserver.
337 # Set envvar QT_DEBUG_BACKINGSTORE=1 to save the window contents to a file
338 # in the current directory, output0000.png, output0001.png, etc. Note that
339 # this will overwrite existing files.
340 if sys
.platform
== 'linux':
341 # This option was verified working on Arch Linux with Qt 5.12.0-2 and
342 # Ubuntu 16.04 with libqt5gui5 5.5.1+dfsg-16ubuntu7.5. On macOS and
343 # Windows it unfortunately crashes (Qt 5.12.0).
344 env
['QT_QPA_PLATFORM'] = 'minimal'
349 def dfilter_env(base_env
, conf_path
, request
, dirs
):
350 '''A process environment with a populated configuration directory.'''
351 src_macro_path
= os
.path
.join(dirs
.dfilter_dir
, 'test_dmacros')
352 dst_macro_path
= os
.path
.join(conf_path
, 'dmacros')
353 shutil
.copy(src_macro_path
, dst_macro_path
)
359 def unicode_env(home_path
, make_env
):
360 '''A Wireshark configuration directory with Unicode in its path.'''
361 home_env
= 'APPDATA' if sys
.platform
.startswith('win32') else 'HOME'
362 uni_home
= os
.path
.join(home_path
, 'unicode-Ф-€-中-testcases')
363 env
= make_env(home
=uni_home
)
364 if sys
.platform
== 'win32':
365 pluginsdir
= os
.path
.join(uni_home
, 'Wireshark', 'plugins')
367 pluginsdir
= os
.path
.join(uni_home
, '.local/lib/wireshark/plugins')
368 os
.makedirs(pluginsdir
)
369 return types
.SimpleNamespace(
370 path
=lambda *args
: os
.path
.join(uni_home
, *args
),
372 pluginsdir
=pluginsdir
376 @pytest.fixture(scope
='session')
377 def make_screenshot():
378 '''Creates a screenshot and save it to a file. Intended for CI purposes.'''
379 def make_screenshot_real(filename
):
381 if sys
.platform
== 'darwin':
382 subprocess
.check_call(['screencapture', filename
])
384 print("Creating a screenshot on this platform is not supported")
386 size
= os
.path
.getsize(filename
)
387 print("Created screenshot %s (%d bytes)" % (filename
, size
))
388 except (subprocess
.CalledProcessError
, OSError) as e
:
389 print("Failed to take screenshot:", e
)
390 return make_screenshot_real
394 def make_screenshot_on_error(request
, make_screenshot
, result_file
):
395 '''Writes a screenshot when a process times out.'''
397 def make_screenshot_on_error_real():
400 except subprocess
.TimeoutExpired
:
401 filename
= result_file('screenshot.png')
402 make_screenshot(filename
)
404 return make_screenshot_on_error_real