potassco-benchmark-tool 2.1.1__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,9 +7,9 @@ specified by the run script.
7
7
  __author__ = "Roland Kaminski"
8
8
 
9
9
  import importlib
10
- import importlib.util
11
10
  import os
12
11
  import re
12
+ import shutil
13
13
  import sys
14
14
  from dataclasses import dataclass, field
15
15
  from functools import total_ordering
@@ -44,7 +44,7 @@ class Machine:
44
44
  out (Any): Output stream to write to.
45
45
  indent (str): Amount of indentation.
46
46
  """
47
- out.write('{1}<machine name="{0.name}" cpu="{0.cpu}" memory="{0.memory}"/>\n'.format(self, indent))
47
+ out.write(f'{indent}<machine name="{self.name}" cpu="{self.cpu}" memory="{self.memory}"/>\n')
48
48
 
49
49
 
50
50
  @dataclass(order=True, frozen=True)
@@ -99,11 +99,11 @@ class System:
99
99
  settings = list(self.settings.values())
100
100
  for setting in sorted(settings, key=lambda s: s.order):
101
101
  setting.to_xml(out, indent + "\t")
102
- out.write("{0}</system>\n".format(indent))
102
+ out.write(f"{indent}</system>\n")
103
103
 
104
104
 
105
- # pylint: disable=too-many-instance-attributes, too-many-positional-arguments
106
- @dataclass(order=True, frozen=True)
105
+ # pylint: disable=too-many-instance-attributes
106
+ @dataclass(order=True, frozen=True, kw_only=True)
107
107
  class Setting:
108
108
  """
109
109
  Describes a setting for a system. This are command line options
@@ -117,7 +117,7 @@ class Setting:
117
117
  order (int): An integer specifying the order of settings.
118
118
  (This should denote the occurrence in the job specification.
119
119
  Again in the scope of a system.)
120
- disttemplate (str): Path to dist-template file. (dist only, related to mpi-version)
120
+ dist_template (str): Path to dist-template file. (dist only, related to mpi-version)
121
121
  attr (dict[str, Any]): A dictionary of additional optional attributes.
122
122
  dist_options (Optional[str]): Additional dist options for this setting.
123
123
  encodings (dict[str, set[str]]): Encodings used with this setting, keyed with tags.
@@ -127,7 +127,7 @@ class Setting:
127
127
  cmdline: str = field(compare=False)
128
128
  tag: set[str] = field(compare=False)
129
129
  order: int = field(compare=False)
130
- disttemplate: str = field(compare=False)
130
+ dist_template: str = field(compare=False)
131
131
  attr: dict[str, Any] = field(compare=False)
132
132
 
133
133
  dist_options: str = field(default="", compare=False)
@@ -142,25 +142,25 @@ class Setting:
142
142
  indent (str): Amount of indentation.
143
143
  """
144
144
  tag = " ".join(sorted(self.tag))
145
- out.write('{1}<setting name="{0.name}" cmdline="{0.cmdline}" tag="{2}"'.format(self, indent, tag))
146
- if self.disttemplate is not None:
147
- out.write(' {0}="{1}"'.format("disttemplate", self.disttemplate))
145
+ out.write(f'{indent}<setting name="{self.name}" cmdline="{self.cmdline}" tag="{tag}"')
146
+ if self.dist_template is not None:
147
+ out.write(f' dist_template="{self.dist_template}"')
148
148
  for key, val in self.attr.items():
149
- out.write(' {0}="{1}"'.format(key, val))
149
+ out.write(f' {key}="{val}"')
150
150
  if self.dist_options != "":
151
- out.write(' {0}="{1}"'.format("distopts", self.dist_options))
151
+ out.write(f' dist_options="{self.dist_options}"')
152
152
  out.write(">\n")
153
153
  for enctag, encodings in self.encodings.items():
154
154
  for enc in sorted(encodings):
155
155
  if enctag == "_default_":
156
- out.write('{0}<encoding file="{1}"/>\n'.format(indent + "\t", enc))
156
+ out.write(f'{indent}\t<encoding file="{enc}"/>\n')
157
157
  else:
158
- out.write('{0}<encoding file="{1}" tag="{2}"/>\n'.format(indent + "\t", enc, enctag))
159
- out.write("{0}</setting>\n".format(indent))
158
+ out.write(f'{indent}\t<encoding file="{enc}" tag="{enctag}"/>\n')
159
+ out.write(f"{indent}</setting>\n")
160
160
 
161
161
 
162
162
  @total_ordering
163
- @dataclass(eq=False, frozen=True)
163
+ @dataclass(eq=False, frozen=True, kw_only=True)
164
164
  class Job:
165
165
  """
