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