Decouple taskloop from task
[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.files = dict()
237 self.verbatim_files = dict()
238 pass
239
240 def parse_config(self, f):
241 mode = None
242
243 for line in f:
244 raw_line = line
245 if mode in ["options", "engines", "files"]:
246 line = re.sub(r"\s*(\s#.*)?$", "", line)
247 if line == "" or line[0] == "#":
248 continue
249 else:
250 line = line.rstrip()
251 # print(line)
252 if mode is None and (len(line) == 0 or line[0] == "#"):
253 continue
254 match = re.match(r"^\s*\[(.*)\]\s*$", line)
255 if match:
256 entries = match.group(1).split()
257 if len(entries) == 0:
258 self.error(f"sby file syntax error: {line}")
259
260 if entries[0] == "options":
261 mode = "options"
262 if len(self.options) != 0 or len(entries) != 1:
263 self.error(f"sby file syntax error: {line}")
264 continue
265
266 if entries[0] == "engines":
267 mode = "engines"
268 if len(self.engines) != 0 or len(entries) != 1:
269 self.error(f"sby file syntax error: {line}")
270 continue
271
272 if entries[0] == "script":
273 mode = "script"
274 if len(self.script) != 0 or len(entries) != 1:
275 self.error(f"sby file syntax error: {line}")
276 continue
277
278 if entries[0] == "file":
279 mode = "file"
280 if len(entries) != 2:
281 self.error(f"sby file syntax error: {line}")
282 current_verbatim_file = entries[1]
283 if current_verbatim_file in self.verbatim_files:
284 self.error(f"duplicate file: {entries[1]}")
285 self.verbatim_files[current_verbatim_file] = list()
286 continue
287
288 if entries[0] == "files":
289 mode = "files"
290 if len(entries) != 1:
291 self.error(f"sby file syntax error: {line}")
292 continue
293
294 self.error(f"sby file syntax error: {line}")
295
296 if mode == "options":
297 entries = line.split()
298 if len(entries) != 2:
299 self.error(f"sby file syntax error: {line}")
300 self.options[entries[0]] = entries[1]
301 continue
302
303 if mode == "engines":
304 entries = line.split()
305 self.engines.append(entries)
306 continue
307
308 if mode == "script":
309 self.script.append(line)
310 continue
311
312 if mode == "files":
313 entries = line.split()
314 if len(entries) == 1:
315 self.files[os.path.basename(entries[0])] = entries[0]
316 elif len(entries) == 2:
317 self.files[entries[0]] = entries[1]
318 else:
319 self.error(f"sby file syntax error: {line}")
320 continue
321
322 if mode == "file":
323 self.verbatim_files[current_verbatim_file].append(raw_line)
324 continue
325
326 self.error(f"sby file syntax error: {line}")
327
328 def error(self, logmessage):
329 raise SbyAbort(logmessage)
330
331
332 class SbyTaskloop:
333 def __init__(self):
334 self.procs_pending = []
335 self.procs_running = []
336 self.tasks = []
337 self.poll_now = False
338
339 def run(self):
340 for proc in self.procs_pending:
341 proc.poll()
342
343 while len(self.procs_running) or self.poll_now:
344 fds = []
345 for proc in self.procs_running:
346 if proc.running:
347 fds.append(proc.p.stdout)
348
349 if not self.poll_now:
350 if os.name == "posix":
351 try:
352 select(fds, [], [], 1.0) == ([], [], [])
353 except InterruptedError:
354 pass
355 else:
356 sleep(0.1)
357 self.poll_now = False
358
359 for proc in self.procs_running:
360 proc.poll()
361
362 for proc in self.procs_pending:
363 proc.poll()
364
365 tasks = self.tasks
366 self.tasks = []
367 for task in tasks:
368 task.check_timeout()
369 if task.procs_pending or task.procs_running:
370 self.tasks.append(task)
371 else:
372 task.exit_callback()
373
374 for task in self.tasks:
375 task.exit_callback()
376
377
378 class SbyTask(SbyConfig):
379 def __init__(self, sbyconfig, workdir, early_logs, reusedir, taskloop=None):
380 super().__init__()
381 self.used_options = set()
382 self.models = dict()
383 self.workdir = workdir
384 self.reusedir = reusedir
385 self.status = "UNKNOWN"
386 self.total_time = 0
387 self.expect = list()
388 self.design_hierarchy = None
389 self.precise_prop_status = False
390 self.timeout_reached = False
391 self.task_local_abort = False
392
393 yosys_program_prefix = "" ##yosys-program-prefix##
394 self.exe_paths = {
395 "yosys": os.getenv("YOSYS", yosys_program_prefix + "yosys"),
396 "abc": os.getenv("ABC", yosys_program_prefix + "yosys-abc"),
397 "smtbmc": os.getenv("SMTBMC", yosys_program_prefix + "yosys-smtbmc"),
398 "suprove": os.getenv("SUPROVE", "suprove"),
399 "aigbmc": os.getenv("AIGBMC", "aigbmc"),
400 "avy": os.getenv("AVY", "avy"),
401 "btormc": os.getenv("BTORMC", "btormc"),
402 "pono": os.getenv("PONO", "pono"),
403 }
404
405 self.taskloop = taskloop or SbyTaskloop()
406 self.taskloop.tasks.append(self)
407
408 self.procs_running = []
409 self.procs_pending = []
410
411 self.start_clock_time = monotonic()
412
413 if os.name == "posix":
414 ru = resource.getrusage(resource.RUSAGE_CHILDREN)
415 self.start_process_time = ru.ru_utime + ru.ru_stime
416
417 self.summary = list()
418
419 self.logfile = open(f"{workdir}/logfile.txt", "a")
420
421 for line in early_logs:
422 print(line, file=self.logfile, flush=True)
423
424 if not reusedir:
425 with open(f"{workdir}/config.sby", "w") as f:
426 for line in sbyconfig:
427 print(line, file=f)
428
429 def check_timeout(self):
430 if self.opt_timeout is not None:
431 total_clock_time = int(monotonic() - self.start_clock_time)
432 if total_clock_time > self.opt_timeout:
433 self.log(f"Reached TIMEOUT ({self.opt_timeout} seconds). Terminating all subprocesses.")
434 self.status = "TIMEOUT"
435 self.terminate(timeout=True)
436
437 def update_proc_pending(self, proc):
438 self.procs_pending.append(proc)
439 self.taskloop.procs_pending.append(proc)
440
441 def update_proc_running(self, proc):
442 self.procs_pending.remove(proc)
443 self.taskloop.procs_pending.remove(proc)
444
445 self.procs_running.append(proc)
446 self.taskloop.procs_running.append(proc)
447 all_procs_running.append(proc)
448
449 def update_proc_stopped(self, proc):
450 self.procs_running.remove(proc)
451 self.taskloop.procs_running.remove(proc)
452 all_procs_running.remove(proc)
453
454 def update_proc_canceled(self, proc):
455 self.procs_pending.remove(proc)
456 self.taskloop.procs_pending.remove(proc)
457
458 def log(self, logmessage):
459 tm = localtime()
460 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True)
461 print("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True)
462
463 def error(self, logmessage):
464 tm = localtime()
465 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), flush=True)
466 print("SBY {:2d}:{:02d}:{:02d} [{}] ERROR: {}".format(tm.tm_hour, tm.tm_min, tm.tm_sec, self.workdir, logmessage), file=self.logfile, flush=True)
467 self.status = "ERROR"
468 if "ERROR" not in self.expect:
469 self.retcode = 16
470 else:
471 self.retcode = 0
472 self.terminate()
473 with open(f"{self.workdir}/{self.status}", "w") as f:
474 print(f"ERROR: {logmessage}", file=f)
475 raise SbyAbort(logmessage)
476
477 def makedirs(self, path):
478 if self.reusedir and os.path.isdir(path):
479 rmtree(path, ignore_errors=True)
480 os.makedirs(path)
481
482 def copy_src(self):
483 os.makedirs(self.workdir + "/src")
484
485 for dstfile, lines in self.verbatim_files.items():
486 dstfile = self.workdir + "/src/" + dstfile
487 self.log(f"Writing '{dstfile}'.")
488
489 with open(dstfile, "w") as f:
490 for line in lines:
491 f.write(line)
492
493 for dstfile, srcfile in self.files.items():
494 if dstfile.startswith("/") or dstfile.startswith("../") or ("/../" in dstfile):
495 self.error(f"destination filename must be a relative path without /../: {dstfile}")
496 dstfile = self.workdir + "/src/" + dstfile
497
498 srcfile = process_filename(srcfile)
499
500 basedir = os.path.dirname(dstfile)
501 if basedir != "" and not os.path.exists(basedir):
502 os.makedirs(basedir)
503
504 self.log(f"Copy '{os.path.abspath(srcfile)}' to '{os.path.abspath(dstfile)}'.")
505 if os.path.isdir(srcfile):
506 copytree(srcfile, dstfile, dirs_exist_ok=True)
507 else:
508 copyfile(srcfile, dstfile)
509
510 def handle_str_option(self, option_name, default_value):
511 if option_name in self.options:
512 self.__dict__["opt_" + option_name] = self.options[option_name]
513 self.used_options.add(option_name)
514 else:
515 self.__dict__["opt_" + option_name] = default_value
516
517 def handle_int_option(self, option_name, default_value):
518 if option_name in self.options:
519 self.__dict__["opt_" + option_name] = int(self.options[option_name])
520 self.used_options.add(option_name)
521 else:
522 self.__dict__["opt_" + option_name] = default_value
523
524 def handle_bool_option(self, option_name, default_value):
525 if option_name in self.options:
526 if self.options[option_name] not in ["on", "off"]:
527 self.error(f"Invalid value '{self.options[option_name]}' for boolean option {option_name}.")
528 self.__dict__["opt_" + option_name] = self.options[option_name] == "on"
529 self.used_options.add(option_name)
530 else:
531 self.__dict__["opt_" + option_name] = default_value
532
533 def make_model(self, model_name):
534 if not os.path.isdir(f"{self.workdir}/model"):
535 os.makedirs(f"{self.workdir}/model")
536
537 def print_common_prep():
538 if self.opt_multiclock:
539 print("clk2fflogic", file=f)
540 else:
541 print("async2sync", file=f)
542 print("chformal -assume -early", file=f)
543 if self.opt_mode in ["bmc", "prove"]:
544 print("chformal -live -fair -cover -remove", file=f)
545 if self.opt_mode == "cover":
546 print("chformal -live -fair -remove", file=f)
547 if self.opt_mode == "live":
548 print("chformal -assert2assume", file=f)
549 print("chformal -cover -remove", file=f)
550 print("opt_clean", file=f)
551 print("setundef -anyseq", file=f)
552 print("opt -keepdc -fast", file=f)
553 print("check", file=f)
554 print("hierarchy -simcheck", file=f)
555
556 if model_name == "base":
557 with open(f"""{self.workdir}/model/design.ys""", "w") as f:
558 print(f"# running in {self.workdir}/src/", file=f)
559 for cmd in self.script:
560 print(cmd, file=f)
561 # the user must designate a top module in [script]
562 print("hierarchy -simcheck", file=f)
563 print(f"""write_jny -no-connections ../model/design.json""", file=f)
564 print(f"""write_rtlil ../model/design.il""", file=f)
565
566 proc = SbyProc(
567 self,
568 model_name,
569 [],
570 "cd {}/src; {} -ql ../model/design.log ../model/design.ys".format(self.workdir, self.exe_paths["yosys"])
571 )
572 proc.checkretcode = True
573
574 def instance_hierarchy_callback(retcode):
575 if self.design_hierarchy == None:
576 with open(f"{self.workdir}/model/design.json") as f:
577 self.design_hierarchy = design_hierarchy(f)
578
579 def instance_hierarchy_error_callback(retcode):
580 self.precise_prop_status = False
581
582 proc.exit_callback = instance_hierarchy_callback
583 proc.error_callback = instance_hierarchy_error_callback
584
585 return [proc]
586
587 if re.match(r"^smt2(_syn)?(_nomem)?(_stbv|_stdt)?$", model_name):
588 with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f:
589 print(f"# running in {self.workdir}/model/", file=f)
590 print(f"""read_ilang design.il""", file=f)
591 if "_nomem" in model_name:
592 print("memory_map", file=f)
593 else:
594 print("memory_nordff", file=f)
595 print_common_prep()
596 if "_syn" in model_name:
597 print("techmap", file=f)
598 print("opt -fast", file=f)
599 print("abc", file=f)
600 print("opt_clean", file=f)
601 print("dffunmap", file=f)
602 print("stat", file=f)
603 if "_stbv" in model_name:
604 print(f"write_smt2 -stbv -wires design_{model_name}.smt2", file=f)
605 elif "_stdt" in model_name:
606 print(f"write_smt2 -stdt -wires design_{model_name}.smt2", file=f)
607 else:
608 print(f"write_smt2 -wires design_{model_name}.smt2", file=f)
609
610 proc = SbyProc(
611 self,
612 model_name,
613 self.model("base"),
614 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name)
615 )
616 proc.checkretcode = True
617
618 return [proc]
619
620 if re.match(r"^btor(_syn)?(_nomem)?$", model_name):
621 with open(f"{self.workdir}/model/design_{model_name}.ys", "w") as f:
622 print(f"# running in {self.workdir}/model/", file=f)
623 print(f"""read_ilang design.il""", file=f)
624 if "_nomem" in model_name:
625 print("memory_map", file=f)
626 else:
627 print("memory_nordff", file=f)
628 print_common_prep()
629 print("flatten", file=f)
630 print("setundef -undriven -anyseq", file=f)
631 if "_syn" in model_name:
632 print("opt -full", file=f)
633 print("techmap", file=f)
634 print("opt -fast", file=f)
635 print("abc", file=f)
636 print("opt_clean", file=f)
637 else:
638 print("opt -fast", file=f)
639 print("delete -output", file=f)
640 print("dffunmap", file=f)
641 print("stat", file=f)
642 print("write_btor {}-i design_{m}.info design_{m}.btor".format("-c " if self.opt_mode == "cover" else "", m=model_name), file=f)
643 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)
644
645 proc = SbyProc(
646 self,
647 model_name,
648 self.model("base"),
649 "cd {}/model; {} -ql design_{s}.log design_{s}.ys".format(self.workdir, self.exe_paths["yosys"], s=model_name)
650 )
651 proc.checkretcode = True
652
653 return [proc]
654
655 if model_name == "aig":
656 with open(f"{self.workdir}/model/design_aiger.ys", "w") as f:
657 print(f"# running in {self.workdir}/model/", file=f)
658 print("read_ilang design.il", file=f)
659 print("memory_map", file=f)
660 print_common_prep()
661 print("flatten", file=f)
662 print("setundef -undriven -anyseq", file=f)
663 print("setattr -unset keep", file=f)
664 print("delete -output", file=f)
665 print("opt -full", file=f)
666 print("techmap", file=f)
667 print("opt -fast", file=f)
668 print("dffunmap", file=f)
669 print("abc -g AND -fast", file=f)
670 print("opt_clean", file=f)
671 print("stat", file=f)
672 print("write_aiger -I -B -zinit -no-startoffset -map design_aiger.aim design_aiger.aig", file=f)
673
674 proc = SbyProc(
675 self,
676 "aig",
677 self.model("base"),
678 f"""cd {self.workdir}/model; {self.exe_paths["yosys"]} -ql design_aiger.log design_aiger.ys"""
679 )
680 proc.checkretcode = True
681
682 return [proc]
683
684 assert False
685
686 def model(self, model_name):
687 if model_name not in self.models:
688 self.models[model_name] = self.make_model(model_name)
689 return self.models[model_name]
690
691 def terminate(self, timeout=False):
692 if timeout:
693 self.timeout_reached = True
694 for proc in list(self.procs_running):
695 proc.terminate(timeout=timeout)
696 for proc in list(self.procs_pending):
697 proc.terminate(timeout=timeout)
698
699 def proc_failed(self, proc):
700 # proc parameter used by autotune override
701 self.status = "ERROR"
702 self.terminate()
703
704 def update_status(self, new_status):
705 assert new_status in ["PASS", "FAIL", "UNKNOWN", "ERROR"]
706
707 if new_status == "UNKNOWN":
708 return
709
710 if self.status == "ERROR":
711 return
712
713 if new_status == "PASS":
714 assert self.status != "FAIL"
715 self.status = "PASS"
716
717 elif new_status == "FAIL":
718 assert self.status != "PASS"
719 self.status = "FAIL"
720
721 elif new_status == "ERROR":
722 self.status = "ERROR"
723
724 else:
725 assert 0
726
727 def run(self, setupmode):
728 self.setup_procs(setupmode)
729 if not setupmode:
730 self.taskloop.run()
731
732 def setup_procs(self, setupmode):
733 with open(f"{self.workdir}/config.sby", "r") as f:
734 self.parse_config(f)
735
736 self.handle_str_option("mode", None)
737
738 if self.opt_mode not in ["bmc", "prove", "cover", "live"]:
739 self.error(f"Invalid mode: {self.opt_mode}")
740
741 self.expect = ["PASS"]
742 if "expect" in self.options:
743 self.expect = self.options["expect"].upper().split(",")
744 self.used_options.add("expect")
745
746 for s in self.expect:
747 if s not in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]:
748 self.error(f"Invalid expect value: {s}")
749
750 self.handle_bool_option("multiclock", False)
751 self.handle_bool_option("wait", False)
752 self.handle_int_option("timeout", None)
753
754 self.handle_str_option("smtc", None)
755 self.handle_int_option("skip", None)
756 self.handle_str_option("tbtop", None)
757
758 if self.opt_smtc is not None:
759 for engine in self.engines:
760 if engine[0] != "smtbmc":
761 self.error("Option smtc is only valid for smtbmc engine.")
762
763 if self.opt_skip is not None:
764 if self.opt_skip == 0:
765 self.opt_skip = None
766 else:
767 for engine in self.engines:
768 if engine[0] not in ["smtbmc", "btor"]:
769 self.error("Option skip is only valid for smtbmc and btor engines.")
770
771 if len(self.engines) == 0:
772 self.error("Config file is lacking engine configuration.")
773
774 if self.reusedir:
775 rmtree(f"{self.workdir}/model", ignore_errors=True)
776 else:
777 self.copy_src()
778
779 if setupmode:
780 self.retcode = 0
781 return
782
783 if self.opt_mode == "bmc":
784 import sby_mode_bmc
785 sby_mode_bmc.run(self)
786
787 elif self.opt_mode == "prove":
788 import sby_mode_prove
789 sby_mode_prove.run(self)
790
791 elif self.opt_mode == "live":
792 import sby_mode_live
793 sby_mode_live.run(self)
794
795 elif self.opt_mode == "cover":
796 import sby_mode_cover
797 sby_mode_cover.run(self)
798
799 else:
800 assert False
801
802 for opt in self.options.keys():
803 if opt not in self.used_options:
804 self.error(f"Unused option: {opt}")
805
806 def summarize(self):
807 total_clock_time = int(monotonic() - self.start_clock_time)
808
809 if os.name == "posix":
810 ru = resource.getrusage(resource.RUSAGE_CHILDREN)
811 total_process_time = int((ru.ru_utime + ru.ru_stime) - self.start_process_time)
812 self.total_time = total_process_time
813
814 self.summary = [
815 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
816 (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time),
817 "Elapsed process time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
818 (total_process_time // (60*60), (total_process_time // 60) % 60, total_process_time % 60, total_process_time),
819 ] + self.summary
820 else:
821 self.summary = [
822 "Elapsed clock time [H:MM:SS (secs)]: {}:{:02d}:{:02d} ({})".format
823 (total_clock_time // (60*60), (total_clock_time // 60) % 60, total_clock_time % 60, total_clock_time),
824 "Elapsed process time unvailable on Windows"
825 ] + self.summary
826
827 for line in self.summary:
828 self.log(f"summary: {line}")
829
830 assert self.status in ["PASS", "FAIL", "UNKNOWN", "ERROR", "TIMEOUT"]
831
832 if self.status in self.expect:
833 self.retcode = 0
834 else:
835 if self.status == "PASS": self.retcode = 1
836 if self.status == "FAIL": self.retcode = 2
837 if self.status == "UNKNOWN": self.retcode = 4
838 if self.status == "TIMEOUT": self.retcode = 8
839 if self.status == "ERROR": self.retcode = 16
840
841 with open(f"{self.workdir}/{self.status}", "w") as f:
842 for line in self.summary:
843 print(line, file=f)
844
845 def exit_callback(self):
846 self.summarize()
847
848 def print_junit_result(self, f, junit_ts_name, junit_tc_name, junit_format_strict=False):
849 junit_time = strftime('%Y-%m-%dT%H:%M:%S')
850 if self.precise_prop_status:
851 checks = self.design_hierarchy.get_property_list()
852 junit_tests = len(checks)
853 junit_failures = 0
854 junit_errors = 0
855 junit_skipped = 0
856 for check in checks:
857 if check.status == "PASS":
858 pass
859 elif check.status == "FAIL":
860 junit_failures += 1
861 elif check.status == "UNKNOWN":
862 junit_skipped += 1
863 else:
864 junit_errors += 1
865 if self.retcode == 16:
866 junit_errors += 1
867 elif self.retcode != 0:
868 junit_failures += 1
869 else:
870 junit_tests = 1
871 junit_errors = 1 if self.retcode == 16 else 0
872 junit_failures = 1 if self.retcode != 0 and junit_errors == 0 else 0
873 junit_skipped = 0
874 print(f'<?xml version="1.0" encoding="UTF-8"?>', file=f)
875 print(f'<testsuites>', file=f)
876 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)
877 print(f'<properties>', file=f)
878 print(f'<property name="os" value="{platform.system()}"/>', file=f)
879 print(f'<property name="expect" value="{", ".join(self.expect)}"/>', file=f)
880 print(f'<property name="status" value="{self.status}"/>', file=f)
881 print(f'</properties>', file=f)
882 if self.precise_prop_status:
883 print(f'<testcase classname="{junit_tc_name}" name="build execution" time="0">', file=f)
884 if self.retcode == 16:
885 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
886 elif self.retcode != 0:
887 if len(self.expect) > 1 or "PASS" not in self.expect:
888 expected = " ".join(self.expect)
889 print(f'<failure type="EXPECT" message="Task returned status {self.status}. Expected values were: {expected}" />', file=f)
890 else:
891 print(f'<failure type="{self.status}" message="Task returned status {self.status}." />', file=f)
892 print(f'</testcase>', file=f)
893
894 for check in checks:
895 if junit_format_strict:
896 detail_attrs = ''
897 else:
898 detail_attrs = f' type="{check.type}" location="{check.location}" id="{check.name}"'
899 if check.tracefile:
900 detail_attrs += f' tracefile="{check.tracefile}"'
901 if check.location:
902 junit_prop_name = f"Property {check.type} in {check.hierarchy} at {check.location}"
903 else:
904 junit_prop_name = f"Property {check.type} {check.name} in {check.hierarchy}"
905 print(f'<testcase classname="{junit_tc_name}" name="{junit_prop_name}" time="0"{detail_attrs}>', file=f)
906 if check.status == "PASS":
907 pass
908 elif check.status == "UNKNOWN":
909 print(f'<skipped />', file=f)
910 elif check.status == "FAIL":
911 traceinfo = f' Trace file: {check.tracefile}' if check.type == check.Type.ASSERT else ''
912 print(f'<failure type="{check.type}" message="{junit_prop_name} failed.{traceinfo}" />', file=f)
913 elif check.status == "ERROR":
914 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
915 print(f'</testcase>', file=f)
916 else:
917 print(f'<testcase classname="{junit_tc_name}" name="{junit_tc_name}" time="{self.total_time}">', file=f)
918 if junit_errors:
919 print(f'<error type="ERROR"/>', file=f) # type mandatory, message optional
920 elif junit_failures:
921 junit_type = "assert" if self.opt_mode in ["bmc", "prove"] else self.opt_mode
922 print(f'<failure type="{junit_type}" message="{self.status}" />', file=f)
923 print(f'</testcase>', file=f)
924 print('<system-out>', end="", file=f)
925 with open(f"{self.workdir}/logfile.txt", "r") as logf:
926 for line in logf:
927 print(line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;"), end="", file=f)
928 print('</system-out>', file=f)
929 print('<system-err>', file=f)
930 #TODO: can we handle errors and still output this file?
931 print('</system-err>', file=f)
932 print(f'</testsuite>', file=f)
933 print(f'</testsuites>', file=f)