1 # SPDX-License-Identifier: GPL-2.0
3 # Runs UML kernel, collects output, and handles errors.
5 # Copyright (C) 2019, Google LLC.
6 # Author: Felix Guo <felixguoxiuping@gmail.com>
7 # Author: Brendan Higgins <brendanhiggins@google.com>
15 from contextlib
import ExitStack
20 KCONFIG_PATH
= '.config'
21 KUNITCONFIG_PATH
= '.kunitconfig'
22 DEFAULT_KUNITCONFIG_PATH
= 'arch/um/configs/kunit_defconfig'
23 BROKEN_ALLCONFIG_PATH
= 'tools/testing/kunit/configs/broken_on_uml.config'
24 OUTFILE_PATH
= 'test.log'
26 def get_file_path(build_dir
, default
):
28 default
= os
.path
.join(build_dir
, default
)
31 class ConfigError(Exception):
32 """Represents an error trying to configure the Linux kernel."""
35 class BuildError(Exception):
36 """Represents an error trying to build the Linux kernel."""
39 class LinuxSourceTreeOperations(object):
40 """An abstraction over command line operations performed on a source tree."""
42 def make_mrproper(self
):
44 subprocess
.check_output(['make', 'mrproper'], stderr
=subprocess
.STDOUT
)
46 raise ConfigError('Could not call make command: ' + str(e
))
47 except subprocess
.CalledProcessError
as e
:
48 raise ConfigError(e
.output
.decode())
50 def make_olddefconfig(self
, build_dir
, make_options
):
51 command
= ['make', 'ARCH=um', 'olddefconfig']
53 command
.extend(make_options
)
55 command
+= ['O=' + build_dir
]
57 subprocess
.check_output(command
, stderr
=subprocess
.STDOUT
)
59 raise ConfigError('Could not call make command: ' + str(e
))
60 except subprocess
.CalledProcessError
as e
:
61 raise ConfigError(e
.output
.decode())
63 def make_allyesconfig(self
, build_dir
, make_options
):
64 kunit_parser
.print_with_timestamp(
65 'Enabling all CONFIGs for UML...')
66 command
= ['make', 'ARCH=um', 'allyesconfig']
68 command
.extend(make_options
)
70 command
+= ['O=' + build_dir
]
71 process
= subprocess
.Popen(
73 stdout
=subprocess
.DEVNULL
,
74 stderr
=subprocess
.STDOUT
)
76 kunit_parser
.print_with_timestamp(
77 'Disabling broken configs to run KUnit tests...')
78 with
ExitStack() as es
:
79 config
= open(get_kconfig_path(build_dir
), 'a')
80 disable
= open(BROKEN_ALLCONFIG_PATH
, 'r').read()
82 kunit_parser
.print_with_timestamp(
83 'Starting Kernel with all configs takes a few minutes...')
85 def make(self
, jobs
, build_dir
, make_options
):
86 command
= ['make', 'ARCH=um', '--jobs=' + str(jobs
)]
88 command
.extend(make_options
)
90 command
+= ['O=' + build_dir
]
92 proc
= subprocess
.Popen(command
,
93 stderr
=subprocess
.PIPE
,
94 stdout
=subprocess
.DEVNULL
)
96 raise BuildError('Could not call make command: ' + str(e
))
97 _
, stderr
= proc
.communicate()
98 if proc
.returncode
!= 0:
99 raise BuildError(stderr
.decode())
100 if stderr
: # likely only due to build warnings
101 print(stderr
.decode())
103 def linux_bin(self
, params
, timeout
, build_dir
):
104 """Runs the Linux UML binary. Must be named 'linux'."""
105 linux_bin
= get_file_path(build_dir
, 'linux')
106 outfile
= get_outfile_path(build_dir
)
107 with
open(outfile
, 'w') as output
:
108 process
= subprocess
.Popen([linux_bin
] + params
,
110 stderr
=subprocess
.STDOUT
)
111 process
.wait(timeout
)
113 def get_kconfig_path(build_dir
):
114 return get_file_path(build_dir
, KCONFIG_PATH
)
116 def get_kunitconfig_path(build_dir
):
117 return get_file_path(build_dir
, KUNITCONFIG_PATH
)
119 def get_outfile_path(build_dir
):
120 return get_file_path(build_dir
, OUTFILE_PATH
)
122 class LinuxSourceTree(object):
123 """Represents a Linux kernel source tree with KUnit tests."""
126 self
._ops
= LinuxSourceTreeOperations()
127 signal
.signal(signal
.SIGINT
, self
.signal_handler
)
131 self
._ops
.make_mrproper()
132 except ConfigError
as e
:
137 def create_kunitconfig(self
, build_dir
, defconfig
=DEFAULT_KUNITCONFIG_PATH
):
138 kunitconfig_path
= get_kunitconfig_path(build_dir
)
139 if not os
.path
.exists(kunitconfig_path
):
140 shutil
.copyfile(defconfig
, kunitconfig_path
)
142 def read_kunitconfig(self
, build_dir
):
143 kunitconfig_path
= get_kunitconfig_path(build_dir
)
144 self
._kconfig
= kunit_config
.Kconfig()
145 self
._kconfig
.read_from_file(kunitconfig_path
)
147 def validate_config(self
, build_dir
):
148 kconfig_path
= get_kconfig_path(build_dir
)
149 validated_kconfig
= kunit_config
.Kconfig()
150 validated_kconfig
.read_from_file(kconfig_path
)
151 if not self
._kconfig
.is_subset_of(validated_kconfig
):
152 invalid
= self
._kconfig
.entries() - validated_kconfig
.entries()
153 message
= 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
154 'but not in .config: %s' % (
155 ', '.join([str(e
) for e
in invalid
])
157 logging
.error(message
)
161 def build_config(self
, build_dir
, make_options
):
162 kconfig_path
= get_kconfig_path(build_dir
)
163 if build_dir
and not os
.path
.exists(build_dir
):
165 self
._kconfig
.write_to_file(kconfig_path
)
167 self
._ops
.make_olddefconfig(build_dir
, make_options
)
168 except ConfigError
as e
:
171 return self
.validate_config(build_dir
)
173 def build_reconfig(self
, build_dir
, make_options
):
174 """Creates a new .config if it is not a subset of the .kunitconfig."""
175 kconfig_path
= get_kconfig_path(build_dir
)
176 if os
.path
.exists(kconfig_path
):
177 existing_kconfig
= kunit_config
.Kconfig()
178 existing_kconfig
.read_from_file(kconfig_path
)
179 if not self
._kconfig
.is_subset_of(existing_kconfig
):
180 print('Regenerating .config ...')
181 os
.remove(kconfig_path
)
182 return self
.build_config(build_dir
, make_options
)
186 print('Generating .config ...')
187 return self
.build_config(build_dir
, make_options
)
189 def build_um_kernel(self
, alltests
, jobs
, build_dir
, make_options
):
192 self
._ops
.make_allyesconfig(build_dir
, make_options
)
193 self
._ops
.make_olddefconfig(build_dir
, make_options
)
194 self
._ops
.make(jobs
, build_dir
, make_options
)
195 except (ConfigError
, BuildError
) as e
:
198 return self
.validate_config(build_dir
)
200 def run_kernel(self
, args
=[], build_dir
='', timeout
=None):
201 args
.extend(['mem=1G', 'console=tty'])
202 self
._ops
.linux_bin(args
, timeout
, build_dir
)
203 outfile
= get_outfile_path(build_dir
)
204 subprocess
.call(['stty', 'sane'])
205 with
open(outfile
, 'r') as file:
209 def signal_handler(self
, sig
, frame
):
210 logging
.error('Build interruption occurred. Cleaning console.')
211 subprocess
.call(['stty', 'sane'])