SbyProc: New error_callback instead of exit_callback for failing procs
[SymbiYosys.git] / sbysrc / sby_core.py
1 #
2 # SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows
3 #
4 # Copyright (C) 2016 Claire Xenia Wolf <claire@yosyshq.com>
5 #
6 # Permission to use, copy, modify, and/or distribute this software for any
7 # purpose with or without fee is hereby granted, provided that the above
8 # copyright notice and this permission notice appear in all copies.
9 #
10 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 #
18
19 import os, re, sys, signal, platform
20 if os.name == "posix":
21 import resource, fcntl
22 import subprocess
23 from shutil import copyfile, copytree, rmtree
24 from select import select
25 from time import time, localtime, sleep, strftime
26 from sby_design import SbyProperty, SbyModule, design_hierarchy
27
28 all_procs_running = []
29
30 def force_shutdown(signum, frame):
31 print("SBY ---- Keyboard interrupt or external termination signal ----", flush=True)
32 for proc in list(all_procs_running):
33 proc.terminate()
34 sys.exit(1)
35
36 if os.name == "posix":
37 signal.signal(signal.SIGHUP, force_shutdown)
38 signal.signal(signal.SIGINT, force_shutdown)
39 signal.signal(signal.SIGTERM, force_shutdown)
40
41 def process_filename(filename):
42 if filename.startswith("~/"):
43 filename = os.environ['HOME'] + filename[1:]
44
45 filename = os.path.expandvars(filename)
46
47 return filename
48
49 class SbyProc:
50 def __init__(self, task, info, deps, cmdline, logfile=None, logstderr=True, silent=False):
51 self.running = False
52 self.finished = False
53 self.terminated = False
54 self.checkretcode = False
55 self.task = task
56 self.info = info
57 self.deps = deps
58 if os.name == "posix":
59 self.cmdline = cmdline
60 else:
61 # Windows command interpreter equivalents for sequential
62 # commands (; => &) command grouping ({} => ()).
63 replacements = {
64 ";" : "&",
65 "{" : "(",
66 "}" : ")",
67 }
68 parts = cmdline.split("'")
69 for i in range(len(parts)):
70 if i % 2 == 0:
71 cmdline_copy = parts[i]
72 for u, w in replacements.items():
73 cmdline_copy = cmdline_copy.replace(u, w)
74 parts[i] = cmdline_copy
75 self.cmdline = '"'.join(parts)
76 self.logfile = logfile
77 self.noprintregex = None
78 self.notify = []
79 self.linebuffer = ""
80 self.logstderr = logstderr
81 self.silent = silent
82
83 self.task.procs_pending.append(self)
84
85 for dep in self.deps:
86 dep.register_dep(self)
87
88 self.output_callback = None
89 self.exit_callback = None
90 self.error_callback = None
91
92 def register_dep(self, next_proc):
93 if self.finished:
94 next_proc.poll()
95 else:
96 self.notify.append(next_proc)
97
98 def log(self, line):
99 if line is not None and (self.noprintregex is None or not self.noprintregex.match(line)):
100 if self.logfile is not None:
101 print(line, file=self.logfile)
102 self.task.log(f"{self.info}: {line}")
103
104 def handle_output(self, line):
105 if self.terminated or len(line) == 0:
106 return
107 if self.output_callback is not None:
108 line = self.output_callback(line)
109 self.log(line)
110
111 def handle_exit(self, retcode):
112 if self.terminated:
113 return
114 if self.logfile is not None:
115 self.logfile.close()
116 if self.exit_callback is not None:
117 self.exit_callback(retcode)
118
119 def handle_error(self, retcode):
120 if self.terminated:
121 return
122 if self.logfile is not None:
123 self.logfile.close()
124 if self.error_callback is not None:
125 self.error_callback(retcode)
126
127 def terminate(self, timeout=False):
128 if self.task.opt_wait and not timeout:
129 return
130 if self.running:
131 if not self.silent:
132 self.task.log(f"{self.info}: terminating process")
133 if os.name == "posix":
134 try:
135 os.killpg(self.p.pid, signal.SIGTERM)
136 except PermissionError:
137 pass
138 self.p.terminate()
139 self.task.procs_running.remove(self)
140 all_procs_running.remove(self)
141 self.terminated = True
142
143 def poll(self):
144 if self.finished or self.terminated:
145 return
146
147 if not self.running:
148 for dep in self.deps:
149 if not dep.finished:
150 return
151
152 if not self.silent:
153 self.task.log(f"{self.info}: starting process \"{self.cmdline}\"")
154
155 if os.name == "posix":
156 def preexec_fn():
157 signal.signal(signal.SIGINT, signal.SIG_IGN)
158 os.setpgrp()
159
160 self.p = subprocess.Popen(["/usr/bin/env", "bash", "-c", self.cmdline], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
161 stderr=(subprocess.STDOUT if self.logstderr else None), preexec_fn=preexec_fn)
162
163 fl = fcntl.fcntl(self.p.stdout, fcntl.F_GETFL)
164 fcntl.fcntl(self.p.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)
165
166 else:
167 self.p = subprocess.Popen(self.cmdline, shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
168 stderr=(subprocess.STDOUT if self.logstderr else None))
169
170 self.task.procs_pending.remove(self)
171 self.task.procs_running.append(self)
172 all_procs_running.append(self)
173 self.running = True
174 return
175
176 while True:
177 outs = self.p.stdout.readline().decode("utf-8")
178 if len(outs) == 0: break
179 if outs[-1] != '\n':
180 self.linebuffer += outs
181 break
182 outs = (self.linebuffer + outs).strip()
183 self.linebuffer = ""
184 self.handle_output(outs)
185
186 if self.p.poll() is not None:
187 if not self.silent:
188 self.task.log(f"{self.info}: finished (returncode={self.p.returncode})")
189 self.task.procs_running.remove(self)
190 all_procs_running.remove(self)
191 self.running = False
192
193 if self.p.returncode == 127:
194 self.task.status = "ERROR"
195 if not self.silent:
196 self.task.log(f"{self.info}: COMMAND NOT FOUND. ERROR.")
197 self.handle_error(self.p.returncode)
198 self.terminated = True
199 self.task.terminate()
200 return
201
202 if self.checkretcode and self.p.returncode != 0:
203 self.task.status = "ERROR"
204 if not self.silent:
205 self.task.log(f"{self.info}: task failed. ERROR.")
206 self.handle_error(self.p.returncode)
207 self.terminated = True
208 self.task.terminate()
209 return
210
211 self.handle_exit(self.p.returncode)
212
213 self.finished = True
214 for next_proc in self.notify:
215 next_proc.poll()
216 return
217
218
219 class SbyAbort(BaseException):
220 pass
221
222
223 class SbyConfig:
224 def __init__(self):
225 self.options = dict()
226 self.engines = list()
227 self.script = list()
228 self.files = dict()
229 self.verbatim_files = dict()
230 pass
231
232 def parse_config(self, f):
233 mode = None
234
235 for line in f:
236 raw_line = line
237 if mode in ["options", "engines", "files"]:
238 line = re.sub(r"\s*(\s#.*)?$", "", line)
239 if line == "" or line[0] == "#":
240 continue
241 else:
242 line = line.rstrip()
243 # print(line)
244 if mode is None and (len(line) == 0 or line[0] == "#"):
245 continue
246 match = re.match(r"^\s*\[(.*)\]\s*$", line)
247 if match:
248 entries = match.group(1).split()
249 if len(entries) == 0:
250 self.error(f"sby file syntax error: {line}")
251
252 if entries[0] == "options":
253 mode = "options"
254 if len(self.options) != 0 or len(entries) != 1:
255 self.error(f"sby file syntax error: {line}")
256 continue
257
258 if entries[0] == "engines":
259 mode = "engines"
260 if len(self.engines) != 0 or len(entries) != 1:
261 self.error(f"sby file syntax error: {line}")
262 continue
263
264 if entries[0] == "script":
265 mode = "script"
266 if len(self.script) != 0 or len(entries) != 1:
267 self.error(f"sby file syntax error: {line}")
268 continue
269
270 if entries[0] == "file":
271 mode = "file"
272 if len(entries) != 2:
273 self.error(f"sby file syntax error: {line}")
274 current_verbatim_file = entries[1]
275 if current_verbatim_file in self.verbatim_files:
276 self.error(f"duplicate file: {entries[1]}")
277 self.verbatim_files[current_verbatim_file] = list()
278 continue
279
280 if entries[0] == "files":
281 mode = "files"
282 if len(entries) != 1:
283 self.error(f"sby file syntax error: {line}")
284 continue
285
286 self.error(f"sby file syntax error: {line}")
287
288 if mode == "options":
289 entries = line.split()
290 if len(entries) != 2:
291 self.error(f"sby file syntax error: {line}")
292 self.options[entries[0]] = entries[1]
293 continue
294
295 if mode == "engines":
296 entries = line.split()
297 self.engines.append(entries)
298 continue
299
300 if mode == "script":
301 self.script.append(line)
302 continue
303
304 if mode == "files":
305 entries = line.split()
306 if len(entries) == 1:
307 self.files[os.path.basename(entries[0])] = entries[0]
308 elif len(entries) == 2:
309 self.files[entries[0]] = entries[1]
310 else:
311 self.error(f"sby file syntax error: {line}")
312 continue
313
314 if mode == "file":
315 self.verbatim_files[current_verbatim_file].append(raw_line)
316 continue
317
318 self.error(f"sby file syntax error: {line}")
319
320 def error(self, logmessage):
321 raise SbyAbort(logmessage)
322
323 class SbyTask(SbyConfig):
324 def __init__(self, sbyconfig, workdir, early_logs, reusedir):
325 super().__init__()
326 self.used_options = set()
327 self.models = dict()
328 self.workdir = workdir
329 self.reusedir = reusedir
330 self.status = "UNKNOWN"
331 self.total_time = 0
332 self.expect = list()
333 self.design_hierarchy = None
334 self.precise_prop_status = False
335
336 yosys_program_prefix = "" ##yosys-program-prefix##
337 self.exe_paths = {
338 "yosys": os.getenv("YOSYS", yosys_program_prefix + "yosys"),
339 "abc": os.getenv("ABC", yosys_program_prefix + "yosys-abc"),
340 "smtbmc": os.getenv("SMTBMC", yosys_program_prefix + "yosys-smtbmc"),
341 "suprove": os.getenv("SUPROVE", "suprove"),
342 "aigbmc": os.getenv("AIGBMC", "aigbmc"),
343 "avy": os.getenv("AVY", "avy"),
344 "btormc": os.getenv("BTORMC", "btormc"),
345 "pono": os.getenv("PONO", "pono"),
346 }
347
348 self.procs_running = []
349 self.procs_pending = []
350
351 self.start_clock_time = time()
352
353 if os.name == "posix":
354 ru = resource.getrusage(resource.RUSAGE_CHILDREN)
355 self.start_process_time = ru.ru_utime + ru.ru_stime
356
357 self.summary = list()
358
359 self.logfile = open(f"{workdir}/logfile.txt", "a")
360
361 for line in early_logs:
362 print(line, file=self.logfile, flush=True)
363
364 if not reusedir:
365 with open(f"{workdir}/config.sby", "w") as f:
366 for line in sbyconfig:
367 print(line, file=f)
368
369 def taskloop(self):
370 for proc in self.procs_pending:
371 proc.poll()
372
373 while len(self.procs_running):
374 fds = []
375 for proc in self.procs_running:
376 if proc.running:
377 fds.append(proc.p.stdout)
378
379 if os.name == "posix":
380 try:
381 select(fds, [], [], 1.0) == ([], [], [])
382 except InterruptedError:
383 pass
384 else:
385 sleep(0.1)
386
387 for proc in self.procs_running:
388 proc.poll()
389
390 for proc in self.procs_pending:
391 proc.poll()
392
393 if self.opt_timeout is not None:
394 total_clock_time = int(time() - self.start_clock_time)
395 if total_clock_time > self.opt_timeout:
396 self.log(f"Reached TIMEOUT ({self.opt_timeout} seconds). Terminating all subprocesses.")
397 self.status = "TIMEOUT"
398 self.terminate(timeout=True)
399
400 def log(self, logmessage):
401 tm = localtime()
402 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True)
403 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True)
404
405 def error(self, logmessage):
406 tm = localtime()
407 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True)
408 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True)
409 self.status = "ERROR"
410 if "ERROR" not in self.expect:
411 self.retcode = 16
412 else:
413 self.retcode = 0
414 self.terminate()
415 with open(f"{self.workdir}/{self.status}", "w") as f:
416 print(f"ERROR: {logmessage}", file=f)
417 raise SbyAbort(logmessage)
418
419 def makedirs(self, path):
420 if self.reusedir and os.path.isdir(path):
421 rmtree(path, ignore_errors=True)
422 os.makedirs(path)
423
424 def copy_src(self):
425 os.makedirs(self.workdir + "/src")
426
427 for dstfile, lines in self.verbatim_files.items():
428 dstfile = self.workdir + "/src/" + dstfile
429 self.log(f"Writing '{dstfile}'.")
430
431 with open(dstfile, "w") as f:
432 for line in lines:
433 f.write(line)
434
435 for dstfile, srcfile in self.files.items():
436 if dstfile.startswith("/") or dstfile.startswith("../") or ("/../" in dstfile):
437 self.error(f"destination filename must be a relative path without /../: {dstfile}")
438 dstfile = self.workdir + "/src/" + dstfile
439
440 srcfile = process_filename(srcfile)
441
442 basedir = os.path.dirname(dstfile)
443 if basedir != "" and not os.path.exists(basedir):
444 os.makedirs(basedir)
445
446 self.log(f"Copy '{os.path.abspath(srcfile)}' to '{os.path.abspath(dstfile)}'.")
447 if os.path.isdir(srcfile):
448 copytree(srcfile, dstfile, dirs_exist_ok=True)
449 else:
450 copyfile(srcfile, dstfile)
451
452 def handle_str_option(self, option_name, default_value):
453 if option_name in self.options:
454 self.__dict__["opt_" + option_name] = self.options[option_name]
455 self.used_options.add(option_name)
456 else:
457 self.__dict__["opt_" + option_name] = default_value
458
459 def handle_int_option(self, option_name, default_value):
460 if option_name in self.options:
461 self.__dict__["opt_" + option_name] = int(self.options[option_name])
462 self.used_options.add(option_name)
463 else:
464 self.__dict__["opt_" + option_name] = default_value
465
466 def handle_bool_option(self, option_name, default_value):
467 if option_name in self.options:
468 if self.options[option_name] not in ["on", "off"]:
469 self.error(f"Invalid value '{self.options[option_name]}' for boolean option {option_name}.")
470 self.__dict__["opt_" + option_name] = self.options[option_name] == "on"
471 self.used_options.add(option_name)
472 else:
473 self.__dict__["opt_" + option_name] = default_value
474
475 def make_model(self, model_name):
476 if not os.path.isdir(f"{self.workdir}/model"):
477 os.makedirs(f"{self.workdir}/model")
478
479 def print_common_prep():
480 if self.opt_multiclock:
481 print("clk2fflogic", file=f)
482 else:
483 print("async2sync", file=f)
484 print("chformal -assume -early", file=f)
485 if self.opt_mode in ["bmc", "prove"]:
486 print("chformal -live -fair -cover -remove", file=f)
487 if self.opt_mode == "cover":
488 print("chformal -live -fair -remove", file=f)
489 if self.opt_mode == "live":
490 print("chformal -assert2assume", file=f)
491 print("chformal -cover -remove", file=f)
492 print("opt_clean", file=f)
493 print("setundef -anyseq", file=f)
494 print("opt -keepdc -fast", file=f)
495 print("check", file=f)
496 print("hierarchy -simcheck", file=f)
497
498 if model_name == "base":
499 with open(f"""{self.workdir}/model/design.ys""", "w") as f:
500 print(f"# running in {self.workdir}/src/", file=f)
501 for cmd in self.script:
502 print(cmd, file=f)
503 # the user must designate a top module in [script]
504 print("hierarchy -simcheck", file=f)
505 print(f"""write_jny -no-connections ../model/design.json""", file=f)
506 print(f"""write_rtlil ../model/design.il""", file=f)
507
508 proc = SbyProc(
509 self,
510 model_name,
511 [],
512 "cd {}/src; {} -ql ../model/design.log ../model/design.ys".format(self.workdir, self.exe_paths["yosys"])
513 )
514 proc.checkretcode = True
515
516 def instance_hierarchy_callback(retcode):
517 if self.design_hierarchy == None:
518 with open(f"{self.workdir}/model/design.json") as f:
519 self.design_hierarchy = design_hierarchy(f)
520
521 def instance_hierarchy_error_callback(retcode):
522 self.precise_prop_status = False
523
524 proc.exit_callback = instance_hierarchy_callback
525 proc.error_callback = instance_hierarchy_error_callback
526
527 return [proc]
528
529 if re.match(r"^smt2(_syn)?(_nomem)?(_stbv|_stdt)?$", model_name):
530 with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f:
531 print(f"# running in {self.workdir}/model/", file=f)
532 print(f"""read_ilang design.il""", file=f)
533 if "_nomem" in model_name:
534 print("memory_map", file=f)
535 else:
536 print("memory_nordff", file=f)
537 print_common_prep()
538 if "_syn" in model_name:
539 print("techmap", file=f)
540 print("opt -fast", file=f)
541 print("abc", file=f)
542 print("opt_clean", file=f)
543 print("dffunmap", file=f)
544 print("stat", file=f)
545 if "_stbv" in model_name:
546 print(f"write_smt2 -stbv -wires design_{model_name}.smt2", file=f)
547 elif "_stdt" in model_name:
548 print(f"write_smt2 -stdt -wires design_{model_name}.smt2", file=f)
549 else:
550 print(f"write_smt2 -wires design_{model_name}.smt2", file=f)
551
552 proc = SbyProc(
553 self,
554 model_name,
555 self.model("base"),
556 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name)
557 )
558 proc.checkretcode = True
559
560 return [proc]
561
562 if re.match(r"^btor(_syn)?(_nomem)?$", model_name):
563 with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f:
564 print(f"# running in {self.workdir}/model/", file=f)
565 print(f"""read_ilang design.il""", file=f)
566 if "_nomem" in model_name:
567 print("memory_map", file=f)
568 else:
569 print("memory_nordff", file=f)
570 print_common_prep()
571 print("flatten", file=f)
572 print("setundef -undriven -anyseq", file=f)
573 if "_syn" in model_name:
574 print("opt -full", file=f)
575 print("techmap", file=f)
576 print("opt -fast", file=f)
577 print("abc", file=f)
578 print("opt_clean", file=f)
579 else:
580 print("opt -fast", file=f)
581 print("delete -output", file=f)
582 print("dffunmap", file=f)
583 print("stat", file=f)
584 print("write_btor {}-i design_{m}.info design_{m}.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f)
585 print("write_btor -s {}-i design_{m}_single.info design_{m}_single.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f)
586
587 proc = SbyProc(
588 self,
589 model_name,
590 self.model("base"),
591 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name)
592 )
593 proc.checkretcode = True
594
595 return [proc]
596
597 if model_name == "aig":
598 with open(f"{self.workdir}/model/design_aiger.ys", "w") as f:
599 print(f"# running in {self.workdir}/model/", file=f)
600 print("read_ilang design.il", file=f)
601 print("memory_map", file=f)
602 print_common_prep()
603 print("flatten", file=f)
604 print("setundef -undriven -anyseq", file=f)
605 print("setattr -unset keep", file=f)
606 print("delete -output", file=f)
607 print("opt -full", file=f)
608 print("techmap", file=f)
609 print("opt -fast", file=f)
610 print("dffunmap", file=f)
611 print("abc -g AND -fast", file=f)
612 print("opt_clean", file=f)
613 print("stat", file=f)
614 print("write_aiger -I -B -zinit -no-startoffset -map design_aiger.aim design_aiger.aig", file=f)
615
616 proc = SbyProc(
617 self,
618 "aig",
619 self.model("base"),
620 f"""cd {self.workdir}/model; {self.exe_paths["yosys"]} -ql design_aiger.log design_aiger.ys"""
621 )
622 proc.checkretcode = True
623
624 return [proc]
625
626 assert False
627
628 def model(self, model_name):
629 if model_name not in self.models:
630 self.models[model_name] = self.make_model(model_name)
631 return self.models[model_name]
632
633 def terminate(self, timeout=False):
634 for proc in list(self.procs_running):
635 proc.terminate(timeout=timeout)
636
637 def update_status(self, new_status):
638 assert new_status in ["PASS", "FAIL", "UNKNOWN", "ERROR"]
639
640 if new_status == "UNKNOWN":
641 return
642
643 if self.status == "ERROR":
644 return
645
646 if new_status == "PASS":
647 assert self.status != "FAIL"
648 self.status = "PASS"
649
650 elif new_status == "FAIL":
651 assert self.status != "PASS"
652 self.status = "FAIL"
653
654 elif new_status == "ERROR":
655 self.status = "ERROR"
656
657 else:
658 assert 0
659
660 def run(self, setupmode):
661 with open(f"{self.workdir}/config.sby", "r") as f:
662 self.parse_config(f)
663
664 self.handle_str_option("mode", None)
665
666 if self.opt_mode not in ["bmc", "prove", "cover", "live"]:
667 self.error(f"Invalid mode: {self.opt_mode}")
668
669 self.expect = ["PASS"]
670 if "expect" in self.options:
671 self.expect = self.options["expect"].upper().split(",")
672 self.used_options.add("expect")
673
674 for s in self.expect:
675 if s not in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]:
676 self.error(f"Invalid expect value: {s}")
677
678 self.handle_bool_option("multiclock", False)
679 self.handle_bool_option("wait", False)
680 self.handle_int_option("timeout", None)
681
682 self.handle_str_option("smtc", None)
683 self.handle_int_option("skip", None)
684 self.handle_str_option("tbtop", None)
685
686 if self.opt_smtc is not None:
687 for engine in self.engines:
688 if engine[0] != "smtbmc":
689 self.error("Option smtc is only valid for smtbmc engine.")
690
691 if self.opt_skip is not None:
692 if self.opt_skip == 0:
693 self.opt_skip = None
694 else:
695 for engine in self.engines:
696 if engine[0] not in ["smtbmc", "btor"]:
697 self.error("Option skip is only valid for smtbmc and btor engines.")
698
699 if len(self.engines) == 0:
700 self.error("Config file is lacking engine configuration.")
701
702 if self.reusedir:
703 rmtree(f"{self.workdir}/model", ignore_errors=True)
704 else:
705 self.copy_src()
706
707 if setupmode:
708 self.retcode = 0
709 return
710
711 if self.opt_mode == "bmc":
712 import sby_mode_bmc
713 sby_mode_bmc.run(self)
714
715 elif self.opt_mode == "prove":
716 import sby_mode_prove
717 sby_mode_prove.run(self)
718
719 elif self.opt_mode == "live":
720 import sby_mode_live
721 sby_mode_live.run(self)
722
723 elif self.opt_mode == "cover":
724 import sby_mode_cover
725 sby_mode_cover.run(self)
726
727 else:
728 assert False
729
730 for opt in self.options.keys():
731 if opt not in self.used_options:
732 self.error(f"Unused option: {opt}")
733
734 self.taskloop()
735
736 total_clock_time = int(time() - self.start_clock_time)
737
738 if os.name == "posix":
739 ru = resource.getrusage(resource.RUSAGE_CHILDREN)
740 total_process_time = int((ru.ru_utime + ru.ru_stime) - self.start_process_time)
741 self.total_time = total_process_time
742
743 self.summary = [
744 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
745 (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time),
746 "Elapsed process time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
747 (total_process_time // (60*60), (total_process_time // 60) % 60, total_process_time % 60, total_process_time),
748 ] + self.summary
749 else:
750 self.summary = [
751 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
752 (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time),
753 "Elapsed process time unvailable on Windows"
754 ] + self.summary
755
756 for line in self.summary:
757 self.log(f"summary: {line}")
758
759 assert self.status in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]
760
761 if self.status in self.expect:
762 self.retcode = 0
763 else:
764 if self.status == "PASS": self.retcode = 1
765 if self.status == "FAIL": self.retcode = 2
766 if self.status == "UNKNOWN": self.retcode = 4
767 if self.status == "TIMEOUT": self.retcode = 8
768 if self.status == "ERROR": self.retcode = 16
769
770 with open(f"{self.workdir}/{self.status}", "w") as f:
771 for line in self.summary:
772 print(line, file=f)
773
774 def print_junit_result(self, f, junit_ts_name, junit_tc_name, junit_format_strict=False):
775 junit_time = strftime('%Y-%m-%dT%H:%M:%S')
776 if self.precise_prop_status:
777 checks = self.design_hierarchy.get_property_list()
778 junit_tests = len(checks)
779 junit_failures = 0
780 junit_errors = 0
781 junit_skipped = 0
782 for check in checks:
783 if check.status == "PASS":
784 pass
785 elif check.status == "FAIL":
786 junit_failures += 1
787 elif check.status == "UNKNOWN":
788 junit_skipped += 1
789 else:
790 junit_errors += 1
791 if self.retcode == 16:
792 junit_errors += 1
793 elif self.retcode != 0:
794 junit_failures += 1
795 else:
796 junit_tests = 1
797 junit_errors = 1 if self.retcode == 16 else 0
798 junit_failures = 1 if self.retcode != 0 and junit_errors == 0 else 0
799 junit_skipped = 0
800 print(f'<?xml version="1.0" encoding="UTF-8"?>', file=f)
801 print(f'<testsuites>', file=f)
802 print(f'<testsuite timestamp="{junit_time}" hostname="{platform.node()}" package="{junit_ts_name}" id="0" name="{junit_tc_name}" tests="{junit_tests}" errors="{junit_errors}" failures="{junit_failures}" time="{self.total_time}" skipped="{junit_skipped}">', file=f)
803 print(f'<properties>', file=f)
804 print(f'<property name="os" value="{platform.system()}"/>', file=f)
805 print(f'<property name="expect" value="{", ".join(self.expect)}"/>', file=f)
806 print(f'<property name="status" value="{self.status}"/>', file=f)
807 print(f'</properties>', file=f)
808 if self.precise_prop_status:
809 print(f'<testcase classname="{junit_tc_name}" name="build execution" time="0">', file=f)
810 if self.retcode == 16:
811 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
812 elif self.retcode != 0:
813 if len(self.expect) > 1 or "PASS" not in self.expect:
814 expected = " ".join(self.expect)
815 print(f'<failure type="EXPECT" message="Task returned status {self.status}. Expected values were: {expected}" />', file=f)
816 else:
817 print(f'<failure type="{self.status}" message="Task returned status {self.status}." />', file=f)
818 print(f'</testcase>', file=f)
819
820 for check in checks:
821 if junit_format_strict:
822 detail_attrs = ''
823 else:
824 detail_attrs = f' type="{check.type}" location="{check.location}" id="{check.name}"'
825 if check.tracefile:
826 detail_attrs += f' tracefile="{check.tracefile}"'
827 if check.location:
828 junit_prop_name = f"Property {check.type} in {check.hierarchy} at {check.location}"
829 else:
830 junit_prop_name = f"Property {check.type} {check.name} in {check.hierarchy}"
831 print(f'<testcase classname="{junit_tc_name}" name="{junit_prop_name}" time="0"{detail_attrs}>', file=f)
832 if check.status == "PASS":
833 pass
834 elif check.status == "UNKNOWN":
835 print(f'<skipped />', file=f)
836 elif check.status == "FAIL":
837 traceinfo = f' Trace file: {check.tracefile}' if check.type == check.Type.ASSERT else ''
838 print(f'<failure type="{check.type}" message="{junit_prop_name} failed.{traceinfo}" />', file=f)
839 elif check.status == "ERROR":
840 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
841 print(f'</testcase>', file=f)
842 else:
843 print(f'<testcase classname="{junit_tc_name}" name="{junit_tc_name}" time="{self.total_time}">', file=f)
844 if junit_errors:
845 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
846 elif junit_failures:
847 junit_type = "assert" if self.opt_mode in ["bmc", "prove"] else self.opt_mode
848 print(f'<failure type="{junit_type}" message="{self.status}" />', file=f)
849 print(f'</testcase>', file=f)
850 print('<system-out>', end="", file=f)
851 with open(f"{self.workdir}/logfile.txt", "r") as logf:
852 for line in logf:
853 print(line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;"), end="", file=f)
854 print('</system-out>', file=f)
855 print('<system-err>', file=f)
856 #TODO: can we handle errors and still output this file?
857 print('</system-err>', file=f)
858 print(f'</testsuite>', file=f)
859 print(f'</testsuites>', file=f)