Merge branches 'edac-spr', 'edac-igen6' and 'edac-misc' into edac-updates-for-v5.11
[sfrench/cifs-2.6.git] / tools / testing / kunit / kunit_kernel.py
1 # SPDX-License-Identifier: GPL-2.0
2 #
3 # Runs UML kernel, collects output, and handles errors.
4 #
5 # Copyright (C) 2019, Google LLC.
6 # Author: Felix Guo <felixguoxiuping@gmail.com>
7 # Author: Brendan Higgins <brendanhiggins@google.com>
8
9 import logging
10 import subprocess
11 import os
12 import shutil
13 import signal
14
15 from contextlib import ExitStack
16
17 import kunit_config
18 import kunit_parser
19
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'
25
26 class ConfigError(Exception):
27         """Represents an error trying to configure the Linux kernel."""
28
29
30 class BuildError(Exception):
31         """Represents an error trying to build the Linux kernel."""
32
33
34 class LinuxSourceTreeOperations(object):
35         """An abstraction over command line operations performed on a source tree."""
36
37         def make_mrproper(self):
38                 try:
39                         subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
40                 except OSError as e:
41                         raise ConfigError('Could not call make command: ' + str(e))
42                 except subprocess.CalledProcessError as e:
43                         raise ConfigError(e.output.decode())
44
45         def make_olddefconfig(self, build_dir, make_options):
46                 command = ['make', 'ARCH=um', 'olddefconfig']
47                 if make_options:
48                         command.extend(make_options)
49                 if build_dir:
50                         command += ['O=' + build_dir]
51                 try:
52                         subprocess.check_output(command, stderr=subprocess.STDOUT)
53                 except OSError as e:
54                         raise ConfigError('Could not call make command: ' + str(e))
55                 except subprocess.CalledProcessError as e:
56                         raise ConfigError(e.output.decode())
57
58         def make_allyesconfig(self, build_dir, make_options):
59                 kunit_parser.print_with_timestamp(
60                         'Enabling all CONFIGs for UML...')
61                 command = ['make', 'ARCH=um', 'allyesconfig']
62                 if make_options:
63                         command.extend(make_options)
64                 if build_dir:
65                         command += ['O=' + build_dir]
66                 process = subprocess.Popen(
67                         command,
68                         stdout=subprocess.DEVNULL,
69                         stderr=subprocess.STDOUT)
70                 process.wait()
71                 kunit_parser.print_with_timestamp(
72                         'Disabling broken configs to run KUnit tests...')
73                 with ExitStack() as es:
74                         config = open(get_kconfig_path(build_dir), 'a')
75                         disable = open(BROKEN_ALLCONFIG_PATH, 'r').read()
76                         config.write(disable)
77                 kunit_parser.print_with_timestamp(
78                         'Starting Kernel with all configs takes a few minutes...')
79
80         def make(self, jobs, build_dir, make_options):
81                 command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
82                 if make_options:
83                         command.extend(make_options)
84                 if build_dir:
85                         command += ['O=' + build_dir]
86                 try:
87                         proc = subprocess.Popen(command,
88                                                 stderr=subprocess.PIPE,
89                                                 stdout=subprocess.DEVNULL)
90                 except OSError as e:
91                         raise BuildError('Could not call make command: ' + str(e))
92                 _, stderr = proc.communicate()
93                 if proc.returncode != 0:
94                         raise BuildError(stderr.decode())
95                 if stderr:  # likely only due to build warnings
96                         print(stderr.decode())
97
98         def linux_bin(self, params, timeout, build_dir):
99                 """Runs the Linux UML binary. Must be named 'linux'."""
100                 linux_bin = './linux'
101                 if build_dir:
102                         linux_bin = os.path.join(build_dir, 'linux')
103                 outfile = get_outfile_path(build_dir)
104                 with open(outfile, 'w') as output:
105                         process = subprocess.Popen([linux_bin] + params,
106                                                    stdout=output,
107                                                    stderr=subprocess.STDOUT)
108                         process.wait(timeout)
109
110 def get_kconfig_path(build_dir):
111         kconfig_path = KCONFIG_PATH
112         if build_dir:
113                 kconfig_path = os.path.join(build_dir, KCONFIG_PATH)
114         return kconfig_path
115
116 def get_kunitconfig_path(build_dir):
117         kunitconfig_path = KUNITCONFIG_PATH
118         if build_dir:
119                 kunitconfig_path = os.path.join(build_dir, KUNITCONFIG_PATH)
120         return kunitconfig_path
121
122 def get_outfile_path(build_dir):
123         outfile_path = OUTFILE_PATH
124         if build_dir:
125                 outfile_path = os.path.join(build_dir, OUTFILE_PATH)
126         return outfile_path
127
128 class LinuxSourceTree(object):
129         """Represents a Linux kernel source tree with KUnit tests."""
130
131         def __init__(self):
132                 self._ops = LinuxSourceTreeOperations()
133                 signal.signal(signal.SIGINT, self.signal_handler)
134
135         def clean(self):
136                 try:
137                         self._ops.make_mrproper()
138                 except ConfigError as e:
139                         logging.error(e)
140                         return False
141                 return True
142
143         def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH):
144                 kunitconfig_path = get_kunitconfig_path(build_dir)
145                 if not os.path.exists(kunitconfig_path):
146                         shutil.copyfile(defconfig, kunitconfig_path)
147
148         def read_kunitconfig(self, build_dir):
149                 kunitconfig_path = get_kunitconfig_path(build_dir)
150                 self._kconfig = kunit_config.Kconfig()
151                 self._kconfig.read_from_file(kunitconfig_path)
152
153         def validate_config(self, build_dir):
154                 kconfig_path = get_kconfig_path(build_dir)
155                 validated_kconfig = kunit_config.Kconfig()
156                 validated_kconfig.read_from_file(kconfig_path)
157                 if not self._kconfig.is_subset_of(validated_kconfig):
158                         invalid = self._kconfig.entries() - validated_kconfig.entries()
159                         message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
160                                           'but not in .config: %s' % (
161                                         ', '.join([str(e) for e in invalid])
162                         )
163                         logging.error(message)
164                         return False
165                 return True
166
167         def build_config(self, build_dir, make_options):
168                 kconfig_path = get_kconfig_path(build_dir)
169                 if build_dir and not os.path.exists(build_dir):
170                         os.mkdir(build_dir)
171                 self._kconfig.write_to_file(kconfig_path)
172                 try:
173                         self._ops.make_olddefconfig(build_dir, make_options)
174                 except ConfigError as e:
175                         logging.error(e)
176                         return False
177                 return self.validate_config(build_dir)
178
179         def build_reconfig(self, build_dir, make_options):
180                 """Creates a new .config if it is not a subset of the .kunitconfig."""
181                 kconfig_path = get_kconfig_path(build_dir)
182                 if os.path.exists(kconfig_path):
183                         existing_kconfig = kunit_config.Kconfig()
184                         existing_kconfig.read_from_file(kconfig_path)
185                         if not self._kconfig.is_subset_of(existing_kconfig):
186                                 print('Regenerating .config ...')
187                                 os.remove(kconfig_path)
188                                 return self.build_config(build_dir, make_options)
189                         else:
190                                 return True
191                 else:
192                         print('Generating .config ...')
193                         return self.build_config(build_dir, make_options)
194
195         def build_um_kernel(self, alltests, jobs, build_dir, make_options):
196                 try:
197                         if alltests:
198                                 self._ops.make_allyesconfig(build_dir, make_options)
199                         self._ops.make_olddefconfig(build_dir, make_options)
200                         self._ops.make(jobs, build_dir, make_options)
201                 except (ConfigError, BuildError) as e:
202                         logging.error(e)
203                         return False
204                 return self.validate_config(build_dir)
205
206         def run_kernel(self, args=[], build_dir='', timeout=None):
207                 args.extend(['mem=1G'])
208                 self._ops.linux_bin(args, timeout, build_dir)
209                 outfile = get_outfile_path(build_dir)
210                 subprocess.call(['stty', 'sane'])
211                 with open(outfile, 'r') as file:
212                         for line in file:
213                                 yield line
214
215         def signal_handler(self, sig, frame):
216                 logging.error('Build interruption occurred. Cleaning console.')
217                 subprocess.call(['stty', 'sane'])