summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Friesel <daniel.friesel@uos.de>2020-09-10 11:30:30 +0200
committerDaniel Friesel <daniel.friesel@uos.de>2020-09-10 11:30:30 +0200
commitc99e584b769011ec9897246586f74df01bd2f4f4 (patch)
treeed976c5b320ad59f79fbe5e72aaa8a54775ac4e3
parentab33810fa92f8a262695077ae9504c836cd3c1a2 (diff)
Add kconfig state space exploration script (random + neighbourhood)
-rw-r--r--.gitmodules6
-rwxr-xr-xbin/explore-kconfig.py83
l---------bin/versuchung1
m---------ext/kconfiglib0
m---------ext/versuchung0
-rw-r--r--lib/kconfig.py213
l---------lib/kconfiglib.py1
7 files changed, 304 insertions, 0 deletions
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
+Subproject 061e71f7d78cb057762d88de088055361863def
diff --git a/ext/versuchung b/ext/versuchung
new file mode 160000
+Subproject 849520ee1eed198f52094b63fa71d32c50a7d0d
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