2019-09-23 12:02:43 +03:00
# SPDX-License-Identifier: GPL-2.0
#
# Runs UML kernel, collects output, and handles errors.
#
# Copyright (C) 2019, Google LLC.
# Author: Felix Guo <felixguoxiuping@gmail.com>
# Author: Brendan Higgins <brendanhiggins@google.com>
2021-05-27 00:24:06 +03:00
import importlib . util
2019-09-23 12:02:43 +03:00
import logging
import subprocess
import os
2020-10-26 19:59:25 +03:00
import shutil
2020-03-16 23:21:25 +03:00
import signal
2021-10-05 04:13:40 +03:00
import threading
from typing import Iterator , List , Optional , Tuple
2020-03-16 23:21:25 +03:00
2019-09-23 12:02:43 +03:00
import kunit_config
2020-03-16 23:21:25 +03:00
import kunit_parser
2021-05-27 00:24:06 +03:00
import qemu_config
2019-09-23 12:02:43 +03:00
KCONFIG_PATH = ' .config '
2020-10-26 19:59:25 +03:00
KUNITCONFIG_PATH = ' .kunitconfig '
2021-05-22 07:42:40 +03:00
DEFAULT_KUNITCONFIG_PATH = ' tools/testing/kunit/configs/default.config '
2020-03-16 23:21:25 +03:00
BROKEN_ALLCONFIG_PATH = ' tools/testing/kunit/configs/broken_on_uml.config '
2020-10-26 19:59:26 +03:00
OUTFILE_PATH = ' test.log '
2021-05-27 00:24:06 +03:00
ABS_TOOL_PATH = os . path . abspath ( os . path . dirname ( __file__ ) )
QEMU_CONFIGS_DIR = os . path . join ( ABS_TOOL_PATH , ' qemu_configs ' )
2019-09-23 12:02:43 +03:00
2020-10-26 19:59:27 +03:00
def get_file_path ( build_dir , default ) :
if build_dir :
default = os . path . join ( build_dir , default )
return default
2019-09-23 12:02:43 +03:00
class ConfigError ( Exception ) :
""" Represents an error trying to configure the Linux kernel. """
class BuildError ( Exception ) :
""" Represents an error trying to build the Linux kernel. """
class LinuxSourceTreeOperations ( object ) :
""" An abstraction over command line operations performed on a source tree. """
2021-05-27 00:24:06 +03:00
def __init__ ( self , linux_arch : str , cross_compile : Optional [ str ] ) :
self . _linux_arch = linux_arch
self . _cross_compile = cross_compile
2021-01-15 03:39:11 +03:00
def make_mrproper ( self ) - > None :
2019-09-23 12:02:43 +03:00
try :
2020-07-09 00:35:43 +03:00
subprocess . check_output ( [ ' make ' , ' mrproper ' ] , stderr = subprocess . STDOUT )
2019-09-23 12:02:43 +03:00
except OSError as e :
2020-09-30 21:31:51 +03:00
raise ConfigError ( ' Could not call make command: ' + str ( e ) )
2019-09-23 12:02:43 +03:00
except subprocess . CalledProcessError as e :
2020-09-30 21:31:51 +03:00
raise ConfigError ( e . output . decode ( ) )
2019-09-23 12:02:43 +03:00
2021-05-27 00:24:06 +03:00
def make_arch_qemuconfig ( self , kconfig : kunit_config . Kconfig ) - > None :
pass
def make_allyesconfig ( self , build_dir , make_options ) - > None :
raise ConfigError ( ' Only the " um " arch is supported for alltests ' )
2021-01-15 03:39:11 +03:00
def make_olddefconfig ( self , build_dir , make_options ) - > None :
2021-05-27 00:24:06 +03:00
command = [ ' make ' , ' ARCH= ' + self . _linux_arch , ' olddefconfig ' ]
if self . _cross_compile :
command + = [ ' CROSS_COMPILE= ' + self . _cross_compile ]
2020-03-23 22:04:59 +03:00
if make_options :
command . extend ( make_options )
2019-09-23 12:02:43 +03:00
if build_dir :
command + = [ ' O= ' + build_dir ]
2021-05-27 00:24:06 +03:00
print ( ' Populating config with: \n $ ' , ' ' . join ( command ) )
2019-09-23 12:02:43 +03:00
try :
2020-07-09 00:35:43 +03:00
subprocess . check_output ( command , stderr = subprocess . STDOUT )
2019-09-23 12:02:43 +03:00
except OSError as e :
2020-09-30 21:31:51 +03:00
raise ConfigError ( ' Could not call make command: ' + str ( e ) )
2019-09-23 12:02:43 +03:00
except subprocess . CalledProcessError as e :
2020-09-30 21:31:51 +03:00
raise ConfigError ( e . output . decode ( ) )
2019-09-23 12:02:43 +03:00
2021-05-27 00:24:06 +03:00
def make ( self , jobs , build_dir , make_options ) - > None :
command = [ ' make ' , ' ARCH= ' + self . _linux_arch , ' --jobs= ' + str ( jobs ) ]
if make_options :
command . extend ( make_options )
if self . _cross_compile :
command + = [ ' CROSS_COMPILE= ' + self . _cross_compile ]
if build_dir :
command + = [ ' O= ' + build_dir ]
print ( ' Building with: \n $ ' , ' ' . join ( command ) )
try :
proc = subprocess . Popen ( command ,
stderr = subprocess . PIPE ,
stdout = subprocess . DEVNULL )
except OSError as e :
raise BuildError ( ' Could not call execute make: ' + str ( e ) )
except subprocess . CalledProcessError as e :
raise BuildError ( e . output )
_ , stderr = proc . communicate ( )
if proc . returncode != 0 :
raise BuildError ( stderr . decode ( ) )
if stderr : # likely only due to build warnings
print ( stderr . decode ( ) )
2021-10-05 04:13:40 +03:00
def start ( self , params : List [ str ] , build_dir : str ) - > subprocess . Popen :
raise RuntimeError ( ' not implemented! ' )
2021-05-27 00:24:06 +03:00
class LinuxSourceTreeOperationsQemu ( LinuxSourceTreeOperations ) :
def __init__ ( self , qemu_arch_params : qemu_config . QemuArchParams , cross_compile : Optional [ str ] ) :
super ( ) . __init__ ( linux_arch = qemu_arch_params . linux_arch ,
cross_compile = cross_compile )
self . _kconfig = qemu_arch_params . kconfig
self . _qemu_arch = qemu_arch_params . qemu_arch
self . _kernel_path = qemu_arch_params . kernel_path
self . _kernel_command_line = qemu_arch_params . kernel_command_line + ' kunit_shutdown=reboot '
self . _extra_qemu_params = qemu_arch_params . extra_qemu_params
def make_arch_qemuconfig ( self , base_kunitconfig : kunit_config . Kconfig ) - > None :
kconfig = kunit_config . Kconfig ( )
kconfig . parse_from_string ( self . _kconfig )
base_kunitconfig . merge_in_entries ( kconfig )
2021-10-05 04:13:40 +03:00
def start ( self , params : List [ str ] , build_dir : str ) - > subprocess . Popen :
2021-05-27 00:24:06 +03:00
kernel_path = os . path . join ( build_dir , self . _kernel_path )
qemu_command = [ ' qemu-system- ' + self . _qemu_arch ,
' -nodefaults ' ,
' -m ' , ' 1024 ' ,
' -kernel ' , kernel_path ,
' -append ' , ' \' ' + ' ' . join ( params + [ self . _kernel_command_line ] ) + ' \' ' ,
' -no-reboot ' ,
' -nographic ' ,
' -serial stdio ' ] + self . _extra_qemu_params
print ( ' Running tests with: \n $ ' , ' ' . join ( qemu_command ) )
2021-10-05 04:13:40 +03:00
return subprocess . Popen ( ' ' . join ( qemu_command ) ,
stdin = subprocess . PIPE ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
2021-10-21 02:21:21 +03:00
text = True , shell = True , errors = ' backslashreplace ' )
2021-05-27 00:24:06 +03:00
class LinuxSourceTreeOperationsUml ( LinuxSourceTreeOperations ) :
""" An abstraction over command line operations performed on a source tree. """
def __init__ ( self , cross_compile = None ) :
super ( ) . __init__ ( linux_arch = ' um ' , cross_compile = cross_compile )
2021-01-15 03:39:11 +03:00
def make_allyesconfig ( self , build_dir , make_options ) - > None :
2020-03-16 23:21:25 +03:00
kunit_parser . print_with_timestamp (
' Enabling all CONFIGs for UML... ' )
2020-09-24 00:19:38 +03:00
command = [ ' make ' , ' ARCH=um ' , ' allyesconfig ' ]
if make_options :
command . extend ( make_options )
if build_dir :
command + = [ ' O= ' + build_dir ]
2020-03-16 23:21:25 +03:00
process = subprocess . Popen (
2020-09-24 00:19:38 +03:00
command ,
2020-03-16 23:21:25 +03:00
stdout = subprocess . DEVNULL ,
stderr = subprocess . STDOUT )
process . wait ( )
kunit_parser . print_with_timestamp (
' Disabling broken configs to run KUnit tests... ' )
2021-09-29 01:11:11 +03:00
with open ( get_kconfig_path ( build_dir ) , ' a ' ) as config :
with open ( BROKEN_ALLCONFIG_PATH , ' r ' ) as disable :
config . write ( disable . read ( ) )
2020-03-16 23:21:25 +03:00
kunit_parser . print_with_timestamp (
' Starting Kernel with all configs takes a few minutes... ' )
2021-10-05 04:13:40 +03:00
def start ( self , params : List [ str ] , build_dir : str ) - > subprocess . Popen :
2019-09-23 12:02:43 +03:00
""" Runs the Linux UML binary. Must be named ' linux ' . """
2020-10-26 19:59:27 +03:00
linux_bin = get_file_path ( build_dir , ' linux ' )
2021-10-05 04:13:40 +03:00
return subprocess . Popen ( [ linux_bin ] + params ,
stdin = subprocess . PIPE ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
2021-10-21 02:21:21 +03:00
text = True , errors = ' backslashreplace ' )
2019-09-23 12:02:43 +03:00
2021-01-15 03:39:11 +03:00
def get_kconfig_path ( build_dir ) - > str :
2020-10-26 19:59:27 +03:00
return get_file_path ( build_dir , KCONFIG_PATH )
2019-09-23 12:02:43 +03:00
2021-01-15 03:39:11 +03:00
def get_kunitconfig_path ( build_dir ) - > str :
2020-10-26 19:59:27 +03:00
return get_file_path ( build_dir , KUNITCONFIG_PATH )
2020-10-26 19:59:25 +03:00
2021-01-15 03:39:11 +03:00
def get_outfile_path ( build_dir ) - > str :
2020-10-26 19:59:27 +03:00
return get_file_path ( build_dir , OUTFILE_PATH )
2020-10-26 19:59:26 +03:00
2021-05-27 00:24:06 +03:00
def get_source_tree_ops ( arch : str , cross_compile : Optional [ str ] ) - > LinuxSourceTreeOperations :
config_path = os . path . join ( QEMU_CONFIGS_DIR , arch + ' .py ' )
if arch == ' um ' :
return LinuxSourceTreeOperationsUml ( cross_compile = cross_compile )
elif os . path . isfile ( config_path ) :
return get_source_tree_ops_from_qemu_config ( config_path , cross_compile ) [ 1 ]
kunit: tool: show list of valid --arch options when invalid
Consider this attempt to run KUnit in QEMU:
$ ./tools/testing/kunit/kunit.py run --arch=x86
Before you'd get this error message:
kunit_kernel.ConfigError: x86 is not a valid arch
After:
kunit_kernel.ConfigError: x86 is not a valid arch, options are ['alpha', 'arm', 'arm64', 'i386', 'powerpc', 'riscv', 's390', 'sparc', 'x86_64']
This should make it a bit easier for people to notice when they make
typos, etc. Currently, one would have to dive into the python code to
figure out what the valid set is.
Signed-off-by: Daniel Latypov <dlatypov@google.com>
Reviewed-by: David Gow <davidgow@google.com>
Reviewed-by: Brendan Higgins <brendanhiggins@google.com>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>
2021-09-30 02:25:34 +03:00
options = [ f [ : - 3 ] for f in os . listdir ( QEMU_CONFIGS_DIR ) if f . endswith ( ' .py ' ) ]
raise ConfigError ( arch + ' is not a valid arch, options are ' + str ( sorted ( options ) ) )
2021-05-27 00:24:06 +03:00
def get_source_tree_ops_from_qemu_config ( config_path : str ,
2021-06-23 22:09:19 +03:00
cross_compile : Optional [ str ] ) - > Tuple [
2021-05-27 00:24:06 +03:00
str , LinuxSourceTreeOperations ] :
# The module name/path has very little to do with where the actual file
# exists (I learned this through experimentation and could not find it
# anywhere in the Python documentation).
#
# Bascially, we completely ignore the actual file location of the config
# we are loading and just tell Python that the module lives in the
# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
# exists as a file.
module_path = ' . ' + os . path . join ( os . path . basename ( QEMU_CONFIGS_DIR ) , os . path . basename ( config_path ) )
spec = importlib . util . spec_from_file_location ( module_path , config_path )
config = importlib . util . module_from_spec ( spec )
2021-10-22 03:49:36 +03:00
# See https://github.com/python/typeshed/pull/2626 for context.
assert isinstance ( spec . loader , importlib . abc . Loader )
spec . loader . exec_module ( config )
if not hasattr ( config , ' QEMU_ARCH ' ) :
raise ValueError ( ' qemu_config module missing " QEMU_ARCH " : ' + config_path )
params : qemu_config . QemuArchParams = config . QEMU_ARCH # type: ignore
return params . linux_arch , LinuxSourceTreeOperationsQemu (
params , cross_compile = cross_compile )
2021-05-27 00:24:06 +03:00
2019-09-23 12:02:43 +03:00
class LinuxSourceTree ( object ) :
""" Represents a Linux kernel source tree with KUnit tests. """
2021-05-27 00:24:06 +03:00
def __init__ (
self ,
build_dir : str ,
load_config = True ,
kunitconfig_path = ' ' ,
arch = None ,
cross_compile = None ,
qemu_config_path = None ) - > None :
2020-03-16 23:21:25 +03:00
signal . signal ( signal . SIGINT , self . signal_handler )
2021-05-27 00:24:06 +03:00
if qemu_config_path :
self . _arch , self . _ops = get_source_tree_ops_from_qemu_config (
qemu_config_path , cross_compile )
else :
self . _arch = ' um ' if arch is None else arch
self . _ops = get_source_tree_ops ( self . _arch , cross_compile )
2021-01-15 03:39:13 +03:00
if not load_config :
return
2019-09-23 12:02:43 +03:00
2021-02-01 23:55:14 +03:00
if kunitconfig_path :
2021-02-23 01:52:41 +03:00
if os . path . isdir ( kunitconfig_path ) :
kunitconfig_path = os . path . join ( kunitconfig_path , KUNITCONFIG_PATH )
2021-02-01 23:55:14 +03:00
if not os . path . exists ( kunitconfig_path ) :
raise ConfigError ( f ' Specified kunitconfig ( { kunitconfig_path } ) does not exist ' )
else :
kunitconfig_path = get_kunitconfig_path ( build_dir )
if not os . path . exists ( kunitconfig_path ) :
shutil . copyfile ( DEFAULT_KUNITCONFIG_PATH , kunitconfig_path )
2020-10-26 19:59:25 +03:00
self . _kconfig = kunit_config . Kconfig ( )
self . _kconfig . read_from_file ( kunitconfig_path )
2021-01-15 03:39:13 +03:00
def clean ( self ) - > bool :
try :
self . _ops . make_mrproper ( )
except ConfigError as e :
logging . error ( e )
return False
return True
2021-01-15 03:39:11 +03:00
def validate_config ( self , build_dir ) - > bool :
2019-11-27 01:36:16 +03:00
kconfig_path = get_kconfig_path ( build_dir )
validated_kconfig = kunit_config . Kconfig ( )
validated_kconfig . read_from_file ( kconfig_path )
if not self . _kconfig . is_subset_of ( validated_kconfig ) :
invalid = self . _kconfig . entries ( ) - validated_kconfig . entries ( )
message = ' Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
' but not in .config: %s ' % (
' , ' . join ( [ str ( e ) for e in invalid ] )
)
logging . error ( message )
return False
return True
2021-01-15 03:39:11 +03:00
def build_config ( self , build_dir , make_options ) - > bool :
2019-09-23 12:02:43 +03:00
kconfig_path = get_kconfig_path ( build_dir )
if build_dir and not os . path . exists ( build_dir ) :
os . mkdir ( build_dir )
try :
2021-05-27 00:24:06 +03:00
self . _ops . make_arch_qemuconfig ( self . _kconfig )
self . _kconfig . write_to_file ( kconfig_path )
2020-03-23 22:04:59 +03:00
self . _ops . make_olddefconfig ( build_dir , make_options )
2019-09-23 12:02:43 +03:00
except ConfigError as e :
logging . error ( e )
return False
2019-11-27 01:36:16 +03:00
return self . validate_config ( build_dir )
2019-09-23 12:02:43 +03:00
2021-01-15 03:39:11 +03:00
def build_reconfig ( self , build_dir , make_options ) - > bool :
2019-12-20 08:14:07 +03:00
""" Creates a new .config if it is not a subset of the .kunitconfig. """
2019-09-23 12:02:43 +03:00
kconfig_path = get_kconfig_path ( build_dir )
if os . path . exists ( kconfig_path ) :
existing_kconfig = kunit_config . Kconfig ( )
existing_kconfig . read_from_file ( kconfig_path )
2021-05-27 00:24:06 +03:00
self . _ops . make_arch_qemuconfig ( self . _kconfig )
2019-09-23 12:02:43 +03:00
if not self . _kconfig . is_subset_of ( existing_kconfig ) :
print ( ' Regenerating .config ... ' )
os . remove ( kconfig_path )
2020-03-23 22:04:59 +03:00
return self . build_config ( build_dir , make_options )
2019-09-23 12:02:43 +03:00
else :
return True
else :
print ( ' Generating .config ... ' )
2020-03-23 22:04:59 +03:00
return self . build_config ( build_dir , make_options )
2019-09-23 12:02:43 +03:00
2021-05-27 00:24:06 +03:00
def build_kernel ( self , alltests , jobs , build_dir , make_options ) - > bool :
2019-09-23 12:02:43 +03:00
try :
2020-09-24 00:19:38 +03:00
if alltests :
self . _ops . make_allyesconfig ( build_dir , make_options )
2020-03-23 22:04:59 +03:00
self . _ops . make_olddefconfig ( build_dir , make_options )
self . _ops . make ( jobs , build_dir , make_options )
2019-09-23 12:02:43 +03:00
except ( ConfigError , BuildError ) as e :
logging . error ( e )
return False
2019-11-27 01:36:16 +03:00
return self . validate_config ( build_dir )
2019-09-23 12:02:43 +03:00
kunit: tool: fix unintentional statefulness in run_kernel()
This is a bug that has been present since the first version of this
code.
Using [] as a default parameter is dangerous, since it's mutable.
Example using the REPL:
>>> def bad(param = []):
... param.append(len(param))
... print(param)
...
>>> bad()
[0]
>>> bad()
[0, 1]
This wasn't a concern in the past since it would just keep appending the
same values to it.
E.g. before, `args` would just grow in size like:
[mem=1G', 'console=tty']
[mem=1G', 'console=tty', mem=1G', 'console=tty']
But with now filter_glob, this is more dangerous, e.g.
run_kernel(filter_glob='my-test*') # default modified here
run_kernel() # filter_glob still applies here!
That earlier `filter_glob` will affect all subsequent calls that don't
specify `args`.
Note: currently the kunit tool only calls run_kernel() at most once, so
it's not possible to trigger any negative side-effects right now.
Fixes: 6ebf5866f2e8 ("kunit: tool: add Python wrappers for running KUnit tests")
Signed-off-by: Daniel Latypov <dlatypov@google.com>
Reviewed-by: Brendan Higgins <brendanhiggins@google.com>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>
2021-02-06 03:08:54 +03:00
def run_kernel ( self , args = None , build_dir = ' ' , filter_glob = ' ' , timeout = None ) - > Iterator [ str ] :
if not args :
args = [ ]
2021-05-27 00:24:04 +03:00
args . extend ( [ ' mem=1G ' , ' console=tty ' , ' kunit_shutdown=halt ' ] )
2021-02-06 03:08:53 +03:00
if filter_glob :
args . append ( ' kunit.filter_glob= ' + filter_glob )
2021-10-05 04:13:40 +03:00
process = self . _ops . start ( args , build_dir )
assert process . stdout is not None # tell mypy it's set
# Enforce the timeout in a background thread.
def _wait_proc ( ) :
try :
process . wait ( timeout = timeout )
except Exception as e :
print ( e )
process . terminate ( )
process . wait ( )
waiter = threading . Thread ( target = _wait_proc )
waiter . start ( )
output = open ( get_outfile_path ( build_dir ) , ' w ' )
try :
# Tee the output to the file and to our caller in real time.
for line in process . stdout :
output . write ( line )
2020-03-16 23:21:25 +03:00
yield line
2021-10-05 04:13:40 +03:00
# This runs even if our caller doesn't consume every line.
finally :
# Flush any leftover output to the file
output . write ( process . stdout . read ( ) )
output . close ( )
process . stdout . close ( )
waiter . join ( )
subprocess . call ( [ ' stty ' , ' sane ' ] )
2020-03-16 23:21:25 +03:00
2021-01-15 03:39:11 +03:00
def signal_handler ( self , sig , frame ) - > None :
2020-03-16 23:21:25 +03:00
logging . error ( ' Build interruption occurred. Cleaning console. ' )
subprocess . call ( [ ' stty ' , ' sane ' ] )