From c99e584b769011ec9897246586f74df01bd2f4f4 Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Thu, 10 Sep 2020 11:30:30 +0200 Subject: Add kconfig state space exploration script (random + neighbourhood) --- .gitmodules | 6 ++ bin/explore-kconfig.py | 83 +++++++++++++++++++ bin/versuchung | 1 + ext/kconfiglib | 1 + ext/versuchung | 1 + lib/kconfig.py | 213 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/kconfiglib.py | 1 + 7 files changed, 306 insertions(+) create mode 100644 .gitmodules create mode 100755 bin/explore-kconfig.py create mode 120000 bin/versuchung create mode 160000 ext/kconfiglib create mode 160000 ext/versuchung create mode 100644 lib/kconfig.py create mode 120000 lib/kconfiglib.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2baed33 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "kconfiglib"] + path = ext/kconfiglib + url = https://github.com/ulfalizer/Kconfiglib.git +[submodule "versuchung"] + path = ext/versuchung + url = https://github.com/stettberger/versuchung.git diff --git a/bin/explore-kconfig.py b/bin/explore-kconfig.py new file mode 100755 index 0000000..596faff --- /dev/null +++ b/bin/explore-kconfig.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import os +import sys + +from dfatool import kconfig + +from versuchung.experiment import Experiment +from versuchung.types import String, Bool, Integer +from versuchung.files import File, Directory + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__ + ) + parser.add_argument( + "--neighbourhood", + type=str, + help="Explore neighbourhood of provided .config file(s)", + ) + parser.add_argument( + "--log-level", + default=logging.INFO, + type=lambda level: getattr(logging, level.upper()), + help="Set log level", + ) + parser.add_argument( + "--random", + type=int, + help="Explore a number of random configurations (make randconfig)", + ) + parser.add_argument( + "--clean-command", type=str, help="Clean command", default="make clean" + ) + parser.add_argument( + "--build-command", type=str, help="Build command", default="make" + ) + parser.add_argument( + "--attribute-command", + type=str, + help="Attribute extraction command", + default="make attributes", + ) + parser.add_argument("project_root", type=str, help="Project root directory") + + args = parser.parse_args() + + if isinstance(args.log_level, int): + logging.basicConfig(level=args.log_level) + else: + print(f"Invalid log level. Setting log level to INFO.", file=sys.stderr) + + kconf = kconfig.KConfig(args.project_root) + + if args.clean_command: + kconf.clean_command = args.clean_command + if args.build_command: + kconf.build_command = args.build_command + if args.attribute_command: + kconf.attribute_command = args.attribute_command + + if args.random: + for i in range(args.random): + logging.info(f"Running experiment {i+1} of {args.random}") + kconf.run_randconfig() + + if args.neighbourhood: + if os.path.isfile(args.neighbourhood): + kconf.run_exploration_from_file(args.neighbourhood) + elif os.path.isdir(args.neighbourhood): + pass + else: + print( + f"--neighbourhod: Error: {args.neighbourhood} must be a file or directory, but is neither", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/bin/versuchung b/bin/versuchung new file mode 120000 index 0000000..57b45a8 --- /dev/null +++ b/bin/versuchung @@ -0,0 +1 @@ +../ext/versuchung/src/versuchung \ No newline at end of file diff --git a/ext/kconfiglib b/ext/kconfiglib new file mode 160000 index 0000000..061e71f --- /dev/null +++ b/ext/kconfiglib @@ -0,0 +1 @@ +Subproject commit 061e71f7d78cb057762d88de088055361863deff diff --git a/ext/versuchung b/ext/versuchung new file mode 160000 index 0000000..849520e --- /dev/null +++ b/ext/versuchung @@ -0,0 +1 @@ +Subproject commit 849520ee1eed198f52094b63fa71d32c50a7d0d1 diff --git a/lib/kconfig.py b/lib/kconfig.py new file mode 100644 index 0000000..b0301db --- /dev/null +++ b/lib/kconfig.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 + +import kconfiglib +import logging +import re +import shutil +import subprocess + +from versuchung.experiment import Experiment +from versuchung.types import String, Bool, Integer +from versuchung.files import File, Directory + +logger = logging.getLogger(__name__) + + +class AttributeExperiment(Experiment): + outputs = { + "config": File(".config"), + "attributes": File("attributes.json"), + "build_out": File("build.out"), + "build_err": File("build.err"), + } + + def run(self): + build_command = self.build_command.value.split() + attr_command = self.attr_command.value.split() + shutil.copyfile(f"{self.project_root.path}/.config", self.config.path) + subprocess.check_call( + ["make", "clean"], + cwd=self.project_root.path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + with open(self.build_out.path, "w") as out_fd, open( + self.build_err.path, "w" + ) as err_fd: + subprocess.check_call( + build_command, + cwd=self.project_root.path, + stdout=out_fd, + stderr=err_fd, + ) + except subprocess.CalledProcessError: + logger.info("build error") + return + with open(self.attributes.path, "w") as attr_fd: + subprocess.check_call( + attr_command, cwd=self.project_root.path, stdout=attr_fd + ) + + +class RandomConfig(AttributeExperiment): + inputs = { + "randconfig_seed": String("FIXME"), + "project_root": Directory("/tmp"), + "project_version": String("FIXME"), + "clean_command": String("make clean"), + "build_command": String("make"), + "attr_command": String("make attributes"), + } + + +class ExploreConfig(AttributeExperiment): + inputs = { + "config_hash": String("FIXME"), + "project_root": Directory("/tmp"), + "project_version": String("FIXME"), + "clean_command": String("make clean"), + "build_command": String("make"), + "attr_command": String("make attributes"), + } + + +class KConfig: + def __init__(self, working_directory): + self.cwd = working_directory + self.clean_command = "make clean" + self.build_command = "make" + self.attribute_command = "make attributes" + + def randconfig(self): + status = subprocess.run( + ["make", "randconfig"], + cwd=self.cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # make randconfig occasionally generates illegal configurations, so a project may run randconfig more than once. + # Make sure to return the seed of the latest run (don't short-circuit). + seed = None + for line in status.stderr.split("\n"): + match = re.match("KCONFIG_SEED=(.*)", line) + if match: + seed = match.group(1) + if seed: + return seed + raise RuntimeError("KCONFIG_SEED not found") + + def git_commit_id(self): + status = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=self.cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + revision = status.stdout.strip() + return revision + + def config_hash(self, config_file): + status = subprocess.run( + ["sha256sum", config_file], + cwd=self.cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + config_hash = status.stdout.split()[0] + return config_hash + + def run_randconfig(self): + """Run a randomconfig experiment in the selected project. Results are written to the current working directory.""" + experiment = RandomConfig() + experiment( + [ + "--randconfig_seed", + self.randconfig(), + "--project_version", + self.git_commit_id(), + "--project_root", + self.cwd, + "--clean_command", + self.clean_command, + "--build_command", + self.build_command, + "--attr_command", + self.attribute_command, + ] + ) + + def config_is_functional(self, kconf): + for choice in kconf.choices: + if ( + not choice.is_optional + and 2 in choice.assignable + and choice.selection is None + ): + return False + return True + + def run_exploration_from_file(self, config_file): + kconf = kconfiglib.Kconfig(f"{self.cwd}/Kconfig") + kconf.load_config(config_file) + symbols = list(kconf.syms.keys()) + + experiment = ExploreConfig() + shutil.copyfile(config_file, f"{self.cwd}/.config") + experiment( + [ + "--config_hash", + self.config_hash(config_file), + "--project_version", + self.git_commit_id(), + "--project_root", + self.cwd, + "--clean_command", + self.clean_command, + "--build_command", + self.build_command, + "--attr_command", + self.attribute_command, + ] + ) + + for symbol in kconf.syms.values(): + if kconfiglib.TYPE_TO_STR[symbol.type] != "bool": + continue + if symbol.tri_value == 0 and 2 in symbol.assignable: + logger.debug(f"Set {symbol.name} to y") + symbol.set_value(2) + elif symbol.tri_value == 2 and 0 in symbol.assignable: + logger.debug(f"Set {symbol.name} to n") + symbol.set_value(0) + else: + continue + + if not self.config_is_functional(kconf): + logger.debug("Configuration is non-functional") + kconf.load_config(config_file) + continue + + kconf.write_config(f"{self.cwd}/.config") + experiment = ExploreConfig() + experiment( + [ + "--config_hash", + self.config_hash(f"{self.cwd}/.config"), + "--project_version", + self.git_commit_id(), + "--project_root", + self.cwd, + "--clean_command", + self.clean_command, + "--build_command", + self.build_command, + "--attr_command", + self.attribute_command, + ] + ) + kconf.load_config(config_file) diff --git a/lib/kconfiglib.py b/lib/kconfiglib.py new file mode 120000 index 0000000..5b2f9ac --- /dev/null +++ b/lib/kconfiglib.py @@ -0,0 +1 @@ +../ext/kconfiglib/kconfiglib.py \ No newline at end of file -- cgit v1.2.3