166
166
  Base class for all jobs.
@@ -168,7 +168,9 @@ class Job:
168
168
  Attributes:
169
169
  name (str): A unique name for a job.
170
170
  timeout (int): A timeout in seconds for individual benchmark runs.
171
+ memout (int): A memory limit in MB for individual benchmark runs (20GB).
171
172
  runs (int): The number of runs per benchmark.
173
+ template_options (str): Template options.
172
174
  attr (dict[str, Any]): A dictionary of arbitrary attributes.
173
175
  """
174
176
 
@@ -176,6 +178,8 @@ class Job:
176
178
  timeout: int = field(compare=False)
177
179
  runs: int = field(compare=False)
178
180
  attr: dict[str, Any] = field(compare=False)
181
+ memout: int = field(compare=False, default=20000)
182
+ template_options: str = field(compare=False, default="")
179
183
 
180
184
  def __eq__(self, other: Any) -> bool:
181
185
  if not isinstance(other, Job):
@@ -201,10 +205,11 @@ class Job:
201
205
  extra (str): Additional arguments for the job.
202
206
  """
203
207
  out.write(
204
- '{1}<{2} name="{0.name}" timeout="{0.timeout}" runs="{0.runs}"{3}'.format(self, indent, xmltag, extra)
208
+ f'{indent}<{xmltag} name="{self.name}" timeout="{self.timeout}" memout="{self.memout}" '
209
+ f'runs="{self.runs}" template_options="{self.template_options}"{extra}'
205
210
  )
206
211
  for key, val in self.attr.items():
207
- out.write(' {0}="{1}"'.format(key, val))
212
+ out.write(f' {key}="{val}"')
208
213
  out.write("/>\n")
209
214
 
210
215
  def script_gen(self) -> Any:
@@ -214,74 +219,6 @@ class Job:
214
219
  raise NotImplementedError
215
220
 
216
221
 
217
- # pylint: disable=too-few-public-methods
218
- @dataclass
219
- class Run:
220
- """
221
- Base class for all runs.
222
-
223
- Attributes:
224
- path (str): Path that holds the target location for start scripts.
225
- root (str): directory relative to the location of the run's path.
226
- """
227
-
228
- path: str
229
-
230
- root: str = field(init=False)
231
-
232
- def __post_init__(self) -> None:
233
- self.root = os.path.relpath(".", self.path)
234
-
235
-
236
- # pylint: disable=too-many-instance-attributes, too-many-positional-arguments, too-few-public-methods
237
- @dataclass
238
- class SeqRun(Run):
239
- """
240
- Describes a sequential run.
241
-
242
- Attributes:
243
- path (str): Path that holds the target location for start scripts.
244
- run (int): The number of the run.
245
- job (Job): A reference to the job description.
246
- runspec (Runspec): A reference to the run description.
247
- instance (Benchmark.Instance): A reference to the instance to benchmark.
248
- root (str): Directory relative to the location of the run's path.
249
- files (str): Relative paths to all instances.
250
- encodings (str): Relative paths to all encodings.
251
- args (str): The command line arguments for this run.
252
- solver (str): The solver for this run.
253
- timeout (int): The timeout of this run.
254
- memout (int): The memory limit of this run.
255
- """
256
-
257
- run: int
258
- job: "Job"
259
- runspec: "Runspec"
260
- instance: "Benchmark.Instance"
261
-
262
- files: str = field(init=False)
263
- encodings: str = field(init=False)
264
- args: str = field(init=False)
265
- solver: str = field(init=False)
266
- timeout: int = field(init=False)
267
- memout: int = field(init=False)
268
-
269
- def __post_init__(self) -> None:
270
- super().__post_init__()
271
- self.files = " ".join([f'"{os.path.relpath(i, self.path)}"' for i in sorted(self.instance.paths())])
272
-
273
- encodings = self.instance.encodings
274
- encodings = encodings.union(self.runspec.setting.encodings.get("_default_", set()))
275
- for i in self.instance.enctags:
276
- encodings = encodings.union(self.runspec.setting.encodings.get(i, set()))
277
- self.encodings = " ".join([f'"{os.path.relpath(e, self.path)}"' for e in sorted(encodings)])
278
-
279
- self.args = self.runspec.setting.cmdline
280
- self.solver = self.runspec.system.name + "-" + self.runspec.system.version
281
- self.timeout = self.job.timeout
282
- self.memout = int(self.job.attr.get("memout", 20000))
283
-
284
-
285
222
  class ScriptGen:
