From 7bbf537a285c99cc211bb4f3ae450b0a042a6296 Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Thu, 21 May 2026 15:47:28 +0200 Subject: [PATCH 1/9] Update evaluation format --- src/benchmarktool/runscript/runscript.py | 35 ++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/benchmarktool/runscript/runscript.py b/src/benchmarktool/runscript/runscript.py index aa0dc5d..1ae8836 100644 --- a/src/benchmarktool/runscript/runscript.py +++ b/src/benchmarktool/runscript/runscript.py @@ -95,10 +95,10 @@ def to_xml(self, out: Any, indent: str, settings: Optional[list["Setting"]] = No f'{indent}\n") if settings is None: settings = list(self.settings.values()) @@ -148,10 +148,10 @@ def to_xml(self, out: Any, indent: str) -> None: """ tag = " ".join(sorted(self.tag)) out.write(f'{indent} None: if enctag == "_default_": out.write(f'{indent}\t\n') else: - out.write(f'{indent}\t\n') + out.write(f'{indent}\t\n') out.write(f"{indent}\n") @@ -215,8 +215,11 @@ def _to_xml(self, out: Any, indent: str, xmltag: str, extra: str) -> None: """ out.write( f'{indent}<{xmltag} name="{self.name}" timeout="{self.timeout}" memout="{self.memout}" ' - f'runs="{self.runs}" template_options="{self.template_options}"{extra}' + f'runs="{self.runs}"' ) + if self.template_options != "": + out.write(f' template_options="{self.template_options}"') + out.write(extra) for key, val in self.attr.items(): out.write(f' {key}="{val}"') out.write("/>\n") @@ -850,13 +853,17 @@ def to_xml(self, out: Any, indent: str) -> None: indent (str): Amount of indentation """ out.write(f'{indent}\n") for instance in sorted(self.files): out.write(f'{indent}\t\n') + for encoding in sorted(self.encodings): + out.write(f'{indent}\t\n') out.write(f"{indent}\n") def paths(self) -> Iterator[str]: From 67077a10c809e104fcf1c615ba8f1d75beac99ff Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Thu, 21 May 2026 15:48:05 +0200 Subject: [PATCH 2/9] Update evaluation parsing --- src/benchmarktool/result/parser.py | 455 ++++++++++++++++++++++++----- src/benchmarktool/result/result.py | 54 ++-- 2 files changed, 421 insertions(+), 88 deletions(-) diff --git a/src/benchmarktool/result/parser.py b/src/benchmarktool/result/parser.py index df34955..b634ad6 100644 --- a/src/benchmarktool/result/parser.py +++ b/src/benchmarktool/result/parser.py @@ -4,7 +4,7 @@ @author: Roland Kaminski """ -from typing import Any, Optional +from typing import Any, Optional, TypeVar from lxml import etree # type: ignore[import-untyped] @@ -38,13 +38,13 @@ def __init__(self) -> None: """ Initializes the parser. """ - self.system_order = 0 self.result = Result() self.setting_order = 0 self.benchscope = False self.system: Optional[System] = None + self.setting: Optional[Setting] = None self.benchmark: Optional[Benchmark] = None self.benchclass: Optional[Class] = None self.classresult: Optional[ClassResult] = None @@ -53,22 +53,363 @@ def __init__(self) -> None: self.project: Optional[Project] = None self.run: Optional[Run] = None - def parse(self, infile: Any) -> Result: + @staticmethod + def _pop_required(attrib: dict[str, Any], key: str, tag: str) -> Any: """ - Parse a given result file and return its representation - in form of an instance of class Result. + Pop required XML attribute and raise a contextual ValueError when missing. Attributes: - infile (Any): The file to parse. + attrib (dict[str, Any]): The attribute dictionary to pop from. + key (str): The key of the required attribute. + tag (str): The name of the XML tag for error context. """ - # to reduce memory consumption especially for large result files - # do not use the full blown etree representation - parser = etree.XMLParser(target=self) - etree.parse(infile, parser) - assert isinstance(self.result, Result) - return self.result + if (value := attrib.pop(key, None)) is None: + raise ValueError(f"Missing required attribute '{key}' in {tag}") + return value + + @staticmethod + def _get_required(attrib: dict[str, Any], key: str, tag: str) -> Any: + """ + Read required XML attribute and raise a contextual ValueError when missing. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + key (str): The key of the required attribute. + tag (str): The name of the XML tag for error context. + """ + if (value := attrib.get(key)) is None: + raise ValueError(f"Missing required attribute '{key}' in {tag}") + return value + + @staticmethod + def _pop_required_int(attrib: dict[str, Any], key: str, tag: str) -> int: + """ + Parse a required integer XML attribute. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to pop from. + key (str): The key of the required attribute. + tag (str): The name of the XML tag for error context. + """ + value = Parser._pop_required(attrib, key, tag) + try: + return int(value) + except (TypeError, ValueError) as e: + raise ValueError(f"Invalid integer value for attribute '{key}' in {tag}: {value}") from e + + @staticmethod + def _get_required_int(attrib: dict[str, Any], key: str, tag: str) -> int: + """ + Parse a required integer XML attribute without removing it. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + key (str): The key of the required attribute. + tag (str): The name of the XML tag for error context. + """ + value = Parser._get_required(attrib, key, tag) + try: + return int(value) + except (TypeError, ValueError) as e: + raise ValueError(f"Invalid integer value for attribute '{key}' in {tag}: {value}") from e + + @staticmethod + def _pop_required_time(attrib: dict[str, Any], key: str, tag: str) -> int: + """ + Parse a required XML time attribute. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to pop from. + key (str): The key of the required attribute. + tag (str): The name of the XML tag for error context. + """ + value = Parser._pop_required(attrib, key, tag) + try: + return tools.xml_to_seconds_time(value) + except (TypeError, ValueError) as e: + raise ValueError(f"Invalid time value for attribute '{key}' in {tag}: {value}") from e + + T = TypeVar("T") + + @staticmethod + def _lookup(mapping: dict[Any, T], key: Any, obj: str, tag: str) -> T: + """ + Lookup an object in a mapping and fail with contextual ValueError. + + Attributes: + mapping (dict[Any, T]): The mapping to lookup from. + key (Any): The key to lookup. + obj (str): The description of the object being looked up. + tag (str): The name of the XML tag for error context. + """ + if (value := mapping.get(key)) is None: + raise ValueError(f"Unknown {obj} '{key}' referenced in {tag}") + return value + + def _parse_machine(self, attrib: dict[str, Any], tag: str) -> Machine: + """ + Parse Machine object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + return Machine( + self._get_required(attrib, "name", tag), + self._get_required(attrib, "cpu", tag), + self._get_required(attrib, "memory", tag), + ) + + def _parse_config(self, attrib: dict[str, Any], tag: str) -> Config: + """ + Parse Config object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + return Config( + self._get_required(attrib, "name", tag), + self._get_required(attrib, "template", tag), + ) + + def _parse_system(self, attrib: dict[str, Any], tag: str) -> System: + """ + Parse System object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + name = self._pop_required(attrib, "name", tag) + version = self._pop_required(attrib, "version", tag) + measures = self._pop_required(attrib, "measures", tag) + config_key = self._pop_required(attrib, "config", tag) + config = self._lookup(self.result.configs, config_key, "config", tag) + cmdline = {"pre": attrib.pop("cmdline", ""), "post": attrib.pop("cmdline_post", "")} + return System(name, version, measures, self.system_order, config, cmdline) + + def _parse_setting(self, attrib: dict[str, Any], tag: str) -> Setting: + """ + Parse Setting object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + setting_tag = self._pop_required(attrib, "tag", tag) + name = self._pop_required(attrib, "name", tag) + cmdline = {"pre": attrib.pop("cmdline", ""), "post": attrib.pop("cmdline_post", "")} + dist_template = attrib.pop("dist_template", "") + dist_options = attrib.pop("dist_options", "") + if self.system is None: + raise ValueError(f"Setting '{name}' defined outside of a system") + return Setting( + system=self.system, + name=name, + cmdline=cmdline, + tag=setting_tag, + order=self.setting_order, + dist_template=dist_template, + dist_options=dist_options, + attr=attrib, + encodings=dict(), + ) + + def _parse_encoding(self, attrib: dict[str, Any], tag: str) -> None: + """ + Parse Encoding object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + file = self._get_required(attrib, "file", tag) + encoding_tag = attrib.pop("encoding_tag", "_default_") + if self.setting is None: + raise ValueError(f"Encoding '{file}' defined outside of a setting") + self.setting.encodings.setdefault(encoding_tag, set()).add(file) + + def _parse_seqjob(self, attrib: dict[str, Any], tag: str) -> SeqJob: + """Parse SeqJob object.""" + name = self._pop_required(attrib, "name", tag) + timeout = self._pop_required_time(attrib, "timeout", tag) + memout = self._pop_required_int(attrib, "memout", tag) + runs = self._pop_required_int(attrib, "runs", tag) + template_options = attrib.pop("template_options", "") + parallel = self._pop_required_int(attrib, "parallel", tag) + return SeqJob( + name=name, + timeout=timeout, + runs=runs, + attr=attrib, + memout=memout, + template_options=template_options, + parallel=parallel, + ) + + def _parse_distjob(self, attrib: dict[str, Any], tag: str) -> DistJob: + """ + Parse DistJob object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + name = self._pop_required(attrib, "name", tag) + timeout = self._pop_required_time(attrib, "timeout", tag) + memout = self._pop_required_int(attrib, "memout", tag) + runs = self._pop_required_int(attrib, "runs", tag) + template_options = attrib.pop("template_options", "") + script_mode = self._pop_required(attrib, "script_mode", tag) + walltime = self._pop_required(attrib, "walltime", tag) + cpt = self._pop_required_int(attrib, "cpt", tag) + partition = self._pop_required(attrib, "partition", tag) + return DistJob( + name=name, + timeout=timeout, + runs=runs, + attr=attrib, + memout=memout, + template_options=template_options, + script_mode=script_mode, + walltime=walltime, + cpt=cpt, + partition=partition, + ) + + def _parse_benchmark(self, attrib: dict[str, Any], tag: str) -> Benchmark: + """ + Parse Benchmark object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + return Benchmark(self._get_required(attrib, "name", tag)) + + def _parse_project(self, attrib: dict[str, Any], tag: str) -> Project: + """ + Parse Project object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + return Project(self._get_required(attrib, "name", tag), self._get_required(attrib, "job", tag)) + + def _parse_runspec(self, attrib: dict[str, Any], tag: str) -> Runspec: + """ + Parse Runspec object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + system_name = self._get_required(attrib, "system", tag) + system_version = self._get_required(attrib, "version", tag) + machine_name = self._get_required(attrib, "machine", tag) + benchmark_name = self._get_required(attrib, "benchmark", tag) + setting_name = self._get_required(attrib, "setting", tag) + system = self._lookup(self.result.systems, (system_name, system_version), "system", tag) + machine = self._lookup(self.result.machines, machine_name, "machine", tag) + benchmark = self._lookup(self.result.benchmarks, benchmark_name, "benchmark", tag) + setting = self._lookup(system.settings, setting_name, "setting", tag) + if self.project is None: + raise ValueError(f"Runspec defined outside of a project") + runspec = Runspec(system, machine, benchmark, setting) + self.project.runspecs.append(runspec) + return runspec + + def _parse_class(self, attrib: dict[str, Any], tag: str) -> None: + """ + Parse Class object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + if self.benchscope: + if self.benchmark is None: + raise ValueError(f"Class defined outside of a benchmark") + self.benchclass = Class( + self.benchmark, + self._get_required(attrib, "name", tag), + self._get_required_int(attrib, "id", tag), + ) + self.benchmark.classes[self.benchclass.id] = self.benchclass + else: + if self.runspec is None: + raise ValueError(f"Class defined outside of a runspec") + benchclass = self._lookup( + self.runspec.benchmark.classes, + self._get_required_int(attrib, "id", tag), + "class", + tag, + ) + self.classresult = ClassResult(benchclass) + self.runspec.classresults.append(self.classresult) + + def _parse_instance(self, attrib: dict[str, Any], tag: str) -> None: + """ + Parse Instance object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + if self.benchscope: + if self.benchclass is None: + raise ValueError(f"Instance defined outside of a class") + cmdline = {"pre": attrib.pop("cmdline", ""), "post": attrib.pop("cmdline_post", "")} + instance = Instance( + self.benchclass, + self._get_required(attrib, "name", tag), + self._get_required_int(attrib, "id", tag), + cmdline, + ) + self.benchclass.instances[instance.id] = instance + else: + if self.classresult is None: + raise ValueError(f"Instance defined outside of a class result") + benchinst = self._lookup( + self.classresult.benchclass.instances, + self._get_required_int(attrib, "id", tag), + "instance", + tag, + ) + self.instresult = InstanceResult(benchinst) + self.classresult.instresults.append(self.instresult) + + def _parse_run(self, attrib: dict[str, Any], tag: str) -> None: + """ + Parse Run object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + if not self.benchscope: + if self.instresult is None: + raise ValueError(f"Run defined outside of an instance result") + self.run = Run(self.instresult, self._get_required_int(attrib, "number", tag)) + self.instresult.runs.append(self.run) + + def _parse_measure(self, attrib: dict[str, Any], tag: str) -> None: + """ + Parse measurement. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ + if self.run is None: + raise ValueError(f"Measure defined outside of a run") + name = self._get_required(attrib, "name", tag) + measure_type = self._get_required(attrib, "type", tag) + value = self._get_required(attrib, "val", tag) + self.run.measures[name] = (measure_type, value) - # pylint: disable=too-many-statements,too-many-branches def start(self, tag: str, attrib: dict[str, Any]) -> None: """ This method is called for every opening XML tag. @@ -79,89 +420,63 @@ def start(self, tag: str, attrib: dict[str, Any]) -> None: """ match tag: case "machine": - machine = Machine(attrib["name"], attrib["cpu"], attrib["memory"]) + machine = self._parse_machine(attrib, tag) self.result.machines[machine.name] = machine case "config": - config = Config(attrib["name"], attrib["template"]) + config = self._parse_config(attrib, tag) self.result.configs[config.name] = config case "system": - self.system = System( - attrib["name"], attrib["version"], attrib["config"], attrib["measures"], self.system_order - ) + self.system = self._parse_system(attrib, tag) self.result.systems[(self.system.name, self.system.version)] = self.system self.system_order += 1 self.setting_order = 0 case "setting": - tag = attrib.pop("tag", None) - name = attrib.pop("name") - cmdline = attrib.pop("cmdline") - assert self.system is not None - setting = Setting(self.system, name, cmdline, tag, self.setting_order, attrib) - self.system.settings[name] = setting + self.setting = self._parse_setting(attrib, tag) + self.system.settings[self.setting.name] = self.setting self.setting_order += 1 + case "encoding": + self._parse_encoding(attrib, tag) case "seqjob": - name = attrib.pop("name") - timeout = tools.xml_to_seconds_time(attrib.pop("timeout")) - runs = int(attrib.pop("runs")) - parallel = int(attrib.pop("parallel")) - seq_job = SeqJob(name, timeout, runs, attrib, parallel) + seq_job = self._parse_seqjob(attrib, tag) self.result.jobs[seq_job.name] = seq_job case "distjob": - name = attrib.pop("name") - timeout = tools.xml_to_seconds_time(attrib.pop("timeout")) - runs = int(attrib.pop("runs")) - script_mode = attrib.pop("script_mode") - walltime = attrib.pop("walltime") - partition = attrib.pop("partition") - dist_job = DistJob(name, timeout, runs, attrib, script_mode, walltime, partition) + dist_job = self._parse_distjob(attrib, tag) self.result.jobs[dist_job.name] = dist_job case "benchmark": self.benchscope = True - self.benchmark = Benchmark(attrib["name"]) + self.benchmark = self._parse_benchmark(attrib, tag) self.result.benchmarks[self.benchmark.name] = self.benchmark case "project": - self.project = Project(attrib["name"], attrib["job"]) + self.project = self._parse_project(attrib, tag) self.result.projects[self.project.name] = self.project case "runspec": self.benchscope = False - self.runspec = Runspec( - self.result.systems[(attrib["system"], attrib["version"])], - self.result.machines[attrib["machine"]], - self.result.benchmarks[attrib["benchmark"]], - self.result.systems[(attrib["system"], attrib["version"])].settings[attrib["setting"]], - ) - assert self.project is not None - self.project.runspecs.append(self.runspec) + self.runspec = self._parse_runspec(attrib, tag) case "class": - if self.benchscope: - assert self.benchmark is not None - self.benchclass = Class(self.benchmark, attrib["name"], int(attrib["id"])) - self.benchmark.classes[self.benchclass.id] = self.benchclass - else: - assert self.runspec is not None - benchclass = self.runspec.benchmark.classes[int(attrib["id"])] - self.classresult = ClassResult(benchclass) - self.runspec.classresults.append(self.classresult) + self._parse_class(attrib, tag) case "instance": - if self.benchscope: - assert self.benchclass is not None - instance = Instance(self.benchclass, attrib["name"], int(attrib["id"])) - self.benchclass.instances[instance.id] = instance - else: - assert self.classresult is not None - benchinst = self.classresult.benchclass.instances[int(attrib["id"])] - self.instresult = InstanceResult(benchinst) - self.classresult.instresults.append(self.instresult) + self._parse_instance(attrib, tag) case "run": - if not self.benchscope: - assert self.instresult is not None - self.run = Run(self.instresult, int(attrib["number"])) - self.instresult.runs.append(self.run) + self._parse_run(attrib, tag) case "measure": - assert self.run is not None - self.run.measures[attrib["name"]] = (attrib["type"], attrib["val"]) + self._parse_measure(attrib, tag) def close(self) -> None: """ This method is called for every closing XML tag. """ + + def parse(self, infile: Any) -> Result: + """ + Parse a given result file and return its representation + in form of an instance of class Result. + + Attributes: + infile (Any): The file to parse. + """ + # to reduce memory consumption especially for large result files + # do not use the full blown etree representation + parser = etree.XMLParser(target=self) + etree.parse(infile, parser) + assert isinstance(self.result, Result) + return self.result diff --git a/src/benchmarktool/result/result.py b/src/benchmarktool/result/result.py index c283654..9c09c25 100644 --- a/src/benchmarktool/result/result.py +++ b/src/benchmarktool/result/result.py @@ -171,21 +171,23 @@ class System: Attributes: name (str): The name of the system. version (str): The version. - config (str): The config (a string). measures (str): The measurement function (a string). order (int): An integer denoting the occurrence in the XML file. + config (Config): The system configuration. + cmdline (dict[str, str]): Command line parameters. settings (dict[str, Setting]): Dictionary of all system settings. """ name: str version: str - config: str = field(compare=False) measures: str = field(compare=False) - order: int + order: int = field(compare=False) + config: Config = field(compare=False) + cmdline: dict[str, str] = field(default_factory=dict, compare=False) settings: dict[str, "Setting"] = field(default_factory=dict, compare=False) -@dataclass(order=True, frozen=True) +@dataclass(order=True, frozen=True, kw_only=True) class Setting: """ Represents a setting. @@ -193,21 +195,28 @@ class Setting: Attributes: system (System): The system associated with the setting. name (str): The name of the setting. - cmdline (str): Command line parameters. + cmdline (dict[str, str]): Command line parameters. tag (str): Tags of the setting. order (int): An integer denoting the occurrence in the XML file. + dist_template (str): A path to the template file for distributed jobs. attr (dict[str, Any]): Arbitrary extra arguments. + dist_options (str): Additional options for distributed jobs. + encodings (dict[str, set[str]]): Mapping from encoding tags to sets of encoding files. """ system: "System" name: str - cmdline: str = field(compare=False) + cmdline: dict[str, str] = field(default_factory=dict, compare=False) tag: str = field(compare=False) - order: int + order: int = field(compare=False) + dist_template: str = field(compare=False) attr: dict[str, Any] = field(compare=False) + dist_options: str = field(default="", compare=False) + encodings: dict[str, set[str]] = field(compare=False, default_factory=dict) -@dataclass(order=True, frozen=True) + +@dataclass(order=True, frozen=True, kw_only=True) class Job: """ Represents a job. @@ -217,15 +226,19 @@ class Job: timeout (int): Timeout of the job. runs (int): Number of repetitions per instance. attr (dict[str, Any]): Arbitrary extra arguments. + memout (int): Memory limit for the job. + template_options (str): Additional options for the job template. """ name: str timeout: int = field(compare=False) runs: int = field(compare=False) attr: dict[str, Any] = field(compare=False) + memout: int = field(compare=False) + template_options: str = field(compare=False) -@dataclass(order=True, frozen=True) +@dataclass(order=True, frozen=True, kw_only=True) class SeqJob(Job): """ Represents a sequential job. @@ -234,29 +247,32 @@ class SeqJob(Job): name (str): The name of the job. timeout (int): Timeout of the job. runs (int): Number of repetitions per instance. - attrib (dict[str, Any]): Arbitrary extra arguments. + attr (dict[str, Any]): Arbitrary extra arguments. parallel (int): Number of processes to start in parallel. """ parallel: int = field(compare=False) -@dataclass(order=True, frozen=True) +@dataclass(order=True, frozen=True, kw_only=True) class DistJob(Job): """ - Represents a dist job. + Represents a distributed job. Attributes: name (str): The name of the job. timeout (int): Timeout of the job. runs (int): Number of repetitions per instance. - attrib (dict[str, Any]): Arbitrary extra arguments. + attr (dict[str, Any]): Arbitrary extra arguments. script_mode (str): Specifies the script generation mode. walltime (str): The walltime for a distributed job. + cpt (int): The number of cpus per task. + partition (str): The partition to run the job on. """ script_mode: str = field(compare=False) walltime: str = field(compare=False) + cpt: int = field(compare=False) partition: str = field(compare=False) @@ -320,16 +336,18 @@ class Instance: Represents a benchmark instance. Attributes: - benchclass (Class): The class of the instance. - name (str): The name of the benchmark. - id (int): A unique id (in the scope of the benchmark). - max_runs (int): Max number of runs. - values (dict[str, Any]): Mutable dict with helper values. + benchclass (Class): The class of the instance. + name (str): The name of the benchmark. + id (int): A unique id (in the scope of the benchmark). + max_runs (int): Max number of runs. + cmdline (dict[str, str]): Command line parameters. + values (dict[str, Any]): Mutable dict with helper values. """ benchclass: Class name: str id: int = field(compare=False) + cmdline: dict[str, str] = field(default_factory=dict, compare=False) values: dict[str, int] = field(default_factory=dict, compare=False) def __post_init__(self) -> None: From bf35a3e36dcdbef1c3378a7456dde7286f5a367a Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Fri, 22 May 2026 11:39:15 +0200 Subject: [PATCH 3/9] Fix instance scope --- src/benchmarktool/result/parser.py | 49 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/benchmarktool/result/parser.py b/src/benchmarktool/result/parser.py index b634ad6..0a3e139 100644 --- a/src/benchmarktool/result/parser.py +++ b/src/benchmarktool/result/parser.py @@ -64,7 +64,7 @@ def _pop_required(attrib: dict[str, Any], key: str, tag: str) -> Any: tag (str): The name of the XML tag for error context. """ if (value := attrib.pop(key, None)) is None: - raise ValueError(f"Missing required attribute '{key}' in {tag}") + raise ValueError(f"Missing required attribute '{key}' in {tag}") # nocoverage return value @staticmethod @@ -78,7 +78,7 @@ def _get_required(attrib: dict[str, Any], key: str, tag: str) -> Any: tag (str): The name of the XML tag for error context. """ if (value := attrib.get(key)) is None: - raise ValueError(f"Missing required attribute '{key}' in {tag}") + raise ValueError(f"Missing required attribute '{key}' in {tag}") # nocoverage return value @staticmethod @@ -94,7 +94,7 @@ def _pop_required_int(attrib: dict[str, Any], key: str, tag: str) -> int: value = Parser._pop_required(attrib, key, tag) try: return int(value) - except (TypeError, ValueError) as e: + except (TypeError, ValueError) as e: # nocoverage raise ValueError(f"Invalid integer value for attribute '{key}' in {tag}: {value}") from e @staticmethod @@ -110,7 +110,7 @@ def _get_required_int(attrib: dict[str, Any], key: str, tag: str) -> int: value = Parser._get_required(attrib, key, tag) try: return int(value) - except (TypeError, ValueError) as e: + except (TypeError, ValueError) as e: # nocoverage raise ValueError(f"Invalid integer value for attribute '{key}' in {tag}: {value}") from e @staticmethod @@ -126,7 +126,7 @@ def _pop_required_time(attrib: dict[str, Any], key: str, tag: str) -> int: value = Parser._pop_required(attrib, key, tag) try: return tools.xml_to_seconds_time(value) - except (TypeError, ValueError) as e: + except (TypeError, ValueError) as e: # nocoverage raise ValueError(f"Invalid time value for attribute '{key}' in {tag}: {value}") from e T = TypeVar("T") @@ -143,7 +143,7 @@ def _lookup(mapping: dict[Any, T], key: Any, obj: str, tag: str) -> T: tag (str): The name of the XML tag for error context. """ if (value := mapping.get(key)) is None: - raise ValueError(f"Unknown {obj} '{key}' referenced in {tag}") + raise ValueError(f"Unknown {obj} '{key}' referenced in {tag}") # nocoverage return value def _parse_machine(self, attrib: dict[str, Any], tag: str) -> Machine: @@ -203,7 +203,7 @@ def _parse_setting(self, attrib: dict[str, Any], tag: str) -> Setting: dist_template = attrib.pop("dist_template", "") dist_options = attrib.pop("dist_options", "") if self.system is None: - raise ValueError(f"Setting '{name}' defined outside of a system") + raise ValueError(f"Setting '{name}' defined outside of a system") # nocoverage return Setting( system=self.system, name=name, @@ -225,13 +225,23 @@ def _parse_encoding(self, attrib: dict[str, Any], tag: str) -> None: tag (str): The name of the XML tag for error context. """ file = self._get_required(attrib, "file", tag) - encoding_tag = attrib.pop("encoding_tag", "_default_") - if self.setting is None: - raise ValueError(f"Encoding '{file}' defined outside of a setting") - self.setting.encodings.setdefault(encoding_tag, set()).add(file) + if self.benchscope: + # instance encodings currently unused + return + else: + encoding_tag = attrib.pop("encoding_tag", "_default_") + if self.setting is None: + raise ValueError(f"Encoding '{file}' defined outside of a setting") # nocoverage + self.setting.encodings.setdefault(encoding_tag, set()).add(file) def _parse_seqjob(self, attrib: dict[str, Any], tag: str) -> SeqJob: - """Parse SeqJob object.""" + """ + Parse SeqJob object. + + Attributes: + attrib (dict[str, Any]): The attribute dictionary to read from. + tag (str): The name of the XML tag for error context. + """ name = self._pop_required(attrib, "name", tag) timeout = self._pop_required_time(attrib, "timeout", tag) memout = self._pop_required_int(attrib, "memout", tag) @@ -316,7 +326,7 @@ def _parse_runspec(self, attrib: dict[str, Any], tag: str) -> Runspec: benchmark = self._lookup(self.result.benchmarks, benchmark_name, "benchmark", tag) setting = self._lookup(system.settings, setting_name, "setting", tag) if self.project is None: - raise ValueError(f"Runspec defined outside of a project") + raise ValueError(f"Runspec defined outside of a project") # nocoverage runspec = Runspec(system, machine, benchmark, setting) self.project.runspecs.append(runspec) return runspec @@ -331,7 +341,7 @@ def _parse_class(self, attrib: dict[str, Any], tag: str) -> None: """ if self.benchscope: if self.benchmark is None: - raise ValueError(f"Class defined outside of a benchmark") + raise ValueError(f"Class defined outside of a benchmark") # nocoverage self.benchclass = Class( self.benchmark, self._get_required(attrib, "name", tag), @@ -340,7 +350,7 @@ def _parse_class(self, attrib: dict[str, Any], tag: str) -> None: self.benchmark.classes[self.benchclass.id] = self.benchclass else: if self.runspec is None: - raise ValueError(f"Class defined outside of a runspec") + raise ValueError(f"Class defined outside of a runspec") # nocoverage benchclass = self._lookup( self.runspec.benchmark.classes, self._get_required_int(attrib, "id", tag), @@ -360,8 +370,9 @@ def _parse_instance(self, attrib: dict[str, Any], tag: str) -> None: """ if self.benchscope: if self.benchclass is None: - raise ValueError(f"Instance defined outside of a class") + raise ValueError(f"Instance defined outside of a class") # nocoverage cmdline = {"pre": attrib.pop("cmdline", ""), "post": attrib.pop("cmdline_post", "")} + # instance and encoding files and encoding_tag currently unused instance = Instance( self.benchclass, self._get_required(attrib, "name", tag), @@ -371,7 +382,7 @@ def _parse_instance(self, attrib: dict[str, Any], tag: str) -> None: self.benchclass.instances[instance.id] = instance else: if self.classresult is None: - raise ValueError(f"Instance defined outside of a class result") + raise ValueError(f"Instance defined outside of a class result") # nocoverage benchinst = self._lookup( self.classresult.benchclass.instances, self._get_required_int(attrib, "id", tag), @@ -391,7 +402,7 @@ def _parse_run(self, attrib: dict[str, Any], tag: str) -> None: """ if not self.benchscope: if self.instresult is None: - raise ValueError(f"Run defined outside of an instance result") + raise ValueError(f"Run defined outside of an instance result") # nocoverage self.run = Run(self.instresult, self._get_required_int(attrib, "number", tag)) self.instresult.runs.append(self.run) @@ -404,7 +415,7 @@ def _parse_measure(self, attrib: dict[str, Any], tag: str) -> None: tag (str): The name of the XML tag for error context. """ if self.run is None: - raise ValueError(f"Measure defined outside of a run") + raise ValueError(f"Measure defined outside of a run") # nocoverage name = self._get_required(attrib, "name", tag) measure_type = self._get_required(attrib, "type", tag) value = self._get_required(attrib, "val", tag) From 55e319929194d189f1c01f9d63fc77176942586c Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Fri, 22 May 2026 11:39:52 +0200 Subject: [PATCH 4/9] Update attribute names --- src/benchmarktool/runscript/runscript.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/benchmarktool/runscript/runscript.py b/src/benchmarktool/runscript/runscript.py index 1ae8836..e80dbfa 100644 --- a/src/benchmarktool/runscript/runscript.py +++ b/src/benchmarktool/runscript/runscript.py @@ -858,12 +858,12 @@ def to_xml(self, out: Any, indent: str) -> None: if post := self.cmdline.get("post"): out.write(f' cmdline_post="{post}"') if enctags := " ".join(self.enctags): - out.write(f' enctags="{enctags}"') + out.write(f' encoding_tag="{enctags}"') out.write(">\n") for instance in sorted(self.files): out.write(f'{indent}\t\n') for encoding in sorted(self.encodings): - out.write(f'{indent}\t\n') + out.write(f'{indent}\t\n') out.write(f"{indent}\n") def paths(self) -> Iterator[str]: From c320cdca92417cb93f6aeac01fb23e5f0c11f739 Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Fri, 22 May 2026 11:42:12 +0200 Subject: [PATCH 5/9] Update tests --- tests/ref/test_eval.xml | 16 +++-- tests/result/test_result_classes.py | 72 +++++++++++++++++++---- tests/result/test_result_parser.py | 27 +++++++-- tests/runscript/test_runscript_classes.py | 21 ++++--- 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/tests/ref/test_eval.xml b/tests/ref/test_eval.xml index 729f71a..00b3890 100644 --- a/tests/ref/test_eval.xml +++ b/tests/ref/test_eval.xml @@ -1,17 +1,21 @@ - - - + + + + + + - - + + - + + diff --git a/tests/result/test_result_classes.py b/tests/result/test_result_classes.py index a938cf7..95a451a 100644 --- a/tests/result/test_result_classes.py +++ b/tests/result/test_result_classes.py @@ -150,14 +150,17 @@ def test_init(self): """ name = "name" ver = "ver" - conf = "conf" + conf = result.Config("conf", "temp") measure = "measure" order = 0 - s = result.System(name, ver, conf, measure, order) + cmdline = {"pre": "cmdline", "post": "cmdline_post"} + s = result.System(name, ver, measure, order, conf, cmdline) self.assertEqual(s.name, name) self.assertEqual(s.version, ver) self.assertEqual(s.config, conf) self.assertEqual(s.measures, measure) + self.assertEqual(s.order, order) + self.assertDictEqual(s.cmdline, cmdline) self.assertDictEqual(s.settings, {}) with self.assertRaises(FrozenInstanceError): @@ -175,16 +178,29 @@ def test_init(self): """ sys = mock.Mock(spec=result.System) name = "name" - cmd = "cmd" + cmd = {"pre": "cmdline", "post": "cmdline_post"} tag = "tag" order = 0 + dist_template = "dist_temp" + dist_options = "dist_opt" attr = {"a": "b"} - s = result.Setting(sys, name, cmd, tag, order, attr) + s = result.Setting( + system=sys, + name=name, + cmdline=cmd, + tag=tag, + order=order, + dist_template=dist_template, + dist_options=dist_options, + attr=attr, + ) self.assertEqual(s.system, sys) self.assertEqual(s.name, name) - self.assertEqual(s.cmdline, cmd) + self.assertDictEqual(s.cmdline, cmd) self.assertEqual(s.tag, tag) self.assertEqual(s.order, order) + self.assertEqual(s.dist_template, dist_template) + self.assertEqual(s.dist_options, dist_options) self.assertDictEqual(s.attr, attr) with self.assertRaises(FrozenInstanceError): @@ -199,9 +215,18 @@ class TestJob(TestCase): def setUp(self): self.name = "name" self.timeout = 10 + self.memout = 5000 self.runs = 2 self.attr = {"a": "b"} - self.j = result.Job(self.name, self.timeout, self.runs, self.attr) + self.template_options = "test_op" + self.j = result.Job( + name=self.name, + timeout=self.timeout, + memout=self.memout, + runs=self.runs, + attr=self.attr, + template_options=self.template_options, + ) def test_init(self): """ @@ -209,8 +234,10 @@ def test_init(self): """ self.assertEqual(self.j.name, self.name) self.assertEqual(self.j.timeout, self.timeout) + self.assertEqual(self.j.memout, self.memout) self.assertEqual(self.j.runs, self.runs) self.assertDictEqual(self.j.attr, self.attr) + self.assertEqual(self.j.template_options, self.template_options) with self.assertRaises(FrozenInstanceError): self.j.name = "new" @@ -224,7 +251,15 @@ class TestSeqJob(TestJob): def setUp(self): super().setUp() self.para = 2 - self.j = result.SeqJob(self.name, self.timeout, self.runs, self.attr, self.para) + self.j = result.SeqJob( + name=self.name, + timeout=self.timeout, + memout=self.memout, + runs=self.runs, + attr=self.attr, + template_options=self.template_options, + parallel=self.para, + ) def test_init(self): """ @@ -243,8 +278,20 @@ def setUp(self): super().setUp() self.sm = "sm" self.wt = "wt" + self.cpt = 2 self.pt = "pt" - self.j = result.DistJob(self.name, self.timeout, self.runs, self.attr, self.sm, self.wt, self.pt) + self.j = result.DistJob( + name=self.name, + timeout=self.timeout, + memout=self.memout, + runs=self.runs, + attr=self.attr, + template_options=self.template_options, + script_mode=self.sm, + walltime=self.wt, + cpt=self.cpt, + partition=self.pt, + ) def test_init(self): """ @@ -253,6 +300,7 @@ def test_init(self): super().test_init() self.assertEqual(self.j.script_mode, self.sm) self.assertEqual(self.j.walltime, self.wt) + self.assertEqual(self.j.cpt, self.cpt) self.assertEqual(self.j.partition, self.pt) @@ -316,11 +364,13 @@ def test_init(self): """ bcls = mock.Mock(spec=result.Class) name = "name" - ident = 0 - i = result.Instance(bcls, name, ident) + id = 0 + cmdline = {"pre": "cmdline", "post": "cmdline_post"} + i = result.Instance(bcls, name, id, cmdline) self.assertEqual(i.benchclass, bcls) self.assertEqual(i.name, name) - self.assertEqual(i.id, ident) + self.assertEqual(i.id, id) + self.assertDictEqual(i.cmdline, cmdline) self.assertDictEqual(i.values, {"row": 0, "max_runs": 0}) with self.assertRaises(FrozenInstanceError): diff --git a/tests/result/test_result_parser.py b/tests/result/test_result_parser.py index 9de3e6e..b0011cc 100644 --- a/tests/result/test_result_parser.py +++ b/tests/result/test_result_parser.py @@ -43,8 +43,9 @@ def test_parse(self): self.assertIsInstance(system, result.System) self.assertEqual(system.name, "test_sys") self.assertEqual(system.version, "1.0.0") - self.assertEqual(system.config, "test_config") + self.assertEqual(system.config, config) self.assertEqual(system.measures, "test") + self.assertDictEqual(system.cmdline, {"pre": "test_cmdline", "post": "test_cmdline_post"}) self.assertEqual(system.order, 0) # settings @@ -53,15 +54,17 @@ def test_parse(self): self.assertIsInstance(setting, result.Setting) self.assertEqual(setting.system.name, "test_sys") self.assertEqual(setting.name, "test_setting0") - self.assertEqual(setting.cmdline, "test_cmdline") + self.assertDictEqual(setting.cmdline, {"pre": "test_cmdline", "post": "test_cmdline_post"}) self.assertEqual(setting.tag, "basic") + self.assertEqual(setting.dist_template, "test_dist_template") + self.assertEqual(setting.dist_options, "test_dist_options") self.assertEqual(setting.order, 0) - self.assertDictEqual(setting.attr, {}) + self.assertDictEqual(setting.attr, {"attr": "test"}) setting = system.settings["test_setting2"] self.assertIsInstance(setting, result.Setting) self.assertEqual(setting.system.name, "test_sys") self.assertEqual(setting.name, "test_setting2") - self.assertEqual(setting.cmdline, "test_cmdline") + self.assertDictEqual(setting.cmdline, {"pre": "test_cmdline", "post": ""}) self.assertEqual(setting.tag, "basic") self.assertEqual(setting.order, 2) self.assertDictEqual(setting.attr, {}) @@ -72,14 +75,18 @@ def test_parse(self): self.assertIsInstance(seq_job, result.SeqJob) self.assertEqual(seq_job.name, "test_seq") self.assertEqual(seq_job.timeout, 10) + self.assertEqual(seq_job.memout, 5000) self.assertEqual(seq_job.runs, 2) + self.assertEqual(seq_job.template_options, "test_op") self.assertEqual(seq_job.parallel, 1) self.assertDictEqual(seq_job.attr, {}) dist_job = res.jobs["test_dist"] self.assertIsInstance(dist_job, result.DistJob) self.assertEqual(dist_job.name, "test_dist") self.assertEqual(dist_job.timeout, 10) + self.assertEqual(dist_job.memout, 3000) self.assertEqual(dist_job.runs, 2) + self.assertEqual(dist_job.template_options, "test_op") self.assertEqual(dist_job.script_mode, "timeout") self.assertEqual(dist_job.walltime, "70000") self.assertEqual(dist_job.partition, "test_partition") @@ -106,12 +113,22 @@ def test_parse(self): # instances self.assertEqual(len(benchclass.instances), 2) + inst = benchclass.instances[1] + self.assertIsInstance(inst, result.Instance) + self.assertEqual(inst.benchclass, benchclass) + self.assertEqual(inst.name, "test_inst11") + self.assertEqual(inst.id, 1) + self.assertDictEqual(inst.values, {"row": 0, "max_runs": 0}) + self.assertDictEqual(inst.cmdline, {"pre": "", "post": ""}) + benchclass = bench.classes[0] + self.assertEqual(len(benchclass.instances), 1) inst = benchclass.instances[0] self.assertIsInstance(inst, result.Instance) self.assertEqual(inst.benchclass, benchclass) - self.assertEqual(inst.name, "test_inst10") + self.assertEqual(inst.name, "test_inst00") self.assertEqual(inst.id, 0) self.assertDictEqual(inst.values, {"row": 0, "max_runs": 0}) + self.assertDictEqual(inst.cmdline, {"pre": "test_cmdline", "post": "test_cmdline_post"}) # projects self.assertEqual(len(res.projects), 2) diff --git a/tests/runscript/test_runscript_classes.py b/tests/runscript/test_runscript_classes.py index 6426e38..08c0439 100644 --- a/tests/runscript/test_runscript_classes.py +++ b/tests/runscript/test_runscript_classes.py @@ -141,8 +141,8 @@ def test_to_xml(self): '\t\n' '\t\t\n' - '\t\t\n' - '\t\t\n' + '\t\t\n' + '\t\t\n' "\t\n", ) @@ -157,7 +157,10 @@ def setUp(self): self.timeout = 20 self.runs = 2 self.attr = {"key": "val"} - self.j = runscript.Job(name=self.name, timeout=self.timeout, runs=self.runs, attr=self.attr) + self.template_options = "test_opt" + self.j = runscript.Job( + name=self.name, timeout=self.timeout, runs=self.runs, attr=self.attr, template_options=self.template_options + ) # pylint: disable=pointless-statement def test_eq(self): @@ -192,14 +195,16 @@ def test_to_xml(self): """ Test _to_xml method. """ - self.j = runscript.Job(name=self.name, timeout=self.timeout, runs=self.runs, attr=self.attr) + self.j = runscript.Job( + name=self.name, timeout=self.timeout, runs=self.runs, attr=self.attr, template_options=self.template_options + ) o = io.StringIO() tag = "tag" extra = " extra" self.j._to_xml(o, "\t", tag, extra) self.assertEqual( o.getvalue(), - '\t\n', + '\t\n', ) def test_script_gen(self): @@ -727,7 +732,7 @@ def setUp(self): self.name = "inst_name" self.files = {"file.lp"} self.encodings = {"encoding"} - self.enctags = set() + self.enctags = {"tag"} self.ins = runscript.Benchmark.Instance( self.location, self.benchclass, self.name, self.files, self.encodings, self.enctags ) @@ -744,8 +749,8 @@ def test_to_xml(self): ins.to_xml(o, "\t") self.assertEqual( o.getvalue(), - '\t\n' - '\t\t\n\t\n', + '\t\n' + '\t\t\n\t\t\n\t\n', ) def test_path(self): From c4736ac6447a2d98a56cb811bd4c440ae2281096 Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Fri, 22 May 2026 11:59:03 +0200 Subject: [PATCH 6/9] Improve documentation clarity --- docs/getting_started/gen/runscript.md | 8 +++++--- docs/getting_started/workflow/index.md | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/getting_started/gen/runscript.md b/docs/getting_started/gen/runscript.md index 6c24fcb..770e29e 100644 --- a/docs/getting_started/gen/runscript.md +++ b/docs/getting_started/gen/runscript.md @@ -261,7 +261,7 @@ benchmark class, and results are separated accordingly: ```xml - + ``` @@ -282,6 +282,7 @@ A `folder` element can contain any number of `encoding` and `ignore` elements: - `ignore`: Excludes folders from the benchmark by defining a path `prefix` to be ignored. - `encoding`: Specifies encodings to be used with all instances in the folder. +This path is relative to where the btool commands are called. ### File Elements @@ -290,7 +291,7 @@ manually add specific files using the `files` element: ```xml - + @@ -311,7 +312,8 @@ the system. Similar to the `system` and `setting` elements, `add` can also inclu `cmdline` and `cmdline_post` attributes. Command-line arguments always count for the entire group, e.g. with `files` element from above instance `instance` would be called with `--text -c n=4`. -- `encoding`: Specifies files which are added to every group/instance. +- `encoding`: Specifies a file which is added to every group/instance. This path is relative +to where the btool commands are called. The example above would result in a single benchmark instance `instance` which includes the files `default.lp` `file1.lp` and `file2.lp`. diff --git a/docs/getting_started/workflow/index.md b/docs/getting_started/workflow/index.md index d3d0777..ab39f65 100644 --- a/docs/getting_started/workflow/index.md +++ b/docs/getting_started/workflow/index.md @@ -30,9 +30,12 @@ setup you system-under-test (SUT) inside the `programs/` folder. If your job use option `--single` and your SUT is a shell script make sure to use `exec`. The name of your SUT should match the `-` given in the runscript. An example script for basic clingo can be found inside the `programs/` folder as `clingo-latest`. -Make sure your SUT is executable i.e., has the correct file permission. !!! info + Prior to starting your benchmarks, make sure your SUT (program) is executable i.e., has the correct + file permission. + +!!! warning When using `--single` avoid using pipes `|` in your scripts or make sure signals are properly propagated between processes. In general the use of pipes is strongly discouraged. From df48c629325483010af2cdf841e125d937b52683 Mon Sep 17 00:00:00 2001 From: Tom Schmidt <58591476+tzschmidt@users.noreply.github.com> Date: Fri, 22 May 2026 14:31:31 +0200 Subject: [PATCH 7/9] Fix formatting Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/getting_started/gen/runscript.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting_started/gen/runscript.md b/docs/getting_started/gen/runscript.md index 770e29e..0f9b3ad 100644 --- a/docs/getting_started/gen/runscript.md +++ b/docs/getting_started/gen/runscript.md @@ -282,7 +282,7 @@ A `folder` element can contain any number of `encoding` and `ignore` elements: - `ignore`: Excludes folders from the benchmark by defining a path `prefix` to be ignored. - `encoding`: Specifies encodings to be used with all instances in the folder. -This path is relative to where the btool commands are called. + This path is relative to where the btool commands are called. ### File Elements @@ -313,7 +313,7 @@ the system. Similar to the `system` and `setting` elements, `add` can also inclu entire group, e.g. with `files` element from above instance `instance` would be called with `--text -c n=4`. - `encoding`: Specifies a file which is added to every group/instance. This path is relative -to where the btool commands are called. + to where the btool commands are called. The example above would result in a single benchmark instance `instance` which includes the files `default.lp` `file1.lp` and `file2.lp`. From ddab1176f89587e626a58ed36880a0f89c25ad96 Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Fri, 22 May 2026 14:52:59 +0200 Subject: [PATCH 8/9] Satisfy linter --- src/benchmarktool/result/parser.py | 29 +++++++++++++++-------------- src/benchmarktool/result/result.py | 1 + tests/result/test_result_classes.py | 6 +++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/benchmarktool/result/parser.py b/src/benchmarktool/result/parser.py index 0a3e139..e5423bf 100644 --- a/src/benchmarktool/result/parser.py +++ b/src/benchmarktool/result/parser.py @@ -213,7 +213,7 @@ def _parse_setting(self, attrib: dict[str, Any], tag: str) -> Setting: dist_template=dist_template, dist_options=dist_options, attr=attrib, - encodings=dict(), + encodings={}, ) def _parse_encoding(self, attrib: dict[str, Any], tag: str) -> None: @@ -228,11 +228,10 @@ def _parse_encoding(self, attrib: dict[str, Any], tag: str) -> None: if self.benchscope: # instance encodings currently unused return - else: - encoding_tag = attrib.pop("encoding_tag", "_default_") - if self.setting is None: - raise ValueError(f"Encoding '{file}' defined outside of a setting") # nocoverage - self.setting.encodings.setdefault(encoding_tag, set()).add(file) + encoding_tag = attrib.pop("encoding_tag", "_default_") + if self.setting is None: + raise ValueError(f"Encoding '{file}' defined outside of a setting") # nocoverage + self.setting.encodings.setdefault(encoding_tag, set()).add(file) def _parse_seqjob(self, attrib: dict[str, Any], tag: str) -> SeqJob: """ @@ -326,7 +325,7 @@ def _parse_runspec(self, attrib: dict[str, Any], tag: str) -> Runspec: benchmark = self._lookup(self.result.benchmarks, benchmark_name, "benchmark", tag) setting = self._lookup(system.settings, setting_name, "setting", tag) if self.project is None: - raise ValueError(f"Runspec defined outside of a project") # nocoverage + raise ValueError("Runspec defined outside of a project") # nocoverage runspec = Runspec(system, machine, benchmark, setting) self.project.runspecs.append(runspec) return runspec @@ -341,7 +340,7 @@ def _parse_class(self, attrib: dict[str, Any], tag: str) -> None: """ if self.benchscope: if self.benchmark is None: - raise ValueError(f"Class defined outside of a benchmark") # nocoverage + raise ValueError("Class defined outside of a benchmark") # nocoverage self.benchclass = Class( self.benchmark, self._get_required(attrib, "name", tag), @@ -350,7 +349,7 @@ def _parse_class(self, attrib: dict[str, Any], tag: str) -> None: self.benchmark.classes[self.benchclass.id] = self.benchclass else: if self.runspec is None: - raise ValueError(f"Class defined outside of a runspec") # nocoverage + raise ValueError("Class defined outside of a runspec") # nocoverage benchclass = self._lookup( self.runspec.benchmark.classes, self._get_required_int(attrib, "id", tag), @@ -370,7 +369,7 @@ def _parse_instance(self, attrib: dict[str, Any], tag: str) -> None: """ if self.benchscope: if self.benchclass is None: - raise ValueError(f"Instance defined outside of a class") # nocoverage + raise ValueError("Instance defined outside of a class") # nocoverage cmdline = {"pre": attrib.pop("cmdline", ""), "post": attrib.pop("cmdline_post", "")} # instance and encoding files and encoding_tag currently unused instance = Instance( @@ -382,7 +381,7 @@ def _parse_instance(self, attrib: dict[str, Any], tag: str) -> None: self.benchclass.instances[instance.id] = instance else: if self.classresult is None: - raise ValueError(f"Instance defined outside of a class result") # nocoverage + raise ValueError("Instance defined outside of a class result") # nocoverage benchinst = self._lookup( self.classresult.benchclass.instances, self._get_required_int(attrib, "id", tag), @@ -402,7 +401,7 @@ def _parse_run(self, attrib: dict[str, Any], tag: str) -> None: """ if not self.benchscope: if self.instresult is None: - raise ValueError(f"Run defined outside of an instance result") # nocoverage + raise ValueError("Run defined outside of an instance result") # nocoverage self.run = Run(self.instresult, self._get_required_int(attrib, "number", tag)) self.instresult.runs.append(self.run) @@ -415,12 +414,13 @@ def _parse_measure(self, attrib: dict[str, Any], tag: str) -> None: tag (str): The name of the XML tag for error context. """ if self.run is None: - raise ValueError(f"Measure defined outside of a run") # nocoverage + raise ValueError("Measure defined outside of a run") # nocoverage name = self._get_required(attrib, "name", tag) measure_type = self._get_required(attrib, "type", tag) value = self._get_required(attrib, "val", tag) self.run.measures[name] = (measure_type, value) + # pylint: disable=too-many-branches def start(self, tag: str, attrib: dict[str, Any]) -> None: """ This method is called for every opening XML tag. @@ -443,7 +443,8 @@ def start(self, tag: str, attrib: dict[str, Any]) -> None: self.setting_order = 0 case "setting": self.setting = self._parse_setting(attrib, tag) - self.system.settings[self.setting.name] = self.setting + # self.system is guaranteed to be not None by _parse_setting, but mypy does not track this + self.system.settings[self.setting.name] = self.setting # type: ignore[union-attr] self.setting_order += 1 case "encoding": self._parse_encoding(attrib, tag) diff --git a/src/benchmarktool/result/result.py b/src/benchmarktool/result/result.py index 9c09c25..b34bef1 100644 --- a/src/benchmarktool/result/result.py +++ b/src/benchmarktool/result/result.py @@ -187,6 +187,7 @@ class System: settings: dict[str, "Setting"] = field(default_factory=dict, compare=False) +# pylint: disable=too-many-instance-attributes @dataclass(order=True, frozen=True, kw_only=True) class Setting: """ diff --git a/tests/result/test_result_classes.py b/tests/result/test_result_classes.py index 95a451a..dd47e48 100644 --- a/tests/result/test_result_classes.py +++ b/tests/result/test_result_classes.py @@ -364,12 +364,12 @@ def test_init(self): """ bcls = mock.Mock(spec=result.Class) name = "name" - id = 0 + inst_id = 0 cmdline = {"pre": "cmdline", "post": "cmdline_post"} - i = result.Instance(bcls, name, id, cmdline) + i = result.Instance(bcls, name, inst_id, cmdline) self.assertEqual(i.benchclass, bcls) self.assertEqual(i.name, name) - self.assertEqual(i.id, id) + self.assertEqual(i.id, inst_id) self.assertDictEqual(i.cmdline, cmdline) self.assertDictEqual(i.values, {"row": 0, "max_runs": 0}) From c6ab591f3ca498ba5b7c2aa4fa51364e33831aae Mon Sep 17 00:00:00 2001 From: tzschmidt Date: Mon, 1 Jun 2026 14:43:32 +0200 Subject: [PATCH 9/9] Update recommended python version --- README.md | 2 +- docs/getting_started/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40d68a5..5ce17a0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Python installation. To build the tool manually run the following commands: ```bash git clone https://github.com/potassco/benchmark-tool cd benchmark-tool -conda create -n python=3.10 +conda create -n python=3.14 conda activate pip install . ``` diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md index 485354b..59c1863 100644 --- a/docs/getting_started/index.md +++ b/docs/getting_started/index.md @@ -21,7 +21,7 @@ installation. To build the tool manually run the following commands: ```bash git clone https://github.com/potassco/benchmark-tool cd benchmark-tool -conda create -n python=3.10 +conda create -n python=3.14 conda activate pip install . ```