Coverage for trlc/trlc.py: 92%
364 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-01 05:08 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-01 05:08 +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.lexer_md import MD_Lexer
32from trlc.parser import Parser
33from trlc.trlc_markdown_parser import TrlcMarkdownParser
34from trlc.version import BUGS_URL, TRLC_VERSION
36# pylint: disable=unused-import
37try:
38 import cvc5
40 VCG_API_AVAILABLE = True
41except ImportError: # pragma: no cover
42 VCG_API_AVAILABLE = False
44MARKDOWN_EXTENSION = ".trlc.md"
47class Source_Manager:
48 """Dependency and source manager for TRLC.
50 This is the main entry point when using the Python API. Create an
51 instance of this, register the files you want to look at, and
52 finally call the process method.
54 :param mh: The message handler to use
55 :type mh: Message_Handler
57 :param error_recovery: If true attempts to continue parsing after \
58 errors. This may generate weird error messages since it's impossible \
59 to reliably recover the parse context in all cases.
60 :type error_recovery: bool
62 :param lint_mode: If true enables additional warning messages.
63 :type lint_mode: bool
65 :param verify_mode: If true performs in-depth static analysis for \
66 user-defined checks. Requires CVC5 and PyVCG to be installed.
67 :type verify_mode: bool
69 :param parse_trlc: If true parses trlc files, otherwise they are \
70 ignored.
71 :type parse_trlc: bool
73 :param debug_vcg: If true and verify_mode is also true, emit the \
74 individual SMTLIB2 VCs and generate a picture of the program \
75 graph. Requires Graphviz to be installed.
76 :type debug_vcg: bool
78 """
80 def __init__(
81 self,
82 mh,
83 lint_mode=True,
84 parse_trlc=True,
85 verify_mode=False,
86 debug_vcg=False,
87 error_recovery=True,
88 ):
89 assert isinstance(mh, Message_Handler)
90 assert isinstance(lint_mode, bool)
91 assert isinstance(parse_trlc, bool)
92 assert isinstance(verify_mode, bool)
93 assert isinstance(debug_vcg, bool)
95 self.mh = mh
96 self.mh.sm = self
97 self.stab = ast.Symbol_Table.create_global_table(mh)
98 self.includes = {}
99 self.rsl_files = {}
100 self.trlc_files = {}
101 self.all_files = {}
102 self.dep_graph = {}
104 self.files_with_preamble_errors = set()
106 self.lint_mode = lint_mode
107 self.parse_trlc = parse_trlc
108 self.verify_mode = verify_mode
109 self.debug_vcg = debug_vcg
110 self.error_recovery = error_recovery
112 self.exclude_patterns = []
113 self.common_root = None
115 self.progress_current = 0
116 self.progress_final = 0
118 def callback_parse_begin(self):
119 pass
121 def callback_parse_progress(self, progress):
122 assert isinstance(progress, int)
124 def callback_parse_end(self):
125 pass
127 def signal_progress(self):
128 self.progress_current += 1
129 if self.progress_final:
130 progress = (self.progress_current * 100) // self.progress_final
131 else: # pragma: no cover
132 progress = 100
133 self.callback_parse_progress(min(progress, 100))
135 def cross_file_reference(self, location):
136 assert isinstance(location, Location)
138 if self.common_root is None: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 return location.to_string(False)
140 elif location.line_no is None:
141 return os.path.relpath(location.file_name, self.common_root)
142 else:
143 return "%s:%u" % (
144 os.path.relpath(location.file_name, self.common_root),
145 location.line_no,
146 )
148 def update_common_root(self, file_name):
149 assert isinstance(file_name, str)
151 if self.common_root is None:
152 self.common_root = os.path.dirname(os.path.abspath(file_name))
153 else:
154 new_root = os.path.dirname(os.path.abspath(file_name))
155 for n, (char_a, char_b) in enumerate(zip(self.common_root, new_root)):
156 if char_a != char_b:
157 self.common_root = self.common_root[0:n]
158 break
160 def create_parser(self, file_name, file_content=None, primary_file=True):
161 assert os.path.isfile(file_name)
162 assert isinstance(file_content, str) or file_content is None
163 assert isinstance(primary_file, bool)
165 if file_name.endswith(MARKDOWN_EXTENSION):
166 lexer = MD_Lexer(self.mh, file_name, file_content)
167 return TrlcMarkdownParser(
168 mh=self.mh,
169 stab=self.stab,
170 file_name=file_name,
171 lint_mode=self.lint_mode,
172 error_recovery=self.error_recovery,
173 primary_file=primary_file,
174 lexer=lexer,
175 )
177 lexer = Token_Stream(self.mh, file_name, file_content)
179 return Parser(
180 mh=self.mh,
181 stab=self.stab,
182 file_name=file_name,
183 lint_mode=self.lint_mode,
184 error_recovery=self.error_recovery,
185 primary_file=primary_file,
186 lexer=lexer,
187 )
189 def register_include(self, dir_name):
190 """Make contents of a directory available for automatic inclusion
192 :param dir_name: name of the directory
193 :type dir_name: str
194 :raise AssertionError: if dir_name is not a directory
195 """
196 assert os.path.isdir(dir_name)
198 for path, dirs, files in os.walk(dir_name):
199 for n, dirname in reversed(list(enumerate(dirs))): 199 ↛ 200line 199 didn't jump to line 200 because the loop on line 199 never started
200 keep = True
201 for exclude_pattern in self.exclude_patterns:
202 if exclude_pattern.match(dirname):
203 keep = False
204 break
205 if not keep:
206 del dirs[n]
208 self.includes.update(
209 {
210 os.path.abspath(full_name): full_name
211 for full_name in (
212 os.path.join(path, file_name)
213 for file_name in files
214 if os.path.splitext(file_name)[1] in (".rsl", ".trlc")
215 )
216 }
217 )
219 def register_file(self, file_name, file_content=None, primary=True):
220 """Schedule a file for parsing.
222 :param file_name: name of the file
223 :type file_name: str
224 :raise AssertionError: if the file does not exist
225 :raise AssertionError: if the file is registed more than once
226 :raise TRLC_Error: if the file is not a rsl, trlc or trlc.md file
228 :param file_content: content of the file
229 :type file_content: str
230 :raise AssertionError: if the content is not of type string
232 :param primary: should be False if the file is a potential \
233 include file, and True otherwise.
234 :type primary: bool
236 :return: true if the file could be registered without issues
237 :rtype: bool
238 """
239 assert os.path.isfile(file_name)
240 assert isinstance(file_content, str) or file_content is None
241 # lobster-trace: LRM.Layout
243 try:
244 if file_name.endswith(".rsl"):
245 self.register_rsl_file(file_name, file_content, primary)
246 elif file_name.endswith(".trlc") or file_name.endswith(MARKDOWN_EXTENSION):
247 self.register_trlc_file(file_name, file_content, primary)
248 else: # pragma: no cover
249 self.mh.error(
250 Location(os.path.basename(file_name)),
251 "is not a rsl, trlc or trlc.md file",
252 fatal=False,
253 )
254 return False
256 except TRLC_Error:
257 return False
259 return True
261 def register_directory(self, dir_name):
262 """Schedule a directory tree for parsing.
264 :param dir_name: name of the directory
265 :type file_name: str
266 :raise AssertionError: if the directory does not exist
267 :raise AssertionError: if any item in the directory is already \
268 registered
269 :raise TRLC_Error: on any parse errors
271 :return: true if the directory could be registered without issues
272 :rtype: bool
273 """
274 assert os.path.isdir(dir_name)
275 # lobster-trace: LRM.Layout
277 ok = True
278 for path, dirs, files in os.walk(dir_name):
279 dirs.sort()
281 for n, dirname in reversed(list(enumerate(dirs))):
282 keep = True
283 for exclude_pattern in self.exclude_patterns:
284 if exclude_pattern.match(dirname):
285 keep = False
286 break
287 if not keep:
288 del dirs[n]
290 for file_name in sorted(files):
291 if os.path.splitext(file_name)[1] in (
292 ".rsl",
293 ".trlc",
294 ) or file_name.endswith(MARKDOWN_EXTENSION):
295 ok &= self.register_file(os.path.join(path, file_name))
296 return ok
298 def register_rsl_file(self, file_name, file_content=None, primary=True):
299 assert os.path.isfile(file_name)
300 assert file_name not in self.rsl_files
301 assert isinstance(file_content, str) or file_content is None
302 assert isinstance(primary, bool)
303 # lobster-trace: LRM.Preamble
305 self.update_common_root(file_name)
306 parser = self.create_parser(file_name, file_content, primary)
307 self.rsl_files[file_name] = parser
308 self.all_files[file_name] = parser
309 if os.path.abspath(file_name) in self.includes:
310 del self.includes[os.path.abspath(file_name)]
312 def register_trlc_file(self, file_name, file_content=None, primary=True):
313 # lobster-trace: LRM.TRLC_File
314 assert os.path.isfile(file_name)
315 assert file_name not in self.trlc_files
316 assert isinstance(file_content, str) or file_content is None
317 assert isinstance(primary, bool)
318 # lobster-trace: LRM.Preamble
320 if not self.parse_trlc: # pragma: no cover
321 # Not executed as process should exit before we attempt this.
322 return
324 self.update_common_root(file_name)
325 parser = self.create_parser(file_name, file_content, primary)
326 self.trlc_files[file_name] = parser
327 self.all_files[file_name] = parser
328 if os.path.abspath(file_name) in self.includes:
329 del self.includes[os.path.abspath(file_name)]
331 def build_graph(self):
332 # lobster-trace: LRM.Preamble
334 # Register all include files not yet registered
335 for file_name in list(sorted(self.includes.values())):
336 self.register_file(file_name, primary=False)
338 # Parse preambles and build dependency graph
339 ok = True
340 graph = self.dep_graph
341 files = {}
342 for container, kind in ((self.rsl_files, "rsl"), (self.trlc_files, "trlc")):
343 # First parse preamble and register packages in graph
344 for file_name in sorted(container):
345 try:
346 parser = container[file_name]
347 parser.parse_preamble(kind)
348 pkg_name = parser.cu.package.name
349 if (pkg_name, "rsl") not in graph:
350 graph[(pkg_name, "rsl")] = set()
351 graph[(pkg_name, "trlc")] = set([(pkg_name, "rsl")])
352 files[(pkg_name, "rsl")] = set()
353 files[(pkg_name, "trlc")] = set()
354 files[(pkg_name, kind)].add(file_name)
355 except TRLC_Error:
356 ok = False
357 self.files_with_preamble_errors.add(file_name)
359 # Then parse all imports and add all valid links
360 for file_name in sorted(container):
361 if file_name in self.files_with_preamble_errors:
362 continue
364 parser = container[file_name]
365 if parser.cu.package is None: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true
366 continue
367 pkg_name = parser.cu.package.name
368 parser.cu.resolve_imports(self.mh, self.stab)
370 graph[(pkg_name, kind)] |= {
371 (imported_pkg.name, kind) for imported_pkg in parser.cu.imports
372 }
374 # Build closure for our files
375 work_list = {
376 (parser.cu.package.name, "rsl")
377 for parser in self.rsl_files.values()
378 if parser.cu.package and parser.primary
379 }
380 work_list |= {
381 (parser.cu.package.name, "trlc")
382 for parser in self.trlc_files.values()
383 if parser.cu.package and parser.primary
384 }
385 work_list &= set(graph)
387 required = set()
388 while work_list:
389 node = work_list.pop()
390 required.add(node)
391 work_list |= (graph[node] - required) & set(graph)
393 # Expand into actual file list and flag dependencies
394 file_list = {file_name for node in required for file_name in files[node]}
395 for file_name in file_list:
396 if not self.all_files[file_name].primary:
397 self.all_files[file_name].secondary = True
399 # Record total files that need parsing
400 self.progress_final = len(file_list)
402 return ok
404 def parse_rsl_files(self) -> bool:
405 # lobster-trace: LRM.Preamble
406 # lobster-trace: LRM.RSL_File
408 ok = True
410 # Select RSL files that we should parse
411 rsl_map = {
412 (parser.cu.package.name, "rsl"): parser
413 for parser in self.rsl_files.values()
414 if parser.cu.package and (parser.primary or parser.secondary)
415 }
417 # Parse packages that have no unparsed dependencies. Keep
418 # doing it until we parse everything or until we have reached
419 # a fix point (in which case we have a cycle in our
420 # dependencies).
421 work_list = set(rsl_map)
422 processed = set()
423 while work_list:
424 candidates = {
425 node
426 for node in work_list
427 if len(self.dep_graph.get(node, set()) - processed) == 0
428 }
429 if not candidates:
430 # lobster-trace: LRM.Circular_Dependencies
431 sorted_work_list = sorted(work_list)
432 offender = rsl_map[sorted_work_list[0]]
433 names = {
434 rsl_map[node].cu.package.name: rsl_map[node].cu.location
435 for node in sorted_work_list[1:]
436 }
437 self.mh.error(
438 location=offender.cu.location,
439 message=(
440 "circular inheritence between %s" % " | ".join(sorted(names))
441 ),
442 explanation="\n".join(
443 sorted(
444 "%s is declared in %s"
445 % (name, self.mh.cross_file_reference(loc))
446 for name, loc in names.items()
447 )
448 ),
449 fatal=False,
450 )
451 return False
453 for node in sorted(candidates):
454 try:
455 ok &= rsl_map[node].parse_rsl_file()
456 self.signal_progress()
457 except TRLC_Error:
458 ok = False
459 processed.add(node)
461 work_list -= candidates
463 return ok
465 def parse_trlc_files(self) -> bool:
466 # lobster-trace: LRM.TRLC_File
467 # lobster-trace: LRM.Preamble
469 ok = True
471 # Then actually parse
472 for name in sorted(self.trlc_files):
473 parser = self.trlc_files[name]
474 if name in self.files_with_preamble_errors:
475 continue
476 if not (parser.primary or parser.secondary):
477 continue
479 try:
480 ok &= parser.parse_trlc_file()
481 self.signal_progress()
482 except TRLC_Error:
483 ok = False
485 return ok
487 def resolve_record_references(self) -> bool:
488 # lobster-trace: LRM.File_Parsing_References
489 # lobster-trace: LRM.Markup_String_Late_Reference_Resolution
490 # lobster-trace: LRM.Late_Reference_Checking
491 ok = True
492 for package in self.stab.values(ast.Package):
493 for obj in package.symbols.values(ast.Record_Object):
494 try:
495 obj.resolve_references(self.mh)
496 except TRLC_Error:
497 ok = False
499 return ok
501 def perform_checks(self) -> bool:
502 # lobster-trace: LRM.Order_Of_Evaluation_Unordered
503 ok = True
504 for package in self.stab.values(ast.Package):
505 for obj in package.symbols.values(ast.Record_Object):
506 try:
507 if not obj.perform_checks(self.mh, self.stab):
508 ok = False
509 except TRLC_Error:
510 ok = False
512 return ok
514 def process(self):
515 """Parse all registered files.
517 :return: a symbol table (or None if there were any errors)
518 :rtype: Symbol_Table
519 """
520 # lobster-trace: LRM.File_Parsing_Order
521 # lobster-trace: LRM.File_Parsing_References
523 # Notify callback
524 self.callback_parse_begin()
525 self.progress_current = 0
527 # Build dependency graph
528 ok = self.build_graph()
530 # Parse RSL files (topologically sorted, in order to deal with
531 # dependencies)
532 ok &= self.parse_rsl_files()
534 if not self.error_recovery and not ok: # pragma: no cover
535 self.callback_parse_end()
536 return None
538 # ─────────────────────────────────────────────────────────────
539 # Phase 2: reprocess markdown files with RSL types available
540 # ─────────────────────────────────────────────────────────────
541 for _md_fname in sorted(self.trlc_files):
542 if not _md_fname.endswith(MARKDOWN_EXTENSION):
543 continue
544 _md_parser = self.trlc_files[_md_fname]
545 if not (_md_parser.primary or _md_parser.secondary): 545 ↛ 546line 545 didn't jump to line 546 because the condition on line 545 was never true
546 continue
547 if _md_fname in self.files_with_preamble_errors: 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true
548 continue
549 _md_lexer = getattr(_md_parser, "lexer", None)
550 if isinstance(_md_lexer, MD_Lexer): 550 ↛ 541line 550 didn't jump to line 541 because the condition on line 550 was always true
551 _md_lexer.prepare_phase2(self.stab) # Rebuild tokens with types
552 _md_parser.ct = None # Reset parser cursor
553 _md_parser.advance() # Prime first body token
555 # Perform sanity checks (enabled by default). We only do this
556 # if there were no errors so far.
557 if self.lint_mode and ok:
558 linter = lint.Linter(
559 mh=self.mh,
560 stab=self.stab,
561 verify_checks=self.verify_mode,
562 debug_vcg=self.debug_vcg,
563 )
564 ok &= linter.perform_sanity_checks()
565 # Stop here if we're not processing TRLC files.
566 if not self.parse_trlc: # pragma: no cover
567 self.callback_parse_end()
568 if ok:
569 return self.stab
570 else:
571 return None
573 # Parse TRLC files. Almost all the semantic analysis and name
574 # resolution happens here, with the notable exception of resolving
575 # record references (as we can have circularity here).
576 if not self.parse_trlc_files(): # pragma: no cover
577 self.callback_parse_end()
578 return None
580 # Resolve record reference names and do the missing semantic
581 # analysis.
582 # lobster-trace: LRM.File_Parsing_References
583 if not self.resolve_record_references():
584 self.callback_parse_end()
585 return None
587 if not ok:
588 self.callback_parse_end()
589 return None
591 # Finally, apply user defined checks
592 if not self.perform_checks():
593 self.callback_parse_end()
594 return None
596 if self.lint_mode and ok:
597 linter.verify_imports()
599 self.callback_parse_end()
600 return self.stab
603def trlc():
604 ap = argparse.ArgumentParser(
605 prog="trlc",
606 description="TRLC %s (Python reference implementation)" % TRLC_VERSION,
607 epilog=("TRLC is licensed under the GPLv3. Report bugs here: %s" % BUGS_URL),
608 allow_abbrev=False,
609 )
610 og_lint = ap.add_argument_group("analysis options")
611 og_lint.add_argument(
612 "--no-lint",
613 default=False,
614 action="store_true",
615 help="Disable additional, optional warnings.",
616 )
617 og_lint.add_argument(
618 "--skip-trlc-files",
619 default=False,
620 action="store_true",
621 help=("Only process rsl files, do not process any trlc files."),
622 )
623 og_lint.add_argument(
624 "--verify",
625 default=False,
626 action="store_true",
627 help=(
628 "[EXPERIMENTAL] Attempt to statically"
629 " verify absence of errors in user defined"
630 " checks. Does not yet support all language"
631 " constructs. Requires PyVCG to be "
632 " installed."
633 ),
634 )
636 og_input = ap.add_argument_group("input options")
637 og_input.add_argument(
638 "--include-bazel-dirs",
639 action="store_true",
640 help=("Enter bazel-* directories, which are excluded by default."),
641 )
642 og_input.add_argument(
643 "-I",
644 action="append",
645 dest="include_dirs",
646 help=(
647 "Add include path. Files from these"
648 " directories are parsed only when needed."
649 " Can be specified more than once."
650 ),
651 default=[],
652 )
654 og_output = ap.add_argument_group("output options")
655 og_output.add_argument(
656 "--version",
657 default=False,
658 action="store_true",
659 help="Print TRLC version and exit.",
660 )
661 og_output.add_argument(
662 "--brief",
663 default=False,
664 action="store_true",
665 help=(
666 "Simpler output intended for CI. Does not"
667 " show context or additional information,"
668 " but prints the usual summary at the end."
669 ),
670 )
671 og_output.add_argument(
672 "--no-detailed-info",
673 default=False,
674 action="store_true",
675 help=(
676 "Do not print counter-examples and other"
677 " supplemental information on failed"
678 " checks. The specific values of"
679 " counter-examples are unpredictable"
680 " from system to system, so if you need"
681 " perfectly reproducible output then use"
682 " this option."
683 ),
684 )
685 og_output.add_argument(
686 "--no-user-warnings",
687 default=False,
688 action="store_true",
689 help=("Do not display any warnings from user defined checks, only errors."),
690 )
691 og_output.add_argument(
692 "--no-error-recovery",
693 default=False,
694 action="store_true",
695 help=(
696 "By default the tool attempts to recover"
697 " from parse errors to show more errors, but"
698 " this can occasionally generate weird"
699 " errors. You can use this option to stop"
700 " at the first real errors."
701 ),
702 )
703 og_output.add_argument(
704 "--show-file-list",
705 action="store_true",
706 help=("If there are no errors, produce a summary naming every file processed."),
707 )
708 og_output.add_argument(
709 "--log",
710 nargs="+",
711 metavar=("FILE", "PREFIX"),
712 default=None,
713 help=(
714 "Write all output to FILE, optionally"
715 " strip PREFIX from file paths in"
716 " messages. Intended for use as a"
717 " Bazel build action."
718 ),
719 )
720 og_output.add_argument(
721 "--error-on-warnings",
722 action="store_true",
723 help=("If there are warnings, return status code 1 instead of 0."),
724 )
726 og_debug = ap.add_argument_group("debug options")
727 og_debug.add_argument(
728 "--debug-dump", default=False, action="store_true", help="Dump symbol table."
729 )
730 og_debug.add_argument(
731 "--debug-api-dump",
732 default=False,
733 action="store_true",
734 help=("Dump json of to_python_object() for all objects."),
735 )
736 og_debug.add_argument(
737 "--debug-vcg",
738 default=False,
739 action="store_true",
740 help=("Emit graph and individual VCs. Requires graphviz to be installed."),
741 )
743 ap.add_argument("items", nargs="*", metavar="DIR|FILE")
744 options = ap.parse_args()
746 if options.log:
747 if len(options.log) > 2:
748 ap.error("--log accepts at most 2 values: FILE and optionally PREFIX")
749 if len(options.log) == 1:
750 options.log.append(None)
752 if options.version: # pragma: no cover
753 print(TRLC_VERSION)
754 sys.exit(0)
756 if options.verify and not VCG_API_AVAILABLE: # pragma: no cover
757 ap.error("The --verify option requires the optional dependency CVC5")
759 mh = Message_Handler(
760 options.brief,
761 not options.no_detailed_info,
762 out_path=options.log[0] if options.log else None,
763 strip_prefix=options.log[1] if options.log else None,
764 )
766 if options.no_user_warnings: # pragma: no cover
767 mh.suppress(Kind.USER_WARNING)
769 sm = Source_Manager(
770 mh=mh,
771 lint_mode=not options.no_lint,
772 parse_trlc=not options.skip_trlc_files,
773 verify_mode=options.verify,
774 debug_vcg=options.debug_vcg,
775 error_recovery=not options.no_error_recovery,
776 )
778 if not options.include_bazel_dirs: # pragma: no cover
779 sm.exclude_patterns.append(re.compile("^bazel-.*$"))
781 # Process includes
782 ok = True
783 for path_name in options.include_dirs:
784 if not os.path.isdir(path_name): 784 ↛ 785line 784 didn't jump to line 785 because the condition on line 784 was never true
785 ap.error("include path %s is not a directory" % path_name)
786 for path_name in options.include_dirs:
787 sm.register_include(path_name)
789 # Process input files, defaulting to the current directory if none
790 # given.
791 for path_name in options.items:
792 if not (
793 os.path.isdir(path_name) or os.path.isfile(path_name)
794 ): # pragma: no cover
795 ap.error("%s is not a file or directory" % path_name)
796 if options.items:
797 for path_name in options.items:
798 if os.path.isdir(path_name):
799 ok &= sm.register_directory(path_name)
800 else: # pragma: no cover
801 try:
802 ok &= sm.register_file(path_name)
803 except TRLC_Error:
804 ok = False
805 else: # pragma: no cover
806 ok &= sm.register_directory(".")
808 if not ok:
809 mh.close()
810 return 1
812 if sm.process() is None:
813 ok = False
815 if ok:
816 if options.debug_dump: # pragma: no cover
817 sm.stab.dump()
818 if options.debug_api_dump:
819 tmp = {}
820 for obj in sm.stab.iter_record_objects():
821 tmp[obj.name] = obj.to_python_dict()
822 for key in tmp[obj.name]:
823 if isinstance(tmp[obj.name][key], Fraction): 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true
824 tmp[obj.name][key] = float(tmp[obj.name][key])
826 print(json.dumps(tmp, indent=2, sort_keys=True), file=mh.out)
828 total_models = len(sm.rsl_files)
829 parsed_models = len(
830 [item for item in sm.rsl_files.values() if item.primary or item.secondary]
831 )
832 total_trlc = len(sm.trlc_files)
833 parsed_trlc = len(
834 [item for item in sm.trlc_files.values() if item.primary or item.secondary]
835 )
837 def count(parsed, total, what):
838 rv = str(parsed)
839 if parsed < total:
840 rv += " (of %u)" % total
841 rv += " " + what
842 if total == 0 or total > 1:
843 rv += "s"
844 return rv
846 summary = "Processed %s" % count(parsed_models, total_models, "model")
848 if not options.skip_trlc_files: # pragma: no cover
849 summary += " and %s" % count(parsed_trlc, total_trlc, "requirement file")
851 summary += " and found"
853 if mh.errors and mh.warnings:
854 summary += " %s" % count(mh.warnings, mh.warnings, "warning")
855 summary += " and %s" % count(mh.errors, mh.errors, "error")
856 elif mh.warnings:
857 summary += " %s" % count(mh.warnings, mh.warnings, "warning")
858 elif mh.errors:
859 summary += " %s" % count(mh.errors, mh.errors, "error")
860 else:
861 summary += " no issues"
863 if mh.suppressed: # pragma: no cover
864 summary += " with %u supressed messages" % mh.suppressed
866 print(summary, file=mh.out)
868 if options.show_file_list and ok: # pragma: no cover
870 def get_status(parser):
871 if parser.primary:
872 return "[Primary] "
873 elif parser.secondary:
874 return "[Included]"
875 else:
876 return "[Excluded]"
878 for filename in sorted(sm.rsl_files):
879 parser = sm.rsl_files[filename]
880 print(
881 "> %s Model %s (Package %s)"
882 % (get_status(parser), filename, parser.cu.package.name),
883 file=mh.out,
884 )
885 if not options.skip_trlc_files:
886 for filename in sorted(sm.trlc_files):
887 parser = sm.trlc_files[filename]
888 print(
889 "> %s Requirements %s (Package %s)"
890 % (get_status(parser), filename, parser.cu.package.name),
891 file=mh.out,
892 )
894 if ok:
895 if (options.error_on_warnings and mh.warnings) or mh.errors: # pragma: no cover
896 rv = 1
897 else:
898 rv = 0
899 else:
900 rv = 1
901 mh.close()
902 return rv
905def main():
906 try:
907 return trlc()
908 except BrokenPipeError:
909 # Python flushes standard streams on exit; redirect remaining output
910 # to devnull to avoid another BrokenPipeError at shutdown
911 devnull = os.open(os.devnull, os.O_WRONLY)
912 os.dup2(devnull, sys.stdout.fileno())
913 return 141
916if __name__ == "__main__":
917 sys.exit(main())