286
223
  """
287
224
  A class providing basic functionality to generate
@@ -317,7 +254,7 @@ class ScriptGen:
317
254
  instance (Benchmark.Instance): The benchmark instance for the start script.
318
255
  run (int): The number of the run for the start script.
319
256
  """
320
- return os.path.join(runspec.path(), instance.benchclass.name, instance.name, "run%d" % run)
257
+ return os.path.join(runspec.path(), instance.benchclass.name, instance.name, f"run{run}")
321
258
 
322
259
  def add_to_script(self, runspec: "Runspec", instance: "Benchmark.Instance") -> None:
323
260
  """
@@ -338,13 +275,41 @@ class ScriptGen:
338
275
  continue
339
276
  with open(runspec.system.config.template, "r", encoding="utf8") as f:
340
277
  template = f.read()
278
+
279
+ template_options = self.job.template_options
280
+ if template_options != "":
281
+ template_options = " \\\n\t".join(template_options.split(","))
282
+ encodings = instance.encodings
283
+ encodings = encodings.union(runspec.setting.encodings.get("_default_", set()))
284
+ for i in instance.enctags:
285
+ encodings = encodings.union(runspec.setting.encodings.get(i, set()))
286
+ encodings_str = " ".join([f'"{os.path.relpath(e, path)}"' for e in sorted(encodings)])
287
+
341
288
  with open(startpath, "w", encoding="utf8") as startfile:
342
- startfile.write(template.format(run=SeqRun(path, run, self.job, runspec, instance)))
289
+ startfile.write(
290
+ template.format(
291
+ root=os.path.relpath(".", path),
292
+ options=template_options,
293
+ memout=self.job.memout,
294
+ timeout=self.job.timeout,
295
+ solver=runspec.system.name + "-" + runspec.system.version,
296
+ args=runspec.setting.cmdline,
297
+ files=" ".join([f'"{os.path.relpath(i, path)}"' for i in sorted(instance.paths())]),
298
+ encodings=encodings_str,
299
+ )
300
+ )
343
301
  self.startfiles.append((runspec, path, "start.sh"))
344
302
  tools.set_executable(startpath)
345
303
 
346
304
  def eval_results(
347
- self, out: Any, indent: str, runspec: "Runspec", instance: "Benchmark.Instance", parx: int = 2
305
+ self,
306
+ *,
307
+ out: Any,
308
+ indent: str,
309
+ runspec: "Runspec",
310
+ instance: "Benchmark.Instance",
311
+ result_parser: Optional[ModuleType],
312
+ parx: int = 2,
348
313
  ) -> None:
349
314
  """
350
315
  Parses the results of a given benchmark instance and outputs them as XML.
@@ -357,44 +322,12 @@ class ScriptGen:
357
322
  parx (int): Factor for penalized-average-runtime score.
