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>
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-01-15 03:39:11 +03:00
from typing import Iterator
2020-03-16 23:21:25 +03:00
from contextlib import ExitStack
2019-09-23 12:02:43 +03:00
import kunit_config
2020-03-16 23:21:25 +03:00
import kunit_parser
2019-09-23 12:02:43 +03:00
KCONFIG_PATH = ' .config '
2020-10-26 19:59:25 +03:00
KUNITCONFIG_PATH = ' .kunitconfig '
DEFAULT_KUNITCONFIG_PATH = ' arch/um/configs/kunit_defconfig '
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 '
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-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-01-15 03:39:11 +03:00
def make_olddefconfig ( self , build_dir , make_options ) - > None :
2019-09-23 12:02:43 +03:00
command = [ ' make ' , ' ARCH=um ' , ' olddefconfig ' ]
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 ]
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-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... ' )
with ExitStack ( ) as es :
2020-09-24 00:19:38 +03:00
config = open ( get_kconfig_path ( build_dir ) , ' a ' )
2020-03-16 23:21:25 +03:00
disable = open ( BROKEN_ALLCONFIG_PATH , ' r ' ) . read ( )
config . write ( disable )
kunit_parser . print_with_timestamp (
' Starting Kernel with all configs takes a few minutes... ' )
2021-01-15 03:39:11 +03:00
def make ( self , jobs , build_dir , make_options ) - > None :
2019-09-23 12:02:43 +03:00
command = [ ' make ' , ' ARCH=um ' , ' --jobs= ' + str ( jobs ) ]
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 ]
try :
2020-10-30 01:09:29 +03:00
proc = subprocess . Popen ( command ,
stderr = subprocess . PIPE ,
stdout = subprocess . DEVNULL )
2019-09-23 12:02:43 +03:00
except OSError as e :
2020-10-30 01:09:29 +03:00
raise BuildError ( ' Could not call make command: ' + str ( e ) )
_ , stderr = proc . communicate ( )
if proc . returncode != 0 :
raise BuildError ( stderr . decode ( ) )
if stderr : # likely only due to build warnings
print ( stderr . decode ( ) )
2019-09-23 12:02:43 +03:00
2021-01-15 03:39:11 +03:00
def linux_bin ( self , params , timeout , build_dir ) - > None :
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 ' )
2020-10-26 19:59:26 +03:00
outfile = get_outfile_path ( build_dir )
2020-03-16 23:21:25 +03:00
with open ( outfile , ' w ' ) as output :
process = subprocess . Popen ( [ linux_bin ] + params ,
stdout = output ,
stderr = subprocess . STDOUT )
process . wait ( timeout )
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
2019-09-23 12:02:43 +03:00
class LinuxSourceTree ( object ) :
""" Represents a Linux kernel source tree with KUnit tests. """
2021-02-01 23:55:14 +03:00
def __init__ ( self , build_dir : str , load_config = True , kunitconfig_path = ' ' ) - > None :
2020-03-16 23:21:25 +03:00
signal . signal ( signal . SIGINT , self . signal_handler )
2019-09-23 12:02:43 +03:00
2021-01-15 03:39:13 +03:00
self . _ops = LinuxSourceTreeOperations ( )
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 )
self . _kconfig . write_to_file ( kconfig_path )
try :
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 )
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-01-15 03:39:11 +03:00
def build_um_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 )
2020-10-26 19:59:26 +03:00
self . _ops . linux_bin ( args , timeout , build_dir )
outfile = get_outfile_path ( build_dir )
2020-03-16 23:21:25 +03:00
subprocess . call ( [ ' stty ' , ' sane ' ] )
with open ( outfile , ' r ' ) as file :
for line in file :
yield line
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 ' ] )