16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0
26ebf5866SFelix Guo#
36ebf5866SFelix Guo# Runs UML kernel, collects output, and handles errors.
46ebf5866SFelix Guo#
56ebf5866SFelix Guo# Copyright (C) 2019, Google LLC.
66ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com>
76ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com>
86ebf5866SFelix Guo
987c9c163SBrendan Higginsimport importlib.util
106ebf5866SFelix Guoimport logging
116ebf5866SFelix Guoimport subprocess
126ebf5866SFelix Guoimport os
13fcdb0bc0SAndy Shevchenkoimport shutil
14021ed9f5SHeidi Fahimimport signal
1558c965d8SDaniel Latypovfrom typing import Iterator, Optional, Tuple
16021ed9f5SHeidi Fahim
176ebf5866SFelix Guoimport kunit_config
18021ed9f5SHeidi Fahimimport kunit_parser
1987c9c163SBrendan Higginsimport qemu_config
206ebf5866SFelix Guo
216ebf5866SFelix GuoKCONFIG_PATH = '.config'
22fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
23d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
24021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
25128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
2687c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
2787c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
286ebf5866SFelix Guo
29f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default):
30f3ed003eSAndy Shevchenko	if build_dir:
31f3ed003eSAndy Shevchenko		default = os.path.join(build_dir, default)
32f3ed003eSAndy Shevchenko	return default
33f3ed003eSAndy Shevchenko
346ebf5866SFelix Guoclass ConfigError(Exception):
356ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
366ebf5866SFelix Guo
376ebf5866SFelix Guo
386ebf5866SFelix Guoclass BuildError(Exception):
396ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
406ebf5866SFelix Guo
416ebf5866SFelix Guo
426ebf5866SFelix Guoclass LinuxSourceTreeOperations(object):
436ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
446ebf5866SFelix Guo
4587c9c163SBrendan Higgins	def __init__(self, linux_arch: str, cross_compile: Optional[str]):
4687c9c163SBrendan Higgins		self._linux_arch = linux_arch
4787c9c163SBrendan Higgins		self._cross_compile = cross_compile
4887c9c163SBrendan Higgins
4909641f7cSDaniel Latypov	def make_mrproper(self) -> None:
506ebf5866SFelix Guo		try:
515a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
526ebf5866SFelix Guo		except OSError as e:
531abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
546ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
551abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
566ebf5866SFelix Guo
5787c9c163SBrendan Higgins	def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None:
5887c9c163SBrendan Higgins		pass
5987c9c163SBrendan Higgins
6087c9c163SBrendan Higgins	def make_allyesconfig(self, build_dir, make_options) -> None:
6187c9c163SBrendan Higgins		raise ConfigError('Only the "um" arch is supported for alltests')
6287c9c163SBrendan Higgins
6309641f7cSDaniel Latypov	def make_olddefconfig(self, build_dir, make_options) -> None:
6487c9c163SBrendan Higgins		command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig']
6587c9c163SBrendan Higgins		if self._cross_compile:
6687c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
670476e69fSGreg Thelen		if make_options:
680476e69fSGreg Thelen			command.extend(make_options)
696ebf5866SFelix Guo		if build_dir:
706ebf5866SFelix Guo			command += ['O=' + build_dir]
7187c9c163SBrendan Higgins		print('Populating config with:\n$', ' '.join(command))
726ebf5866SFelix Guo		try:
735a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
746ebf5866SFelix Guo		except OSError as e:
751abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
766ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
771abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
786ebf5866SFelix Guo
7987c9c163SBrendan Higgins	def make(self, jobs, build_dir, make_options) -> None:
8087c9c163SBrendan Higgins		command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)]
8187c9c163SBrendan Higgins		if make_options:
8287c9c163SBrendan Higgins			command.extend(make_options)
8387c9c163SBrendan Higgins		if self._cross_compile:
8487c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
8587c9c163SBrendan Higgins		if build_dir:
8687c9c163SBrendan Higgins			command += ['O=' + build_dir]
8787c9c163SBrendan Higgins		print('Building with:\n$', ' '.join(command))
8887c9c163SBrendan Higgins		try:
8987c9c163SBrendan Higgins			proc = subprocess.Popen(command,
9087c9c163SBrendan Higgins						stderr=subprocess.PIPE,
9187c9c163SBrendan Higgins						stdout=subprocess.DEVNULL)
9287c9c163SBrendan Higgins		except OSError as e:
9387c9c163SBrendan Higgins			raise BuildError('Could not call execute make: ' + str(e))
9487c9c163SBrendan Higgins		except subprocess.CalledProcessError as e:
9587c9c163SBrendan Higgins			raise BuildError(e.output)
9687c9c163SBrendan Higgins		_, stderr = proc.communicate()
9787c9c163SBrendan Higgins		if proc.returncode != 0:
9887c9c163SBrendan Higgins			raise BuildError(stderr.decode())
9987c9c163SBrendan Higgins		if stderr:  # likely only due to build warnings
10087c9c163SBrendan Higgins			print(stderr.decode())
10187c9c163SBrendan Higgins
10287c9c163SBrendan Higgins	def run(self, params, timeout, build_dir, outfile) -> None:
10387c9c163SBrendan Higgins		pass
10487c9c163SBrendan Higgins
10587c9c163SBrendan Higgins
10687c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
10787c9c163SBrendan Higgins
10887c9c163SBrendan Higgins	def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
10987c9c163SBrendan Higgins		super().__init__(linux_arch=qemu_arch_params.linux_arch,
11087c9c163SBrendan Higgins				 cross_compile=cross_compile)
11187c9c163SBrendan Higgins		self._kconfig = qemu_arch_params.kconfig
11287c9c163SBrendan Higgins		self._qemu_arch = qemu_arch_params.qemu_arch
11387c9c163SBrendan Higgins		self._kernel_path = qemu_arch_params.kernel_path
11487c9c163SBrendan Higgins		self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
11587c9c163SBrendan Higgins		self._extra_qemu_params = qemu_arch_params.extra_qemu_params
11687c9c163SBrendan Higgins
11787c9c163SBrendan Higgins	def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
11887c9c163SBrendan Higgins		kconfig = kunit_config.Kconfig()
11987c9c163SBrendan Higgins		kconfig.parse_from_string(self._kconfig)
12087c9c163SBrendan Higgins		base_kunitconfig.merge_in_entries(kconfig)
12187c9c163SBrendan Higgins
12287c9c163SBrendan Higgins	def run(self, params, timeout, build_dir, outfile):
12387c9c163SBrendan Higgins		kernel_path = os.path.join(build_dir, self._kernel_path)
12487c9c163SBrendan Higgins		qemu_command = ['qemu-system-' + self._qemu_arch,
12587c9c163SBrendan Higgins				'-nodefaults',
12687c9c163SBrendan Higgins				'-m', '1024',
12787c9c163SBrendan Higgins				'-kernel', kernel_path,
12887c9c163SBrendan Higgins				'-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'',
12987c9c163SBrendan Higgins				'-no-reboot',
13087c9c163SBrendan Higgins				'-nographic',
13187c9c163SBrendan Higgins				'-serial stdio'] + self._extra_qemu_params
13287c9c163SBrendan Higgins		print('Running tests with:\n$', ' '.join(qemu_command))
13387c9c163SBrendan Higgins		with open(outfile, 'w') as output:
13487c9c163SBrendan Higgins			process = subprocess.Popen(' '.join(qemu_command),
13587c9c163SBrendan Higgins						   stdin=subprocess.PIPE,
13687c9c163SBrendan Higgins						   stdout=output,
13787c9c163SBrendan Higgins						   stderr=subprocess.STDOUT,
13887c9c163SBrendan Higgins						   text=True, shell=True)
13987c9c163SBrendan Higgins		try:
14087c9c163SBrendan Higgins			process.wait(timeout=timeout)
14187c9c163SBrendan Higgins		except Exception as e:
14287c9c163SBrendan Higgins			print(e)
14387c9c163SBrendan Higgins			process.terminate()
14487c9c163SBrendan Higgins		return process
14587c9c163SBrendan Higgins
14687c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
14787c9c163SBrendan Higgins	"""An abstraction over command line operations performed on a source tree."""
14887c9c163SBrendan Higgins
14987c9c163SBrendan Higgins	def __init__(self, cross_compile=None):
15087c9c163SBrendan Higgins		super().__init__(linux_arch='um', cross_compile=cross_compile)
15187c9c163SBrendan Higgins
15209641f7cSDaniel Latypov	def make_allyesconfig(self, build_dir, make_options) -> None:
153021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
154021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
15567e2fae3SBrendan Higgins		command = ['make', 'ARCH=um', 'allyesconfig']
15667e2fae3SBrendan Higgins		if make_options:
15767e2fae3SBrendan Higgins			command.extend(make_options)
15867e2fae3SBrendan Higgins		if build_dir:
15967e2fae3SBrendan Higgins			command += ['O=' + build_dir]
160021ed9f5SHeidi Fahim		process = subprocess.Popen(
16167e2fae3SBrendan Higgins			command,
162021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
163021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
164021ed9f5SHeidi Fahim		process.wait()
165021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
166021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
167a54ea2e0SDaniel Latypov
168a54ea2e0SDaniel Latypov		with open(get_kconfig_path(build_dir), 'a') as config:
169a54ea2e0SDaniel Latypov			with open(BROKEN_ALLCONFIG_PATH, 'r') as disable:
170a54ea2e0SDaniel Latypov				config.write(disable.read())
171021ed9f5SHeidi Fahim		kunit_parser.print_with_timestamp(
172021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
173021ed9f5SHeidi Fahim
17487c9c163SBrendan Higgins	def run(self, params, timeout, build_dir, outfile):
1756ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
176f3ed003eSAndy Shevchenko		linux_bin = get_file_path(build_dir, 'linux')
177128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
178021ed9f5SHeidi Fahim		with open(outfile, 'w') as output:
179021ed9f5SHeidi Fahim			process = subprocess.Popen([linux_bin] + params,
18087c9c163SBrendan Higgins						   stdin=subprocess.PIPE,
181021ed9f5SHeidi Fahim						   stdout=output,
18287c9c163SBrendan Higgins						   stderr=subprocess.STDOUT,
18387c9c163SBrendan Higgins						   text=True)
184021ed9f5SHeidi Fahim			process.wait(timeout)
1856ebf5866SFelix Guo
18609641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str:
187f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KCONFIG_PATH)
1886ebf5866SFelix Guo
18909641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str:
190f3ed003eSAndy Shevchenko	return get_file_path(build_dir, KUNITCONFIG_PATH)
191fcdb0bc0SAndy Shevchenko
19209641f7cSDaniel Latypovdef get_outfile_path(build_dir) -> str:
193f3ed003eSAndy Shevchenko	return get_file_path(build_dir, OUTFILE_PATH)
194128dc4bcSAndy Shevchenko
19587c9c163SBrendan Higginsdef get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations:
19687c9c163SBrendan Higgins	config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
19787c9c163SBrendan Higgins	if arch == 'um':
19887c9c163SBrendan Higgins		return LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
19987c9c163SBrendan Higgins	elif os.path.isfile(config_path):
20087c9c163SBrendan Higgins		return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1]
201*fe678fedSDaniel Latypov
202*fe678fedSDaniel Latypov	options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
203*fe678fedSDaniel Latypov	raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
20487c9c163SBrendan Higgins
20587c9c163SBrendan Higginsdef get_source_tree_ops_from_qemu_config(config_path: str,
20658c965d8SDaniel Latypov					 cross_compile: Optional[str]) -> Tuple[
20787c9c163SBrendan Higgins							 str, LinuxSourceTreeOperations]:
20887c9c163SBrendan Higgins	# The module name/path has very little to do with where the actual file
20987c9c163SBrendan Higgins	# exists (I learned this through experimentation and could not find it
21087c9c163SBrendan Higgins	# anywhere in the Python documentation).
21187c9c163SBrendan Higgins	#
21287c9c163SBrendan Higgins	# Bascially, we completely ignore the actual file location of the config
21387c9c163SBrendan Higgins	# we are loading and just tell Python that the module lives in the
21487c9c163SBrendan Higgins	# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
21587c9c163SBrendan Higgins	# exists as a file.
21687c9c163SBrendan Higgins	module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
21787c9c163SBrendan Higgins	spec = importlib.util.spec_from_file_location(module_path, config_path)
21887c9c163SBrendan Higgins	config = importlib.util.module_from_spec(spec)
21987c9c163SBrendan Higgins	# TODO(brendanhiggins@google.com): I looked this up and apparently other
22087c9c163SBrendan Higgins	# Python projects have noted that pytype complains that "No attribute
22187c9c163SBrendan Higgins	# 'exec_module' on _importlib_modulespec._Loader". Disabling for now.
22287c9c163SBrendan Higgins	spec.loader.exec_module(config) # pytype: disable=attribute-error
22387c9c163SBrendan Higgins	return config.QEMU_ARCH.linux_arch, LinuxSourceTreeOperationsQemu(
22487c9c163SBrendan Higgins			config.QEMU_ARCH, cross_compile=cross_compile)
22587c9c163SBrendan Higgins
2266ebf5866SFelix Guoclass LinuxSourceTree(object):
2276ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
2286ebf5866SFelix Guo
22987c9c163SBrendan Higgins	def __init__(
23087c9c163SBrendan Higgins	      self,
23187c9c163SBrendan Higgins	      build_dir: str,
23287c9c163SBrendan Higgins	      load_config=True,
23387c9c163SBrendan Higgins	      kunitconfig_path='',
23487c9c163SBrendan Higgins	      arch=None,
23587c9c163SBrendan Higgins	      cross_compile=None,
23687c9c163SBrendan Higgins	      qemu_config_path=None) -> None:
237021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
23887c9c163SBrendan Higgins		if qemu_config_path:
23987c9c163SBrendan Higgins			self._arch, self._ops = get_source_tree_ops_from_qemu_config(
24087c9c163SBrendan Higgins					qemu_config_path, cross_compile)
24187c9c163SBrendan Higgins		else:
24287c9c163SBrendan Higgins			self._arch = 'um' if arch is None else arch
24387c9c163SBrendan Higgins			self._ops = get_source_tree_ops(self._arch, cross_compile)
2442b8fdbbfSDaniel Latypov
2452b8fdbbfSDaniel Latypov		if not load_config:
2462b8fdbbfSDaniel Latypov			return
2472b8fdbbfSDaniel Latypov
248243180f5SDaniel Latypov		if kunitconfig_path:
2499854781dSDaniel Latypov			if os.path.isdir(kunitconfig_path):
2509854781dSDaniel Latypov				kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH)
251243180f5SDaniel Latypov			if not os.path.exists(kunitconfig_path):
252243180f5SDaniel Latypov				raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
253243180f5SDaniel Latypov		else:
2542b8fdbbfSDaniel Latypov			kunitconfig_path = get_kunitconfig_path(build_dir)
2552b8fdbbfSDaniel Latypov			if not os.path.exists(kunitconfig_path):
256243180f5SDaniel Latypov				shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
2572b8fdbbfSDaniel Latypov
2582b8fdbbfSDaniel Latypov		self._kconfig = kunit_config.Kconfig()
2592b8fdbbfSDaniel Latypov		self._kconfig.read_from_file(kunitconfig_path)
2602b8fdbbfSDaniel Latypov
26109641f7cSDaniel Latypov	def clean(self) -> bool:
2626ebf5866SFelix Guo		try:
2636ebf5866SFelix Guo			self._ops.make_mrproper()
2646ebf5866SFelix Guo		except ConfigError as e:
2656ebf5866SFelix Guo			logging.error(e)
2666ebf5866SFelix Guo			return False
2676ebf5866SFelix Guo		return True
2686ebf5866SFelix Guo
26909641f7cSDaniel Latypov	def validate_config(self, build_dir) -> bool:
270dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
271dde54b94SHeidi Fahim		validated_kconfig = kunit_config.Kconfig()
272dde54b94SHeidi Fahim		validated_kconfig.read_from_file(kconfig_path)
273dde54b94SHeidi Fahim		if not self._kconfig.is_subset_of(validated_kconfig):
274dde54b94SHeidi Fahim			invalid = self._kconfig.entries() - validated_kconfig.entries()
275dde54b94SHeidi Fahim			message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
276dde54b94SHeidi Fahim					  'but not in .config: %s' % (
277dde54b94SHeidi Fahim					', '.join([str(e) for e in invalid])
278dde54b94SHeidi Fahim			)
279dde54b94SHeidi Fahim			logging.error(message)
280dde54b94SHeidi Fahim			return False
281dde54b94SHeidi Fahim		return True
282dde54b94SHeidi Fahim
28309641f7cSDaniel Latypov	def build_config(self, build_dir, make_options) -> bool:
2846ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
2856ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
2866ebf5866SFelix Guo			os.mkdir(build_dir)
2876ebf5866SFelix Guo		try:
28887c9c163SBrendan Higgins			self._ops.make_arch_qemuconfig(self._kconfig)
28987c9c163SBrendan Higgins			self._kconfig.write_to_file(kconfig_path)
2900476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
2916ebf5866SFelix Guo		except ConfigError as e:
2926ebf5866SFelix Guo			logging.error(e)
2936ebf5866SFelix Guo			return False
294dde54b94SHeidi Fahim		return self.validate_config(build_dir)
2956ebf5866SFelix Guo
29609641f7cSDaniel Latypov	def build_reconfig(self, build_dir, make_options) -> bool:
29714ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
2986ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
2996ebf5866SFelix Guo		if os.path.exists(kconfig_path):
3006ebf5866SFelix Guo			existing_kconfig = kunit_config.Kconfig()
3016ebf5866SFelix Guo			existing_kconfig.read_from_file(kconfig_path)
30287c9c163SBrendan Higgins			self._ops.make_arch_qemuconfig(self._kconfig)
3036ebf5866SFelix Guo			if not self._kconfig.is_subset_of(existing_kconfig):
3046ebf5866SFelix Guo				print('Regenerating .config ...')
3056ebf5866SFelix Guo				os.remove(kconfig_path)
3060476e69fSGreg Thelen				return self.build_config(build_dir, make_options)
3076ebf5866SFelix Guo			else:
3086ebf5866SFelix Guo				return True
3096ebf5866SFelix Guo		else:
3106ebf5866SFelix Guo			print('Generating .config ...')
3110476e69fSGreg Thelen			return self.build_config(build_dir, make_options)
3126ebf5866SFelix Guo
31387c9c163SBrendan Higgins	def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
3146ebf5866SFelix Guo		try:
31567e2fae3SBrendan Higgins			if alltests:
31667e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
3170476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
3180476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
3196ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
3206ebf5866SFelix Guo			logging.error(e)
3216ebf5866SFelix Guo			return False
322dde54b94SHeidi Fahim		return self.validate_config(build_dir)
3236ebf5866SFelix Guo
3247af29141SDaniel Latypov	def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
3257af29141SDaniel Latypov		if not args:
3267af29141SDaniel Latypov			args = []
327b6d5799bSDavid Gow		args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
328d992880bSDaniel Latypov		if filter_glob:
329d992880bSDaniel Latypov			args.append('kunit.filter_glob='+filter_glob)
330128dc4bcSAndy Shevchenko		outfile = get_outfile_path(build_dir)
33187c9c163SBrendan Higgins		self._ops.run(args, timeout, build_dir, outfile)
332021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
333021ed9f5SHeidi Fahim		with open(outfile, 'r') as file:
334021ed9f5SHeidi Fahim			for line in file:
335021ed9f5SHeidi Fahim				yield line
336021ed9f5SHeidi Fahim
33709641f7cSDaniel Latypov	def signal_handler(self, sig, frame) -> None:
338021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
339021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
340