358
323
  """
359
324
 
360
- def import_from_path(module_name: str, file_path: str) -> ModuleType: # nocoverage
361
- """
362
- Helper function to import modules from path.
363
-
364
- Attributes:
365
- module_name (str): Name of the module.
366
- file_path (str): Path to the module.
367
- """
368
- spec = importlib.util.spec_from_file_location(module_name, file_path)
369
- assert spec is not None
370
- module = importlib.util.module_from_spec(spec)
371
- sys.modules[module_name] = module
372
- assert spec.loader is not None
373
- spec.loader.exec_module(module)
374
- return module
375
-
376
- result_parser: Optional[ModuleType] = None
377
- # dynamicly import result parser
378
- # prioritize local resultparsers over included in package
379
- rp_name = "{0}".format(runspec.system.measures)
380
- try:
381
- result_parser = import_from_path(
382
- rp_name, os.path.join(os.getcwd(), "resultparsers", "{0}.py".format(runspec.system.measures))
383
- )
384
- except FileNotFoundError:
385
- try:
386
- result_parser = importlib.import_module(f"benchmarktool.resultparser.{rp_name}")
387
- except ModuleNotFoundError:
388
- sys.stderr.write(
389
- f"*** ERROR: Result parser import failed: {rp_name}! "
390
- "All runs using this parser will have no measures recorded!\n."
391
- )
392
325
  for run in range(1, self.job.runs + 1):
393
- out.write('{0}<run number="{1}">\n'.format(indent, run))
326
+ out.write(f'{indent}<run number="{run}">\n')
394
327
  # result parser call
395
328
  try:
396
329
  result: dict[str, tuple[str, Any]] = result_parser.parse( # type: ignore
397
- self._path(runspec, instance, run), runspec, instance
330
+ self._path(runspec, instance, run), runspec, instance, run
398
331
  )
399
332
  # penalized-average-runtime score
400
333
  if all(key in result for key in ["time", "timeout"]):
@@ -402,13 +335,12 @@ class ScriptGen:
402
335
  result[f"par{parx}"] = ("float", value)
403
336
 
404
337
  for key, (valtype, val) in sorted(result.items()):
405
- out.write(
406
- '{0}<measure name="{1}" type="{2}" val="{3}"/>\n'.format(indent + "\t", key, valtype, val)
407
- )
338
+ out.write(f'{indent}\t<measure name="{key}" type="{valtype}" val="{val}"/>\n')
408
339
  except AttributeError:
340
+ # skip if parser import failed or no parse function is defined
409
341
  pass
410
342
 
411
- out.write("{0}</run>\n".format(indent))
343
+ out.write(f"{indent}</run>\n")
412
344
 
413
345
 
414
346
  class SeqScriptGen(ScriptGen):
@@ -447,7 +379,7 @@ class SeqScriptGen(ScriptGen):
447
379
  comma = True
448
380
  queue += repr(os.path.join(relpath, instname))
449
381
  startfile.write(
450
- """\
382
+ f"""\
451
383
  #!/usr/bin/python -u
452
384
 
453
385
  import optparse
@@ -458,7 +390,7 @@ import sys
458
390
  import signal
459
391
  import time
460
392
 
461
- queue = [{0}]
393
+ queue = [{queue}]
462
394
 
463
395
  class Main:
464
396
  def __init__(self):
@@ -469,7 +401,7 @@ class Main:
469
401
  self.finished = threading.Condition()
470
402
  self.coreLock = threading.Lock()
471
403
  c = 0
472
- while len(self.cores) < {1}:
404
+ while len(self.cores) < {self.job.parallel}:
473
405
  self.cores.add(c)
474
406
  c += 1
475
407
 
@@ -488,7 +420,7 @@ class Main:
488
420
  thread = Run(cmd, self, core)
489
421
  self.started += 1
490
422
  self.running.add(thread)
491
- print("({{0}}/{{1}}/{{2}}/{{4}}) {{3}}".format(len(self.running), self.started, self.total, cmd, core))
423
+ print(f"({{len(self.running)}}/{{self.started}}/{{self.total}}/{{core}}) {{cmd}}")
492
424
  thread.start()
493
425
 
494
426
  def run(self, queue):
@@ -498,7 +430,7 @@ class Main:
498
430
  self.finished.acquire()
499
431
  self.total = len(queue)
500
432
  for cmd in queue:
501
- while len(self.running) >= {1}:
433
+ while len(self.running) >= {self.job.parallel}:
502
434
  self.finished.wait()
503
435
  self.start(cmd)
504
436
  while len(self.running) != 0:
@@ -595,9 +527,7 @@ if __name__ == '__main__':
595
527
 
596
528
  m = Main()
597
529
  m.run(queue)
598
- """.format(
599
- queue, self.job.parallel
600
- )
530
+ """
601
531
  )
602
532
  tools.set_executable(os.path.join(path, "start.py"))
603
533
 
@@ -638,13 +568,13 @@ class DistScriptGen(ScriptGen):
638
568
  assert isinstance(self.runspec.project, Project)
639
569
  assert isinstance(self.runspec.project.job, DistJob)
640
570
  self.num = 0
641
- with open(self.runspec.setting.disttemplate, "r", encoding="utf8") as f:
571
+ with open(self.runspec.setting.dist_template, "r", encoding="utf8") as f:
642
572
  template = f.read()
643
- script = os.path.join(self.path, "start{0:04}.dist".format(len(self.queue)))
573
+ script = os.path.join(self.path, f"start{len(self.queue):04}.dist")
644
574
  if self.runspec.setting.dist_options != "":
645
- distopts = "\n".join(self.runspec.setting.dist_options.split(",")) + "\n"
575
+ dist_options = "\n".join(self.runspec.setting.dist_options.split(",")) + "\n"
646
576
  else:
647
- distopts = ""
577
+ dist_options = ""
648
578
  with open(script, "w", encoding="utf8") as f:
649
579
  f.write(
650
580
  template.format(
@@ -652,7 +582,7 @@ class DistScriptGen(ScriptGen):
652
582
  jobs=self.startscripts,
653
583
  cpt=self.runspec.project.job.cpt,
654
584
  partition=self.runspec.project.job.partition,
655
- dist_options=distopts,
585
+ dist_options=dist_options,
656
586
  )
657
587
  )
658
588
  self.queue.append(script)
@@ -703,7 +633,7 @@ class DistScriptGen(ScriptGen):
703
633
  relpath = os.path.relpath(instpath, path)
704
634
  job_script = os.path.join(relpath, instname)
705
635
  dist_key = (
706
- runspec.setting.disttemplate,
636
+ runspec.setting.dist_template,
707
637
  runspec.setting.dist_options,
708
638
  runspec.project.job.walltime,
709
639
  runspec.project.job.cpt,
@@ -731,22 +661,23 @@ class DistScriptGen(ScriptGen):
731
661
 
732
662
  with open(os.path.join(path, "start.sh"), "w", encoding="utf8") as startfile:
733
663
  startfile.write(
734
- """#!/bin/bash\n\ncd "$(dirname $0)"\n"""
735
- + "\n".join(['sbatch "{0}"'.format(os.path.basename(x)) for x in queue])
664
+ '#!/bin/bash\n\ncd "$(dirname $0)"\n' + "\n".join([f'sbatch "{os.path.basename(x)}"' for x in queue])
736
665
  )
