Coverage for trlc/trlc.py: 93%

345 statements  

« 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/>. 

20 

21import argparse 

22import json 

23import os 

24import re 

25import sys 

26from fractions import Fraction 

27 

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 

33 

34# pylint: disable=unused-import 

35try: 

36 import cvc5 

37 VCG_API_AVAILABLE = True 

38except ImportError: # pragma: no cover 

39 VCG_API_AVAILABLE = False 

40 

41 

42class Source_Manager: 

43 """Dependency and source manager for TRLC. 

44 

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. 

48 

49 :param mh: The message handler to use 

50 :type mh: Message_Handler 

51 

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 

56 

57 :param lint_mode: If true enables additional warning messages. 

58 :type lint_mode: bool 

59 

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 

63 

64 :param parse_trlc: If true parses trlc files, otherwise they are \ 

65 ignored. 

66 :type parse_trlc: bool 

67 

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 

72 

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) 

85 

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 = {} 

94 

95 self.files_with_preamble_errors = set() 

96 

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 

102 

103 self.exclude_patterns = [] 

104 self.common_root = None 

105 

106 self.progress_current = 0 

107 self.progress_final = 0 

108 

109 def callback_parse_begin(self): 

110 pass 

111 

112 def callback_parse_progress(self, progress): 

113 assert isinstance(progress, int) 

114 

115 def callback_parse_end(self): 

116 pass 

117 

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)) 

125 

126 def cross_file_reference(self, location): 

127 assert isinstance(location, Location) 

128 

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) 

138 

139 def update_common_root(self, file_name): 

140 assert isinstance(file_name, str) 

141 

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 

151 

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) 

156 

157 lexer = Token_Stream(self.mh, file_name, file_content) 

158 

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) 

166 

167 def register_include(self, dir_name): 

168 """Make contents of a directory available for automatic inclusion 

169 

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) 

175 

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] 

185 

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"))}) 

193 

194 def register_file(self, file_name, file_content=None, primary=True): 

195 """Schedule a file for parsing. 

196 

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 

202 

203 :param file_content: content of the file 

204 :type file_content: str 

205 :raise AssertionError: if the content is not of type string 

206 

207 :param primary: should be False if the file is a potential \ 

208 include file, and True otherwise. 

209 :type primary: bool 

210 

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 

217 

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 

228 

229 except TRLC_Error: 

230 return False 

231 

232 return True 

233 

234 def register_directory(self, dir_name): 

235 """Schedule a directory tree for parsing. 

236 

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 

243 

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 

249 

250 ok = True 

251 for path, dirs, files in os.walk(dir_name): 

252 dirs.sort() 

253 

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] 

262 

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 

268 

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 

275 

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)] 

284 

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 

292 

293 if not self.parse_trlc: # pragma: no cover 

294 # Not executed as process should exit before we attempt this. 

295 return 

296 

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)] 

305 

306 def build_graph(self): 

307 # lobster-trace: LRM.Preamble 

308 

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) 

312 

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) 

334 

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 

339 

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) 

345 

346 graph[(pkg_name , kind)] |= \ 

347 {(imported_pkg.name , kind) 

348 for imported_pkg in parser.cu.imports} 

349 

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) 

358 

359 required = set() 

360 while work_list: 

361 node = work_list.pop() 

362 required.add(node) 

363 work_list |= (graph[node] - required) & set(graph) 

364 

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 

372 

373 # Record total files that need parsing 

374 self.progress_final = len(file_list) 

375 

376 return ok 

377 

378 def parse_rsl_files(self) -> bool: 

379 # lobster-trace: LRM.Preamble 

380 # lobster-trace: LRM.RSL_File 

381 

382 ok = True 

383 

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)} 

389 

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 

419 

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) 

427 

428 work_list -= candidates 

429 

430 return ok 

431 

432 def parse_trlc_files(self) -> bool: 

433 # lobster-trace: LRM.TRLC_File 

434 # lobster-trace: LRM.Preamble 

435 

436 ok = True 

437 

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 

445 

446 try: 

447 ok &= parser.parse_trlc_file() 

448 self.signal_progress() 

449 except TRLC_Error: 

450 ok = False 

451 

452 return ok 

453 

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 

465 

466 return ok 

467 

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 

478 

479 return ok 

480 

481 def process(self): 

482 """Parse all registered files. 

483 

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 

489 

490 # Notify callback 

491 self.callback_parse_begin() 

492 self.progress_current = 0 

493 

494 # Build dependency graph 

495 ok = self.build_graph() 

496 

497 # Parse RSL files (topologically sorted, in order to deal with 

498 # dependencies) 

499 ok &= self.parse_rsl_files() 

500 

501 if not self.error_recovery and not ok: # pragma: no cover 

502 self.callback_parse_end() 

503 return None 

504 

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 

520 

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 

527 

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 

534 

535 if not ok: 

536 self.callback_parse_end() 

537 return None 

538 

539 # Finally, apply user defined checks 

540 if not self.perform_checks(): 

541 self.callback_parse_end() 

542 return None 

543 

544 if self.lint_mode and ok: 

545 linter.verify_imports() 

546 

547 self.callback_parse_end() 

548 return self.stab 

549 

550 

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.")) 

577 

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=[]) 

590 

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.")) 

641 

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.")) 

657 

658 ap.add_argument("items", 

659 nargs="*", 

660 metavar="DIR|FILE") 

661 options = ap.parse_args() 

662 

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) 

668 

669 if options.version: # pragma: no cover 

670 print(TRLC_VERSION) 

671 sys.exit(0) 

672 

673 if options.verify and not VCG_API_AVAILABLE: # pragma: no cover 

674 ap.error("The --verify option requires the optional dependency" 

675 " CVC5") 

676 

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) 

681 

682 if options.no_user_warnings: # pragma: no cover 

683 mh.suppress(Kind.USER_WARNING) 

684 

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) 

691 

692 if not options.include_bazel_dirs: # pragma: no cover 

693 sm.exclude_patterns.append(re.compile("^bazel-.*$")) 

694 

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) 

702 

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(".") 

720 

721 if not ok: 

722 mh.close() 

723 return 1 

724 

725 if sm.process() is None: 

726 ok = False 

727 

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]) 

738 

739 print(json.dumps(tmp, indent=2, sort_keys=True), file=mh.out) 

740 

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]) 

749 

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 

758 

759 summary = "Processed %s" % count(parsed_models, 

760 total_models, 

761 "model") 

762 

763 if not options.skip_trlc_files: # pragma: no cover 

764 summary += " and %s" % count(parsed_trlc, 

765 total_trlc, 

766 "requirement file") 

767 

768 summary += " and found" 

769 

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" 

779 

780 if mh.suppressed: # pragma: no cover 

781 summary += " with %u supressed messages" % mh.suppressed 

782 

783 print(summary, file=mh.out) 

784 

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]" 

793 

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) 

807 

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 

817 

818 

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 

828 

829 

830if __name__ == "__main__": 

831 sys.exit(main())