Coverage for trlc/trlc.py: 93%
345 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-30 12:50 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-30 12:50 +0000
1#!/usr/bin/env python3
2#
3# TRLC - Treat Requirements Like Code
4# Copyright (C) 2022-2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This file is part of the TRLC Python Reference Implementation.
7#
8# TRLC is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# TRLC is distributed in the hope that it will be useful, but WITHOUT
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
16# License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with TRLC. If not, see <https://www.gnu.org/licenses/>.
21import argparse
22import json
23import os
24import re
25import sys
26from fractions import Fraction
28from trlc import ast, lint
29from trlc.errors import Kind, Location, Message_Handler, TRLC_Error
30from trlc.lexer import Token_Stream
31from trlc.parser import Parser
32from trlc.version import BUGS_URL, TRLC_VERSION
34# pylint: disable=unused-import
35try:
36 import cvc5
37 VCG_API_AVAILABLE = True
38except ImportError: # pragma: no cover
39 VCG_API_AVAILABLE = False
42class Source_Manager:
43 """Dependency and source manager for TRLC.
45 This is the main entry point when using the Python API. Create an
46 instance of this, register the files you want to look at, and
47 finally call the process method.
49 :param mh: The message handler to use
50 :type mh: Message_Handler
52 :param error_recovery: If true attempts to continue parsing after \
53 errors. This may generate weird error messages since it's impossible \
54 to reliably recover the parse context in all cases.
55 :type error_recovery: bool
57 :param lint_mode: If true enables additional warning messages.
58 :type lint_mode: bool
60 :param verify_mode: If true performs in-depth static analysis for \
61 user-defined checks. Requires CVC5 and PyVCG to be installed.
62 :type verify_mode: bool
64 :param parse_trlc: If true parses trlc files, otherwise they are \
65 ignored.
66 :type parse_trlc: bool
68 :param debug_vcg: If true and verify_mode is also true, emit the \
69 individual SMTLIB2 VCs and generate a picture of the program \
70 graph. Requires Graphviz to be installed.
71 :type debug_vcg: bool
73 """
74 def __init__(self, mh,
75 lint_mode = True,
76 parse_trlc = True,
77 verify_mode = False,
78 debug_vcg = False,
79 error_recovery = True):
80 assert isinstance(mh, Message_Handler)
81 assert isinstance(lint_mode, bool)
82 assert isinstance(parse_trlc, bool)
83 assert isinstance(verify_mode, bool)
84 assert isinstance(debug_vcg, bool)
86 self.mh = mh
87 self.mh.sm = self
88 self.stab = ast.Symbol_Table.create_global_table(mh)
89 self.includes = {}
90 self.rsl_files = {}
91 self.trlc_files = {}
92 self.all_files = {}
93 self.dep_graph = {}
95 self.files_with_preamble_errors = set()
97 self.lint_mode = lint_mode
98 self.parse_trlc = parse_trlc
99 self.verify_mode = verify_mode
100 self.debug_vcg = debug_vcg
101 self.error_recovery = error_recovery
103 self.exclude_patterns = []
104 self.common_root = None
106 self.progress_current = 0
107 self.progress_final = 0
109 def callback_parse_begin(self):
110 pass
112 def callback_parse_progress(self, progress):
113 assert isinstance(progress, int)
115 def callback_parse_end(self):
116 pass
118 def signal_progress(self):
119 self.progress_current += 1
120 if self.progress_final:
121 progress = (self.progress_current * 100) // self.progress_final
122 else: # pragma: no cover
123 progress = 100
124 self.callback_parse_progress(min(progress, 100))
126 def cross_file_reference(self, location):
127 assert isinstance(location, Location)
129 if self.common_root is None: 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 return location.to_string(False)
131 elif location.line_no is None:
132 return os.path.relpath(location.file_name,
133 self.common_root)
134 else:
135 return "%s:%u" % (os.path.relpath(location.file_name,
136 self.common_root),
137 location.line_no)
139 def update_common_root(self, file_name):
140 assert isinstance(file_name, str)
142 if self.common_root is None:
143 self.common_root = os.path.dirname(os.path.abspath(file_name))
144 else:
145 new_root = os.path.dirname(os.path.abspath(file_name))
146 for n, (char_a, char_b) in enumerate(zip(self.common_root,
147 new_root)):
148 if char_a != char_b:
149 self.common_root = self.common_root[0:n]
150 break
152 def create_parser(self, file_name, file_content=None, primary_file=True):
153 assert os.path.isfile(file_name)
154 assert isinstance(file_content, str) or file_content is None
155 assert isinstance(primary_file, bool)
157 lexer = Token_Stream(self.mh, file_name, file_content)
159 return Parser(mh = self.mh,
160 stab = self.stab,
161 file_name = file_name,
162 lint_mode = self.lint_mode,
163 error_recovery = self.error_recovery,
164 primary_file = primary_file,
165 lexer = lexer)
167 def register_include(self, dir_name):
168 """Make contents of a directory available for automatic inclusion
170 :param dir_name: name of the directory
171 :type dir_name: str
172 :raise AssertionError: if dir_name is not a directory
173 """
174 assert os.path.isdir(dir_name)
176 for path, dirs, files in os.walk(dir_name):
177 for n, dirname in reversed(list(enumerate(dirs))): 177 ↛ 178line 177 didn't jump to line 178 because the loop on line 177 never started
178 keep = True
179 for exclude_pattern in self.exclude_patterns:
180 if exclude_pattern.match(dirname):
181 keep = False
182 break
183 if not keep:
184 del dirs[n]
186 self.includes.update(
187 {os.path.abspath(full_name): full_name
188 for full_name in
189 (os.path.join(path, file_name)
190 for file_name in files
191 if os.path.splitext(file_name)[1] in (".rsl",
192 ".trlc"))})
194 def register_file(self, file_name, file_content=None, primary=True):
195 """Schedule a file for parsing.
197 :param file_name: name of the file
198 :type file_name: str
199 :raise AssertionError: if the file does not exist
200 :raise AssertionError: if the file is registed more than once
201 :raise TRLC_Error: if the file is not a rsl/trlc file
203 :param file_content: content of the file
204 :type file_content: str
205 :raise AssertionError: if the content is not of type string
207 :param primary: should be False if the file is a potential \
208 include file, and True otherwise.
209 :type primary: bool
211 :return: true if the file could be registered without issues
212 :rtype: bool
213 """
214 assert os.path.isfile(file_name)
215 assert isinstance(file_content, str) or file_content is None
216 # lobster-trace: LRM.Layout
218 try:
219 if file_name.endswith(".rsl"):
220 self.register_rsl_file(file_name, file_content, primary)
221 elif file_name.endswith(".trlc"):
222 self.register_trlc_file(file_name, file_content, primary)
223 else: # pragma: no cover
224 self.mh.error(Location(os.path.basename(file_name)),
225 "is not a rsl or trlc file",
226 fatal = False)
227 return False
229 except TRLC_Error:
230 return False
232 return True
234 def register_directory(self, dir_name):
235 """Schedule a directory tree for parsing.
237 :param dir_name: name of the directory
238 :type file_name: str
239 :raise AssertionError: if the directory does not exist
240 :raise AssertionError: if any item in the directory is already \
241 registered
242 :raise TRLC_Error: on any parse errors
244 :return: true if the directory could be registered without issues
245 :rtype: bool
246 """
247 assert os.path.isdir(dir_name)
248 # lobster-trace: LRM.Layout
250 ok = True
251 for path, dirs, files in os.walk(dir_name):
252 dirs.sort()
254 for n, dirname in reversed(list(enumerate(dirs))):
255 keep = True
256 for exclude_pattern in self.exclude_patterns:
257 if exclude_pattern.match(dirname):
258 keep = False
259 break
260 if not keep:
261 del dirs[n]
263 for file_name in sorted(files):
264 if os.path.splitext(file_name)[1] in (".rsl",
265 ".trlc"):
266 ok &= self.register_file(os.path.join(path, file_name))
267 return ok
269 def register_rsl_file(self, file_name, file_content=None, primary=True):
270 assert os.path.isfile(file_name)
271 assert file_name not in self.rsl_files
272 assert isinstance(file_content, str) or file_content is None
273 assert isinstance(primary, bool)
274 # lobster-trace: LRM.Preamble
276 self.update_common_root(file_name)
277 parser = self.create_parser(file_name,
278 file_content,
279 primary)
280 self.rsl_files[file_name] = parser
281 self.all_files[file_name] = parser
282 if os.path.abspath(file_name) in self.includes:
283 del self.includes[os.path.abspath(file_name)]
285 def register_trlc_file(self, file_name, file_content=None, primary=True):
286 # lobster-trace: LRM.TRLC_File
287 assert os.path.isfile(file_name)
288 assert file_name not in self.trlc_files
289 assert isinstance(file_content, str) or file_content is None
290 assert isinstance(primary, bool)
291 # lobster-trace: LRM.Preamble
293 if not self.parse_trlc: # pragma: no cover
294 # Not executed as process should exit before we attempt this.
295 return
297 self.update_common_root(file_name)
298 parser = self.create_parser(file_name,
299 file_content,
300 primary)
301 self.trlc_files[file_name] = parser
302 self.all_files[file_name] = parser
303 if os.path.abspath(file_name) in self.includes:
304 del self.includes[os.path.abspath(file_name)]
306 def build_graph(self):
307 # lobster-trace: LRM.Preamble
309 # Register all include files not yet registered
310 for file_name in list(sorted(self.includes.values())):
311 self.register_file(file_name, primary=False)
313 # Parse preambles and build dependency graph
314 ok = True
315 graph = self.dep_graph
316 files = {}
317 for container, kind in ((self.rsl_files, "rsl"),
318 (self.trlc_files, "trlc")):
319 # First parse preamble and register packages in graph
320 for file_name in sorted(container):
321 try:
322 parser = container[file_name]
323 parser.parse_preamble(kind)
324 pkg_name = parser.cu.package.name
325 if (pkg_name , "rsl") not in graph:
326 graph[(pkg_name , "rsl")] = set()
327 graph[(pkg_name , "trlc")] = set([(pkg_name , "rsl")])
328 files[(pkg_name , "rsl")] = set()
329 files[(pkg_name , "trlc")] = set()
330 files[(pkg_name , kind)].add(file_name)
331 except TRLC_Error:
332 ok = False
333 self.files_with_preamble_errors.add(file_name)
335 # Then parse all imports and add all valid links
336 for file_name in sorted(container):
337 if file_name in self.files_with_preamble_errors:
338 continue
340 parser = container[file_name]
341 if parser.cu.package is None: 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true
342 continue
343 pkg_name = parser.cu.package.name
344 parser.cu.resolve_imports(self.mh, self.stab)
346 graph[(pkg_name , kind)] |= \
347 {(imported_pkg.name , kind)
348 for imported_pkg in parser.cu.imports}
350 # Build closure for our files
351 work_list = {(parser.cu.package.name , "rsl")
352 for parser in self.rsl_files.values()
353 if parser.cu.package and parser.primary}
354 work_list |= {(parser.cu.package.name , "trlc")
355 for parser in self.trlc_files.values()
356 if parser.cu.package and parser.primary}
357 work_list &= set(graph)
359 required = set()
360 while work_list:
361 node = work_list.pop()
362 required.add(node)
363 work_list |= (graph[node] - required) & set(graph)
365 # Expand into actual file list and flag dependencies
366 file_list = {file_name
367 for node in required
368 for file_name in files[node]}
369 for file_name in file_list:
370 if not self.all_files[file_name].primary:
371 self.all_files[file_name].secondary = True
373 # Record total files that need parsing
374 self.progress_final = len(file_list)
376 return ok
378 def parse_rsl_files(self) -> bool:
379 # lobster-trace: LRM.Preamble
380 # lobster-trace: LRM.RSL_File
382 ok = True
384 # Select RSL files that we should parse
385 rsl_map = {(parser.cu.package.name , "rsl"): parser
386 for parser in self.rsl_files.values()
387 if parser.cu.package and (parser.primary or
388 parser.secondary)}
390 # Parse packages that have no unparsed dependencies. Keep
391 # doing it until we parse everything or until we have reached
392 # a fix point (in which case we have a cycle in our
393 # dependencies).
394 work_list = set(rsl_map)
395 processed = set()
396 while work_list:
397 candidates = {node
398 for node in work_list
399 if len(self.dep_graph.get(node, set()) -
400 processed) == 0}
401 if not candidates:
402 # lobster-trace: LRM.Circular_Dependencies
403 sorted_work_list = sorted(work_list)
404 offender = rsl_map[sorted_work_list[0]]
405 names = {rsl_map[node].cu.package.name:
406 rsl_map[node].cu.location
407 for node in sorted_work_list[1:]}
408 self.mh.error(
409 location = offender.cu.location,
410 message = ("circular inheritence between %s" %
411 " | ".join(sorted(names))),
412 explanation = "\n".join(
413 sorted("%s is declared in %s" %
414 (name,
415 self.mh.cross_file_reference(loc))
416 for name, loc in names.items())),
417 fatal = False)
418 return False
420 for node in sorted(candidates):
421 try:
422 ok &= rsl_map[node].parse_rsl_file()
423 self.signal_progress()
424 except TRLC_Error:
425 ok = False
426 processed.add(node)
428 work_list -= candidates
430 return ok
432 def parse_trlc_files(self) -> bool:
433 # lobster-trace: LRM.TRLC_File
434 # lobster-trace: LRM.Preamble
436 ok = True
438 # Then actually parse
439 for name in sorted(self.trlc_files):
440 parser = self.trlc_files[name]
441 if name in self.files_with_preamble_errors:
442 continue
443 if not (parser.primary or parser.secondary):
444 continue
446 try:
447 ok &= parser.parse_trlc_file()
448 self.signal_progress()
449 except TRLC_Error:
450 ok = False
452 return ok
454 def resolve_record_references(self) -> bool:
455 # lobster-trace: LRM.File_Parsing_References
456 # lobster-trace: LRM.Markup_String_Late_Reference_Resolution
457 # lobster-trace: LRM.Late_Reference_Checking
458 ok = True
459 for package in self.stab.values(ast.Package):
460 for obj in package.symbols.values(ast.Record_Object):
461 try:
462 obj.resolve_references(self.mh)
463 except TRLC_Error:
464 ok = False
466 return ok
468 def perform_checks(self) -> bool:
469 # lobster-trace: LRM.Order_Of_Evaluation_Unordered
470 ok = True
471 for package in self.stab.values(ast.Package):
472 for obj in package.symbols.values(ast.Record_Object):
473 try:
474 if not obj.perform_checks(self.mh, self.stab):
475 ok = False
476 except TRLC_Error:
477 ok = False
479 return ok
481 def process(self):
482 """Parse all registered files.
484 :return: a symbol table (or None if there were any errors)
485 :rtype: Symbol_Table
486 """
487 # lobster-trace: LRM.File_Parsing_Order
488 # lobster-trace: LRM.File_Parsing_References
490 # Notify callback
491 self.callback_parse_begin()
492 self.progress_current = 0
494 # Build dependency graph
495 ok = self.build_graph()
497 # Parse RSL files (topologically sorted, in order to deal with
498 # dependencies)
499 ok &= self.parse_rsl_files()
501 if not self.error_recovery and not ok: # pragma: no cover
502 self.callback_parse_end()
503 return None
505 # Perform sanity checks (enabled by default). We only do this
506 # if there were no errors so far.
507 if self.lint_mode and ok:
508 linter = lint.Linter(mh = self.mh,
509 stab = self.stab,
510 verify_checks = self.verify_mode,
511 debug_vcg = self.debug_vcg)
512 ok &= linter.perform_sanity_checks()
513 # Stop here if we're not processing TRLC files.
514 if not self.parse_trlc: # pragma: no cover
515 self.callback_parse_end()
516 if ok:
517 return self.stab
518 else:
519 return None
521 # Parse TRLC files. Almost all the semantic analysis and name
522 # resolution happens here, with the notable exception of resolving
523 # record references (as we can have circularity here).
524 if not self.parse_trlc_files(): # pragma: no cover
525 self.callback_parse_end()
526 return None
528 # Resolve record reference names and do the missing semantic
529 # analysis.
530 # lobster-trace: LRM.File_Parsing_References
531 if not self.resolve_record_references():
532 self.callback_parse_end()
533 return None
535 if not ok:
536 self.callback_parse_end()
537 return None
539 # Finally, apply user defined checks
540 if not self.perform_checks():
541 self.callback_parse_end()
542 return None
544 if self.lint_mode and ok:
545 linter.verify_imports()
547 self.callback_parse_end()
548 return self.stab
551def trlc():
552 ap = argparse.ArgumentParser(
553 prog="trlc",
554 description="TRLC %s (Python reference implementation)" % TRLC_VERSION,
555 epilog=("TRLC is licensed under the GPLv3."
556 " Report bugs here: %s" % BUGS_URL),
557 allow_abbrev=False,
558 )
559 og_lint = ap.add_argument_group("analysis options")
560 og_lint.add_argument("--no-lint",
561 default=False,
562 action="store_true",
563 help="Disable additional, optional warnings.")
564 og_lint.add_argument("--skip-trlc-files",
565 default=False,
566 action="store_true",
567 help=("Only process rsl files,"
568 " do not process any trlc files."))
569 og_lint.add_argument("--verify",
570 default=False,
571 action="store_true",
572 help=("[EXPERIMENTAL] Attempt to statically"
573 " verify absence of errors in user defined"
574 " checks. Does not yet support all language"
575 " constructs. Requires PyVCG to be "
576 " installed."))
578 og_input = ap.add_argument_group("input options")
579 og_input.add_argument("--include-bazel-dirs",
580 action="store_true",
581 help=("Enter bazel-* directories, which are"
582 " excluded by default."))
583 og_input.add_argument("-I",
584 action="append",
585 dest="include_dirs",
586 help=("Add include path. Files from these"
587 " directories are parsed only when needed."
588 " Can be specified more than once."),
589 default=[])
591 og_output = ap.add_argument_group("output options")
592 og_output.add_argument("--version",
593 default=False,
594 action="store_true",
595 help="Print TRLC version and exit.")
596 og_output.add_argument("--brief",
597 default=False,
598 action="store_true",
599 help=("Simpler output intended for CI. Does not"
600 " show context or additional information,"
601 " but prints the usual summary at the end."))
602 og_output.add_argument("--no-detailed-info",
603 default=False,
604 action="store_true",
605 help=("Do not print counter-examples and other"
606 " supplemental information on failed"
607 " checks. The specific values of"
608 " counter-examples are unpredictable"
609 " from system to system, so if you need"
610 " perfectly reproducible output then use"
611 " this option."))
612 og_output.add_argument("--no-user-warnings",
613 default=False,
614 action="store_true",
615 help=("Do not display any warnings from user"
616 " defined checks, only errors."))
617 og_output.add_argument("--no-error-recovery",
618 default=False,
619 action="store_true",
620 help=("By default the tool attempts to recover"
621 " from parse errors to show more errors, but"
622 " this can occasionally generate weird"
623 " errors. You can use this option to stop"
624 " at the first real errors."))
625 og_output.add_argument("--show-file-list",
626 action="store_true",
627 help=("If there are no errors, produce a summary"
628 " naming every file processed."))
629 og_output.add_argument("--log",
630 nargs = '+',
631 metavar = ("FILE", "PREFIX"),
632 default = None,
633 help = ("Write all output to FILE, optionally"
634 " strip PREFIX from file paths in"
635 " messages. Intended for use as a"
636 " Bazel build action."))
637 og_output.add_argument("--error-on-warnings",
638 action="store_true",
639 help=("If there are warnings, return status code"
640 " 1 instead of 0."))
642 og_debug = ap.add_argument_group("debug options")
643 og_debug.add_argument("--debug-dump",
644 default=False,
645 action="store_true",
646 help="Dump symbol table.")
647 og_debug.add_argument("--debug-api-dump",
648 default=False,
649 action="store_true",
650 help=("Dump json of to_python_object() for all"
651 " objects."))
652 og_debug.add_argument("--debug-vcg",
653 default=False,
654 action="store_true",
655 help=("Emit graph and individual VCs. Requires"
656 " graphviz to be installed."))
658 ap.add_argument("items",
659 nargs="*",
660 metavar="DIR|FILE")
661 options = ap.parse_args()
663 if options.log:
664 if len(options.log) > 2:
665 ap.error("--log accepts at most 2 values: FILE and optionally PREFIX")
666 if len(options.log) == 1:
667 options.log.append(None)
669 if options.version: # pragma: no cover
670 print(TRLC_VERSION)
671 sys.exit(0)
673 if options.verify and not VCG_API_AVAILABLE: # pragma: no cover
674 ap.error("The --verify option requires the optional dependency"
675 " CVC5")
677 mh = Message_Handler(options.brief,
678 not options.no_detailed_info,
679 out_path = options.log[0] if options.log else None,
680 strip_prefix = options.log[1] if options.log else None)
682 if options.no_user_warnings: # pragma: no cover
683 mh.suppress(Kind.USER_WARNING)
685 sm = Source_Manager(mh = mh,
686 lint_mode = not options.no_lint,
687 parse_trlc = not options.skip_trlc_files,
688 verify_mode = options.verify,
689 debug_vcg = options.debug_vcg,
690 error_recovery = not options.no_error_recovery)
692 if not options.include_bazel_dirs: # pragma: no cover
693 sm.exclude_patterns.append(re.compile("^bazel-.*$"))
695 # Process includes
696 ok = True
697 for path_name in options.include_dirs:
698 if not os.path.isdir(path_name): 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true
699 ap.error("include path %s is not a directory" % path_name)
700 for path_name in options.include_dirs:
701 sm.register_include(path_name)
703 # Process input files, defaulting to the current directory if none
704 # given.
705 for path_name in options.items:
706 if not (os.path.isdir(path_name) or
707 os.path.isfile(path_name)): # pragma: no cover
708 ap.error("%s is not a file or directory" % path_name)
709 if options.items:
710 for path_name in options.items:
711 if os.path.isdir(path_name):
712 ok &= sm.register_directory(path_name)
713 else: # pragma: no cover
714 try:
715 ok &= sm.register_file(path_name)
716 except TRLC_Error:
717 ok = False
718 else: # pragma: no cover
719 ok &= sm.register_directory(".")
721 if not ok:
722 mh.close()
723 return 1
725 if sm.process() is None:
726 ok = False
728 if ok:
729 if options.debug_dump: # pragma: no cover
730 sm.stab.dump()
731 if options.debug_api_dump:
732 tmp = {}
733 for obj in sm.stab.iter_record_objects():
734 tmp[obj.name] = obj.to_python_dict()
735 for key in tmp[obj.name]:
736 if isinstance(tmp[obj.name][key], Fraction): 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 tmp[obj.name][key] = float(tmp[obj.name][key])
739 print(json.dumps(tmp, indent=2, sort_keys=True), file=mh.out)
741 total_models = len(sm.rsl_files)
742 parsed_models = len([item
743 for item in sm.rsl_files.values()
744 if item.primary or item.secondary])
745 total_trlc = len(sm.trlc_files)
746 parsed_trlc = len([item
747 for item in sm.trlc_files.values()
748 if item.primary or item.secondary])
750 def count(parsed, total, what):
751 rv = str(parsed)
752 if parsed < total:
753 rv += " (of %u)" % total
754 rv += " " + what
755 if total == 0 or total > 1:
756 rv += "s"
757 return rv
759 summary = "Processed %s" % count(parsed_models,
760 total_models,
761 "model")
763 if not options.skip_trlc_files: # pragma: no cover
764 summary += " and %s" % count(parsed_trlc,
765 total_trlc,
766 "requirement file")
768 summary += " and found"
770 if mh.errors and mh.warnings:
771 summary += " %s" % count(mh.warnings, mh.warnings, "warning")
772 summary += " and %s" % count(mh.errors, mh.errors, "error")
773 elif mh.warnings:
774 summary += " %s" % count(mh.warnings, mh.warnings, "warning")
775 elif mh.errors:
776 summary += " %s" % count(mh.errors, mh.errors, "error")
777 else:
778 summary += " no issues"
780 if mh.suppressed: # pragma: no cover
781 summary += " with %u supressed messages" % mh.suppressed
783 print(summary, file=mh.out)
785 if options.show_file_list and ok: # pragma: no cover
786 def get_status(parser):
787 if parser.primary:
788 return "[Primary] "
789 elif parser.secondary:
790 return "[Included]"
791 else:
792 return "[Excluded]"
794 for filename in sorted(sm.rsl_files):
795 parser = sm.rsl_files[filename]
796 print("> %s Model %s (Package %s)" %
797 (get_status(parser),
798 filename,
799 parser.cu.package.name), file=mh.out)
800 if not options.skip_trlc_files:
801 for filename in sorted(sm.trlc_files):
802 parser = sm.trlc_files[filename]
803 print("> %s Requirements %s (Package %s)" %
804 (get_status(parser),
805 filename,
806 parser.cu.package.name), file=mh.out)
808 if ok:
809 if (options.error_on_warnings and mh.warnings) or mh.errors: # pragma: no cover
810 rv = 1
811 else:
812 rv = 0
813 else:
814 rv = 1
815 mh.close()
816 return rv
819def main():
820 try:
821 return trlc()
822 except BrokenPipeError:
823 # Python flushes standard streams on exit; redirect remaining output
824 # to devnull to avoid another BrokenPipeError at shutdown
825 devnull = os.open(os.devnull, os.O_WRONLY)
826 os.dup2(devnull, sys.stdout.fileno())
827 return 141
830if __name__ == "__main__":
831 sys.exit(main())