737
666
  tools.set_executable(os.path.join(path, "start.sh"))
738
667
 
739
668
 
740
- @dataclass(eq=False, frozen=True)
669
+ @dataclass(eq=False, frozen=True, kw_only=True)
741
670
  class SeqJob(Job):
742
671
  """
743
672
  Describes a sequential job.
744
673
 
745
674
  Attributes:
746
- name (str): A unique name for a job.
747
- timeout (int): A timeout in seconds for individual benchmark runs.
748
- runs (int): The number of runs per benchmark.
749
- attr (dict[str,Any]): A dictionary of arbitrary attributes.
675
+ name (str): A unique name for a job.
676
+ timeout (int): A timeout in seconds for individual benchmark runs.
677
+ memout (int): A memory limit in MB for individual benchmark runs (20GB).
678
+ runs (int): The number of runs per benchmark.
679
+ template_options (str): Template options.
680
+ attr (dict[str, Any]): A dictionary of arbitrary attributes.
750
681
  parallel (int): The number of runs that can be started in parallel.
751
682
  """
752
683
 
@@ -767,20 +698,22 @@ class SeqJob(Job):
767
698
  out (Any): Output stream to write to.
768
699
  indent (str): Amount of indentation.
769
700
  """
770
- extra = ' parallel="{0.parallel}"'.format(self)
701
+ extra = f' parallel="{self.parallel}"'
771
702
  Job._to_xml(self, out, indent, "seqjob", extra)
772
703
 
773
704
 
774
- @dataclass(eq=False, frozen=True)
705
+ @dataclass(eq=False, frozen=True, kw_only=True)
775
706
  class DistJob(Job):
776
707
  """
777
708
  Describes a dist job.
778
709
 
779
710
  Attributes:
780
- name (str): A unique name for a job.
781
- timeout (int): A timeout in seconds for individual benchmark runs.
782
- runs (int): The number of runs per benchmark.
783
- attr (dict[str,Any]): A dictionary of arbitrary attributes.
711
+ name (str): A unique name for a job.
712
+ timeout (int): A timeout in seconds for individual benchmark runs.
713
+ memout (int): A memory limit in MB for individual benchmark runs (20GB).
714
+ runs (int): The number of runs per benchmark.
715
+ template_options (str): Template options.
716
+ attr (dict[str, Any]): A dictionary of arbitrary attributes.
784
717
  script_mode (str): Specifies the script generation mode.
785
718
  walltime (int): The walltime for a distributed job.
786
719
  cpt (int): Number of cpus per task for distributed jobs.
@@ -790,7 +723,7 @@ class DistJob(Job):
790
723
  script_mode: str = field(compare=False)
791
724
  walltime: int = field(compare=False)
792
725
  cpt: int = field(compare=False)
793
- partition: str = field(compare=False)
726
+ partition: str = field(compare=False, default="kr")
794
727
 
795
728
  def to_xml(self, out: Any, indent: str) -> None:
796
729
  """
@@ -800,8 +733,9 @@ class DistJob(Job):
800
733
  out (Any): Output stream to write to
801
734
  indent (str): Amount of indentation
802
735
  """
803
- extra = ' script_mode="{0.script_mode}" walltime="{0.walltime}" cpt="{0.cpt}" partition="{0.partition}"'.format(
804
- self
736
+ extra = (
737
+ f' script_mode="{self.script_mode}" walltime="{self.walltime}" cpt="{self.cpt}" '
738
+ f'partition="{self.partition}"'
805
739
  )
806
740
  Job._to_xml(self, out, indent, "distjob", extra)
807
741
 
@@ -835,7 +769,7 @@ class Config:
835
769
  out (Any): Output stream to write to.
836
770
  indent (str): Amount of indentation.
837
771
  """
838
- out.write('{1}<config name="{0.name}" template="{0.template}"/>\n'.format(self, indent))
772
+ out.write(f'{indent}<config name="{self.name}" template="{self.template}"/>\n')
839
773
 
840
774
 
841
775
  @dataclass(order=True, unsafe_hash=True)
@@ -899,10 +833,10 @@ class Benchmark:
899
833
  out (Any): Output stream to write to
900
834
  indent (str): Amount of indentation
901
835
  """
902
- out.write('{1}<instance name="{0.name}" id="{0.id}">\n'.format(self, indent))
836
+ out.write(f'{indent}<instance name="{self.name}" id="{self.id}">\n')
903
837
  for instance in sorted(self.files):
904
- out.write('{1}<file name="{0}"/>\n'.format(instance, indent + "\t"))
905
- out.write("{0}</instance>\n".format(indent))
838
+ out.write(f'{indent}\t<file name="{instance}"/>\n')
839
+ out.write(f"{indent}</instance>\n")
906
840
 
907
841
  def paths(self) -> Iterator[str]:
908
842
  """
@@ -994,11 +928,12 @@ class Benchmark:
994
928
  for filename in files:
995
929
  if self._skip(relroot, filename):
996
930
  continue
997
- m = re.match(r"^(([^\.]+).*)\.[^.]+$", filename)
931
+ m = re.match(r"^(([^.]+(?:\.[^.]+)*?)(?:\.[^.]+)?)\.[^.]+$", filename)
998
932
  if m is None:
999
- raise RuntimeError("Invalid file name.")
933
+ sys.stderr.write(f"*** WARNING: skipping invalid file name: {filename}\n")
934
+ continue
1000
935
  if self.group:
1001
- # remove file extension, file.1.txt -> file
936
+ # remove last 2 file extensions, file.1.txt -> file
1002
937
  group = m.group(2)
1003
938
  else:
1004
939
  # remove last file extension, file.1.txt -> file.1
@@ -1007,7 +942,13 @@ class Benchmark:
1007
942
  instances[group] = set()
1008
943
  instances[group].add(filename)
1009
944
  for group, instfiles in instances.items():
1010
- benchmark.add_instance(self.path, relroot, (group, instfiles), self.encodings, self.enctags)
945
+ benchmark.add_instance(
946
+ root=self.path,
947
+ relroot=relroot,
948
+ files=(group, instfiles),
949
+ encodings=self.encodings,
950
+ enctags=self.enctags,
951
+ )
1011
952
 
1012
953
  class Files:
1013
954
  """
@@ -1035,9 +976,10 @@ class Benchmark:
1035
976
  group (Optional[str]): Instance group.
1036
977
  """
1037
978
  if group is None:
1038
- m = re.match(r"^(([^\.]+).*)\.[^.]+$", os.path.basename(path))
979
+ m = re.match(r"^([^.]+(?:\.[^.]+)*)\.[^.]+$", os.path.basename(path))
1039
980
  if m is None:
1040
- raise RuntimeError("Invalid file name.")
981
+ sys.stderr.write(f"*** WARNING: skipping invalid file name: {path}\n")
982
+ return
1041
983
  # remove file extension, file.1.txt -> file.1
1042
984
  group = m.group(1)
1043
985
  if group not in self.files:
@@ -1073,15 +1015,23 @@ class Benchmark:
1073
1015
  benchmark (Benchmark): The benchmark to be populated.
1074
1016
  """
1075
1017
  for group, files in self.files.items():
1076
- for file in files:
1077
- if not os.path.exists(os.path.join(self.path, file)):
1078
- raise FileNotFoundError("Specified instance file does not exist.")
1018
+ if not all(os.path.exists(os.path.join(self.path, file)) for file in files):
1019
+ sys.stderr.write(f"*** WARNING: skipping instance '{group}' due to missing files!\n")
1020
+ continue
1079
1021
  paths = list(map(os.path.split, sorted(files)))
1080
1022
  if len(set(map(lambda x: x[0], paths))) != 1:
1081
- raise RuntimeError("Instances of the same group must be in the same directory.")
1023
+ sys.stderr.write(
1024
+ f"*** WARNING: skipping instance '{group}' due to inconsistent file paths!\n"
1025
+ "Grouped files must be located in the same folder.\n"
1026
+ )
1027
+ continue
1082
1028
  relroot = paths[0][0]
1083
1029
  benchmark.add_instance(
1084
- self.path, relroot, (group, set(map(lambda x: x[1], paths))), self.encodings, self.enctags
1030
+ root=self.path,
1031
+ relroot=relroot,
1032
+ files=(group, set(map(lambda x: x[1], paths))),
1033
+ encodings=self.encodings,
1034
+ enctags=self.enctags,
1085
1035
  )
1086
1036
 
1087
1037
  def add_element(self, element: Any) -> None:
@@ -1094,7 +1044,7 @@ class Benchmark:
1094
1044
  self.elements.append(element)
1095
1045
 
1096
1046
  def add_instance(
1097
- self, root: str, relroot: str, files: tuple[str, set[str]], encodings: set[str], enctags: set[str]
1047
+ self, *, root: str, relroot: str, files: tuple[str, set[str]], encodings: set[str], enctags: set[str]
1098
1048
  ) -> None:
1099
1049
  """
1100
1050
  Adds an instance to the benchmark set. (This function
@@ -1152,14 +1102,14 @@ class Benchmark:
1152
1102
  indent (str): Amount of indentation.
1153
1103
  """
1154
1104
  self.init()
1155
- out.write('{1}<benchmark name="{0}">\n'.format(self.name, indent))
1105
+ out.write(f'{indent}<benchmark name="{self.name}">\n')
1156
1106
  for classname in sorted(self.instances.keys()):
1157
1107
  instances = self.instances[classname]
1158
- out.write('{1}<class name="{0.name}" id="{0.id}">\n'.format(classname, indent + "\t"))
1108
+ out.write(f'{indent}\t<class name="{classname.name}" id="{classname.id}">\n')
1159
1109
  for instance in sorted(instances):
1160
1110
  instance.to_xml(out, indent + "\t\t")
1161
- out.write("{0}</class>\n".format(indent + "\t"))
1162
- out.write("{0}</benchmark>\n".format(indent))
1111
+ out.write(f"{indent}\t</class>\n")
1112
+ out.write(f"{indent}</benchmark>\n")
1163
1113
 
1164
1114
 
1165
1115
  @dataclass(order=True, frozen=True)
@@ -1187,7 +1137,7 @@ class Runspec:
1187
1137
  Returns an output path under which start scripts
1188
1138
  and benchmark results are stored.
1189
1139
  """
1190
- name = self.system.name + "-" + self.system.version + "-" + self.setting.name
1140
+ name = f"{self.system.name}-{self.system.version}-{self.setting.name}"
1191
1141
  return os.path.join(self.project.path(), self.machine.name, "results", self.benchmark.name, name)
1192
1142
 
1193
1143
  def gen_scripts(self, script_gen: "ScriptGen") -> None:
@@ -1237,10 +1187,16 @@ class Project:
1237
1187
  for system in self.runscript.systems.values():
1238
1188
  for setting in system.settings.values():
1239
1189
  if disj.match(setting.tag):
1240
- self.add_runspec(machine_name, system.name, system.version, setting.name, benchmark_name)
1190
+ self.add_runspec(
1191
+ machine_name=machine_name,
1192
+ system_name=system.name,
1193
+ system_version=system.version,
1194
+ setting_name=setting.name,
1195
+ benchmark_name=benchmark_name,
1196
+ )
1241
1197
 
1242
1198
  def add_runspec(
1243
- self, machine_name: str, system_name: str, version: str, setting_name: str, benchmark_name: str
1199
+ self, *, machine_name: str, system_name: str, system_version: str, setting_name: str, benchmark_name: str
1244
1200
  ) -> None:
1245
1201
  """
1246
1202
  Adds a run specification, described by machine, system+settings, and benchmark set,
@@ -1255,8 +1211,8 @@ class Project:
1255
1211
  """
1256
1212
  runspec = Runspec(
1257
1213
  self.runscript.machines[machine_name],
1258
- self.runscript.systems[(system_name, version)],
1259
- self.runscript.systems[(system_name, version)].settings[setting_name],
1214
+ self.runscript.systems[(system_name, system_version)],
1215
+ self.runscript.systems[(system_name, system_version)].settings[setting_name],
1260
1216
  self.runscript.benchmarks[benchmark_name],
1261
1217
  self,
1262
1218
  )
@@ -1358,11 +1314,17 @@ class Runscript:
1358
1314
  """
1359
1315
  self.projects[project.name] = project
1360
1316
 
1361
- def gen_scripts(self, skip: bool) -> None:
1317
+ def gen_scripts(self, skip: bool, force: bool = False) -> None:
1362
1318
  """
1363
1319
  Generates the start scripts for all benchmarks described by
1364
1320
  this run script.
1365
1321
  """
1322
+ if os.path.isdir(self.output):
1323
+ if force:
1324
+ shutil.rmtree(self.output)
1325
+ else:
1326
+ sys.stderr.write("*** ERROR: Output directory already exists.\n")
1327
+ sys.exit(1)
1366
1328
  for project in self.projects.values():
1367
1329
  project.gen_scripts(skip)
1368
1330
 
@@ -1372,6 +1334,36 @@ class Runscript:
1372
1334
  """
1373
1335
  return self.output
1374
1336
 
1337
+ def _get_result_parser(self, runspec: Runspec) -> Optional[ModuleType]: # nocoverage
1338
+ """
1339
+ Helper function to obtain result parsers.
1340
+
1341
+ Attributes:
1342
+ runspec (Runspec): The run specification.
1343
+ """
1344
+ result_parser: Optional[ModuleType] = None
1345
+ # dynamicly import result parser
1346
+ # prioritize local resultparsers over included in package
1347
+ rp_name = runspec.system.measures
1348
+ try:
1349
+ result_parser = tools.import_from_path(rp_name, os.path.join(os.getcwd(), "resultparsers", f"{rp_name}.py"))
1350
+ except FileNotFoundError:
1351
+ try:
1352
+ result_parser = importlib.import_module(f"benchmarktool.resultparser.{rp_name}")
1353
+ except ModuleNotFoundError:
1354
+ sys.stderr.write(
1355
+ f"*** WARNING: Import of resultparser '{rp_name}' for system "
1356
+ f"'{runspec.system.name}-{runspec.system.version}' failed! "
1357
+ "All runs using this parser will have no measures recorded!\n"
1358
+ )
1359
+ if result_parser is not None and not callable(getattr(result_parser, "parse", None)):
1360
+ sys.stderr.write(
1361
+ f"*** WARNING: Resultparser '{rp_name}' for system "
1362
+ f"'{runspec.system.name}-{runspec.system.version}' has no callable 'parse' function! "
1363
+ "All runs using this parser will have no measures recorded!\n"
1364
+ )
1365
+ return result_parser
1366
+
1375
1367
  # pylint: disable=too-many-branches
1376
1368
  def eval_results(self, out: Any, parx: int = 2) -> None:
1377
1369
  """
@@ -1416,24 +1408,31 @@ class Runscript:
1416
1408
 
1417
1409
  for project in self.projects.values():
1418
1410
  assert isinstance(project.job, (SeqJob, DistJob))
1419
- out.write('\t<project name="{0.name}" job="{0.job.name}">\n'.format(project))
1411
+ out.write(f'\t<project name="{project.name}" job="{project.job.name}">\n')
1420
1412
  job_gen = project.job.script_gen()
1421
1413
  jobs.add(project.job)
1422
1414
  for runspecs in project.runspecs.values():
1423
1415
  for runspec in runspecs:
1424
1416
  out.write(
1425
1417
  (
1426
- '\t\t<runspec machine="{0.machine.name}" system="{0.system.name}" '
1427
- 'version="{0.system.version}" benchmark="{0.benchmark.name}" '
1428
- 'setting="{0.setting.name}">\n'
1429
- ).format(runspec)
1418
+ f'\t\t<runspec machine="{runspec.machine.name}" system="{runspec.system.name}" '
1419
+ f'version="{runspec.system.version}" benchmark="{runspec.benchmark.name}" '
1420
+ f'setting="{runspec.setting.name}">\n'
1421
+ )
1430
1422
  )
1431
1423
  for classname in sorted(runspec.benchmark.instances):
1432
- out.write('\t\t\t<class id="{0.id}">\n'.format(classname))
1424
+ out.write(f'\t\t\t<class id="{classname.id}">\n')
1433
1425
  instances = runspec.benchmark.instances[classname]
1434
1426
  for instance in instances:
1435
- out.write('\t\t\t\t<instance id="{0.id}">\n'.format(instance))
1436
- job_gen.eval_results(out, "\t\t\t\t\t", runspec, instance, parx)
1427
+ out.write(f'\t\t\t\t<instance id="{instance.id}">\n')
1428
+ job_gen.eval_results(
1429
+ out=out,
1430
+ indent="\t\t\t\t\t",
1431
+ runspec=runspec,
1432
+ instance=instance,
1433
+ result_parser=self._get_result_parser(runspec),
1434
+ parx=parx,
1435
+ )
1437
1436
  out.write("\t\t\t\t</instance>\n")
1438
1437
  out.write("\t\t\t</class>\n")
1439
1438
  out.write("\t\t</runspec>\n")