1# Copyright 2019 The TensorFlow Authors. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# 15# ============================================================================== 16"""Checks if a set of configuration(s) is version and dependency compatible.""" 17 18import configparser 19import re 20import sys 21 22 23from tensorflow.python.platform import tf_logging as logging 24from tensorflow.python.util import tf_inspect 25 26PATH_TO_DIR = "tensorflow/tools/tensorflow_builder/compat_checker" 27 28 29def _compare_versions(v1, v2): 30 """Compare two versions and return information on which is smaller vs. larger. 31 32 Args: 33 v1: String that is a version to be compared against `v2`. 34 v2: String that is a version to be compared against `v1`. 35 36 Returns: 37 Dict that stores larger version with key `larger` and smaller version with 38 key `smaller`. 39 e.g. {`larger`: `1.5.0`, `smaller`: `1.2.0`} 40 41 Raises: 42 RuntimeError: If asked to compare `inf` to `inf`. 43 """ 44 # Throw error is asked to compare `inf` to `inf`. 45 if v1 == "inf" and v2 == "inf": 46 raise RuntimeError("Cannot compare `inf` to `inf`.") 47 48 rtn_dict = {"smaller": None, "larger": None} 49 v1_list = v1.split(".") 50 v2_list = v2.split(".") 51 # Take care of cases with infinity (arg=`inf`). 52 if v1_list[0] == "inf": 53 v1_list[0] = str(int(v2_list[0]) + 1) 54 if v2_list[0] == "inf": 55 v2_list[0] = str(int(v1_list[0]) + 1) 56 57 # Determine which of the two lists are longer vs. shorter. 58 v_long = v1_list if len(v1_list) >= len(v2_list) else v2_list 59 v_short = v1_list if len(v1_list) < len(v2_list) else v2_list 60 61 larger, smaller = None, None 62 for i, ver in enumerate(v_short, start=0): 63 if int(ver) > int(v_long[i]): 64 larger = _list_to_string(v_short, ".") 65 smaller = _list_to_string(v_long, ".") 66 elif int(ver) < int(v_long[i]): 67 larger = _list_to_string(v_long, ".") 68 smaller = _list_to_string(v_short, ".") 69 else: 70 if i == len(v_short) - 1: 71 if v_long[i + 1:] == ["0"]*(len(v_long) - 1 - i): 72 larger = "equal" 73 smaller = "equal" 74 else: 75 larger = _list_to_string(v_long, ".") 76 smaller = _list_to_string(v_short, ".") 77 else: 78 # Go to next round. 79 pass 80 81 if larger: 82 break 83 84 rtn_dict["smaller"] = smaller 85 rtn_dict["larger"] = larger 86 87 return rtn_dict 88 89 90def _list_to_string(l, s): 91 """Concatenates list items into a single string separated by `s`. 92 93 Args: 94 l: List with items to be concatenated into a single string. 95 s: String or char that will be concatenated in between each item. 96 97 Returns: 98 String that has all items in list `l` concatenated with `s` separator. 99 """ 100 101 return s.join(l) 102 103 104def _get_func_name(): 105 """Get the name of current function. 106 107 Returns: 108 String that is the name of current function. 109 """ 110 return tf_inspect.stack()[1][3] 111 112 113class ConfigCompatChecker: 114 """Class that checks configuration versions and dependency compatibilities. 115 116 `ConfigCompatChecker` checks a given set of configurations and their versions 117 against supported versions and dependency rules defined in `.ini` config file. 118 For project `TensorFlow Builder`, it functions as a sub-module for the builder 119 service that validates requested build configurations from a client prior to 120 initiating a TensorFlow build. 121 """ 122 123 class _Reqs(object): 124 """Class that stores specifications related to a single requirement. 125 126 `_Reqs` represents a single version or dependency requirement specified in 127 the `.ini` config file. It is meant ot be used inside `ConfigCompatChecker` 128 to help organize and identify version and dependency compatibility for a 129 given configuration (e.g. gcc version) required by the client. 130 """ 131 132 def __init__(self, req, config, section): 133 """Initializes a version or dependency requirement object. 134 135 Args: 136 req: List that contains individual supported versions or a single string 137 that contains `range` definition. 138 e.g. [`range(1.0, 2.0) include(3.0) exclude(1.5)`] 139 e.g. [`1.0`, `3.0`, `7.1`] 140 config: String that is the configuration name. 141 e.g. `platform` 142 section: String that is the section name from the `.ini` config file 143 under which the requirement is defined. 144 e.g. `Required`, `Optional`, `Unsupported`, `Dependency` 145 """ 146 # Req class variables. 147 self.req = req 148 self.exclude = None 149 self.include = None 150 self.range = [None, None] # for [min, max] 151 self.config = config 152 self._req_type = "" # e.g. `range` or `no_range` 153 self._section = section 154 self._initialized = None 155 self._error_message = [] 156 157 # Parse and store requirement specifications. 158 self.parse_single_req() 159 160 @property 161 def get_status(self): 162 """Get status of `_Reqs` initialization. 163 164 Returns: 165 Tuple 166 (Boolean indicating initialization status, 167 List of error messages, if any) 168 169 """ 170 171 return self._initialized, self._error_message 172 173 def __str__(self): 174 """Prints a requirement and its components. 175 176 Returns: 177 String that has concatenated information about a requirement. 178 """ 179 info = { 180 "section": self._section, 181 "config": self.config, 182 "req_type": self._req_type, 183 "req": str(self.req), 184 "range": str(self.range), 185 "exclude": str(self.exclude), 186 "include": str(self.include), 187 "init": str(self._initialized) 188 } 189 req_str = "\n >>> _Reqs Instance <<<\n" 190 req_str += "Section: {section}\n" 191 req_str += "Configuration name: {config}\n" 192 req_str += "Requirement type: {req_type}\n" 193 req_str += "Requirement: {req}\n" 194 req_str += "Range: {range}\n" 195 req_str += "Exclude: {exclude}\n" 196 req_str += "Include: {include}\n" 197 req_str += "Initialized: {init}\n\n" 198 199 return req_str.format(**info) 200 201 def parse_single_req(self): 202 """Parses a requirement and stores information. 203 204 `self.req` _initialized in `__init__` is called for retrieving the 205 requirement. 206 207 A requirement can come in two forms: 208 [1] String that includes `range` indicating range syntax for defining 209 a requirement. 210 e.g. `range(1.0, 2.0) include(3.0) exclude(1.5)` 211 [2] List that includes individual supported versions or items. 212 e.g. [`1.0`, `3.0`, `7.1`] 213 214 For a list type requirement, it directly stores the list to 215 `self.include`. 216 217 Call `get_status` for checking the status of the parsing. This function 218 sets `self._initialized` to `False` and immediately returns with an error 219 message upon encountering a failure. It sets `self._initialized` to `True` 220 and returns without an error message upon success. 221 """ 222 # Regex expression for filtering requirement line. Please refer 223 # to docstring above for more information. 224 expr = r"(range\()?([\d\.\,\s]+)(\))?( )?(include\()?" 225 expr += r"([\d\.\,\s]+)?(\))?( )?(exclude\()?([\d\.\,\s]+)?(\))?" 226 227 # Check that arg `req` is not empty. 228 if not self.req: 229 err_msg = "[Error] Requirement is missing. " 230 err_msg += "(section = %s, " % str(self._section) 231 err_msg += "config = %s, req = %s)" % (str(self.config), str(self.req)) 232 logging.error(err_msg) 233 self._initialized = False 234 self._error_message.append(err_msg) 235 236 return 237 238 # For requirement given in format with `range`. For example: 239 # python = [range(3.3, 3.7) include(2.7)] as opposed to 240 # python = [2.7, 3.3, 3.4, 3.5, 3.6, 3.7] 241 if "range" in self.req[0]: 242 self._req_type = "range" 243 match = re.match(expr, self.req[0]) 244 if not match: 245 err_msg = "[Error] Encountered issue when parsing the requirement." 246 err_msg += " (req = %s, match = %s)" % (str(self.req), str(match)) 247 logging.error(err_msg) 248 self._initialized = False 249 self._error_message.append(err_msg) 250 251 return 252 else: 253 match_grp = match.groups() 254 match_size = len(match_grp) 255 for i, m in enumerate(match_grp[0:match_size-1], start=0): 256 # Get next index. For example: 257 # | idx | next_idx | 258 # +------------+------------+ 259 # | `range(` | `1.1, 1.5` | 260 # | `exclude(` | `1.1, 1.5` | 261 # | `include(` | `1.1, 1.5` | 262 next_match = match_grp[i + 1] 263 264 if m not in ["", None, " ", ")"]: 265 if "range" in m: 266 # Check that the range definition contains only one comma. 267 # If more than one comma, then there is format error with the 268 # requirement config file. 269 comma_count = next_match.count(",") 270 if comma_count > 1 or comma_count == 0: 271 err_msg = "[Error] Found zero or more than one comma in range" 272 err_msg += " definition. (req = %s, " % str(self.req) 273 err_msg += "match = %s)" % str(next_match) 274 logging.error(err_msg) 275 self._initialized = False 276 self._error_message.append(err_msg) 277 278 return 279 280 # Remove empty space in range and separate min, max by 281 # comma. (e.g. `1.0, 2.0` => `1.0,2.0` => [`1.0`, `2.0`]) 282 min_max = next_match.replace(" ", "").split(",") 283 284 # Explicitly define min and max values. 285 # If min_max = ['', ''], then `range(, )` was provided as 286 # req, which is equivalent to `include all versions`. 287 if not min_max[0]: 288 min_max[0] = "0" 289 290 if not min_max[1]: 291 min_max[1] = "inf" 292 293 self.range = min_max 294 if "exclude" in m: 295 self.exclude = next_match.replace(" ", "").split(",") 296 297 if "include" in m: 298 self.include = next_match.replace(" ", "").split(",") 299 300 self._initialized = True 301 302 # For requirement given in format without a `range`. For example: 303 # python = [2.7, 3.3, 3.4, 3.5, 3.6, 3.7] as opposed to 304 # python = [range(3.3, 3.7) include(2.7)] 305 else: 306 self._req_type = "no_range" 307 # Requirement (self.req) should be a list. 308 if not isinstance(self.req, list): 309 err_msg = "[Error] Requirement is not a list." 310 err_msg += "(req = %s, " % str(self.req) 311 err_msg += "type(req) = %s)" % str(type(self.req)) 312 logging.error(err_msg) 313 self._initialized = False 314 self._error_message.append(err_msg) 315 else: 316 self.include = self.req 317 self._initialized = True 318 319 return 320 321 def __init__(self, usr_config, req_file): 322 """Initializes a configuration compatibility checker. 323 324 Args: 325 usr_config: Dict of all configuration(s) whose version compatibilities are 326 to be checked against the rules defined in the `.ini` config 327 file. 328 req_file: String that is the full name of the `.ini` config file. 329 e.g. `config.ini` 330 """ 331 # ConfigCompatChecker class variables. 332 self.usr_config = usr_config 333 self.req_file = req_file 334 self.warning_msg = [] 335 self.error_msg = [] 336 # Get and store requirements. 337 reqs_all = self.get_all_reqs() 338 self.required = reqs_all["required"] 339 self.optional = reqs_all["optional"] 340 self.unsupported = reqs_all["unsupported"] 341 self.dependency = reqs_all["dependency"] 342 343 self.successes = [] 344 self.failures = [] 345 346 def get_all_reqs(self): 347 """Parses all compatibility specifications listed in the `.ini` config file. 348 349 Reads and parses each and all compatibility specifications from the `.ini` 350 config file by sections. It then populates appropriate dicts that represent 351 each section (e.g. `self.required`) and returns a tuple of the populated 352 dicts. 353 354 Returns: 355 Dict of dict 356 { `required`: Dict of `Required` configs and supported versions, 357 `optional`: Dict of `Optional` configs and supported versions, 358 `unsupported`: Dict of `Unsupported` configs and supported versions, 359 `dependency`: Dict of `Dependency` configs and supported versions } 360 """ 361 # First check if file exists. Exit on failure. 362 try: 363 open(self.req_file, "rb") 364 except IOError: 365 msg = "[Error] Cannot read file '%s'." % self.req_file 366 logging.error(msg) 367 sys.exit(1) 368 369 # Store status of parsing requirements. For local usage only. 370 curr_status = True 371 372 # Initialize config parser for parsing version requirements file. 373 parser = configparser.ConfigParser() 374 parser.read(self.req_file) 375 376 if not parser.sections(): 377 err_msg = "[Error] Empty config file. " 378 err_msg += "(file = %s, " % str(self.req_file) 379 err_msg += "parser sectons = %s)" % str(parser.sections()) 380 self.error_msg.append(err_msg) 381 logging.error(err_msg) 382 curr_status = False 383 384 # Each dependency dict will have the following format. 385 # _dict = { 386 # `<config_name>` : [_Reqs()], 387 # `<config_name>` : [_Reqs()] 388 # } 389 required_dict = {} 390 optional_dict = {} 391 unsupported_dict = {} 392 dependency_dict = {} 393 394 # Parse every config under each section defined in config file 395 # and populate requirement dict(s). 396 for section in parser.sections(): 397 all_configs = parser.options(section) 398 for config in all_configs: 399 spec = parser.get(section, config) 400 # Separately manage each section: 401 # `Required`, 402 # `Optional`, 403 # `Unsupported`, 404 # `Dependency` 405 # One of the sections is required. 406 if section == "Dependency": 407 dependency_dict[config] = [] 408 spec_split = spec.split(",\n") 409 # First dependency item may only or not have `[` depending 410 # on the indentation style in the config (.ini) file. 411 # If it has `[`, then either skip or remove from string. 412 if spec_split[0] == "[": 413 spec_split = spec_split[1:] 414 elif "[" in spec_split[0]: 415 spec_split[0] = spec_split[0].replace("[", "") 416 else: 417 warn_msg = "[Warning] Config file format error: Missing `[`." 418 warn_msg += "(section = %s, " % str(section) 419 warn_msg += "config = %s)" % str(config) 420 logging.warning(warn_msg) 421 self.warning_msg.append(warn_msg) 422 423 # Last dependency item may only or not have `]` depending 424 # on the indentation style in the config (.ini) file. 425 # If it has `[`, then either skip or remove from string. 426 if spec_split[-1] == "]": 427 spec_split = spec_split[:-1] 428 elif "]" in spec_split[-1]: 429 spec_split[-1] = spec_split[-1].replace("]", "") 430 else: 431 warn_msg = "[Warning] Config file format error: Missing `]`." 432 warn_msg += "(section = %s, " % str(section) 433 warn_msg += "config = %s)" % str(config) 434 logging.warning(warn_msg) 435 self.warning_msg.append(warn_msg) 436 437 # Parse `spec_split` which is a list of all dependency rules 438 # retrieved from the config file. 439 # Create a _Reqs() instance for each rule and store it under 440 # appropriate class dict (e.g. dependency_dict) with a proper 441 # key. 442 # 443 # For dependency definition, it creates one _Reqs() instance each 444 # for requirement and dependency. For example, it would create 445 # a list in the following indexing sequence: 446 # 447 # [`config', <`config` _Reqs()>, `dep', <`dep` _Reqs()>] 448 # 449 # For example: 450 # [`python`, _Reqs(), `tensorflow`, _Reqs()] for 451 # `python 3.7 requires tensorflow 1.13` 452 for rule in spec_split: 453 # Filter out only the necessary information from `rule` string. 454 spec_dict = self.filter_dependency(rule) 455 # Create _Reqs() instance for each rule. 456 cfg_name = spec_dict["cfg"] # config name 457 dep_name = spec_dict["cfgd"] # dependency name 458 cfg_req = self._Reqs( 459 self.convert_to_list(spec_dict["cfg_spec"], " "), 460 config=cfg_name, 461 section=section 462 ) 463 dep_req = self._Reqs( 464 self.convert_to_list(spec_dict["cfgd_spec"], " "), 465 config=dep_name, 466 section=section 467 ) 468 # Check status of _Reqs() initialization. If wrong formats are 469 # detected from the config file, it would return `False` for 470 # initialization status. 471 # `<_Reqs>.get_status` returns [_initialized, _error_message] 472 cfg_req_status = cfg_req.get_status 473 dep_req_status = dep_req.get_status 474 if not cfg_req_status[0] or not dep_req_status[0]: 475 # `<_Reqs>.get_status()[1]` returns empty upon successful init. 476 msg = "[Error] Failed to create _Reqs() instance for a " 477 msg += "dependency item. (config = %s, " % str(cfg_name) 478 msg += "dep = %s)" % str(dep_name) 479 logging.error(msg) 480 self.error_msg.append(cfg_req_status[1]) 481 self.error_msg.append(dep_req_status[1]) 482 curr_status = False 483 break 484 else: 485 dependency_dict[config].append( 486 [cfg_name, cfg_req, dep_name, dep_req]) 487 488 # Break out of `if section == 'Dependency'` block. 489 if not curr_status: 490 break 491 492 else: 493 if section == "Required": 494 add_to = required_dict 495 elif section == "Optional": 496 add_to = optional_dict 497 elif section == "Unsupported": 498 add_to = unsupported_dict 499 else: 500 msg = "[Error] Section name `%s` is not accepted." % str(section) 501 msg += "Accepted section names are `Required`, `Optional`, " 502 msg += "`Unsupported`, and `Dependency`." 503 logging.error(msg) 504 self.error_msg.append(msg) 505 curr_status = False 506 break 507 508 # Need to make sure `req` argument for _Reqs() instance is always 509 # a list. If not, convert to list. 510 req_list = self.convert_to_list(self.filter_line(spec), " ") 511 add_to[config] = self._Reqs(req_list, config=config, section=section) 512 # Break out of `for config in all_configs` loop. 513 if not curr_status: 514 break 515 516 # Break out of `for section in parser.sections()` loop. 517 if not curr_status: 518 break 519 520 return_dict = { 521 "required": required_dict, 522 "optional": optional_dict, 523 "unsupported": unsupported_dict, 524 "dependency": dependency_dict 525 } 526 527 return return_dict 528 529 def filter_dependency(self, line): 530 """Filters dependency compatibility rules defined in the `.ini` config file. 531 532 Dependency specifications are defined as the following: 533 `<config> <config_version> requires <dependency> <dependency_version>` 534 e.g. 535 `python 3.7 requires tensorflow 1.13` 536 `tensorflow range(1.0.0, 1.13.1) requires gcc range(4.8, )` 537 538 Args: 539 line: String that is a dependency specification defined under `Dependency` 540 section in the `.ini` config file. 541 542 Returns: 543 Dict with configuration and its dependency information. 544 e.g. {`cfg`: `python`, # configuration name 545 `cfg_spec`: `3.7`, # configuration version 546 `cfgd`: `tensorflow`, # dependency name 547 `cfgd_spec`: `4.8`} # dependency version 548 """ 549 line = line.strip("\n") 550 expr = r"(?P<cfg>[\S]+) (?P<cfg_spec>range\([\d\.\,\s]+\)( )?" 551 expr += r"(include\([\d\.\,\s]+\))?( )?(exclude\([\d\.\,\s]+\))?( )?" 552 expr += r"|[\d\,\.\s]+) requires (?P<cfgd>[\S]+) (?P<cfgd_spec>range" 553 expr += r"\([\d\.\,\s]+\)( )?(include\([\d\.\,\s]+\))?( )?" 554 expr += r"(exclude\([\d\.\,\s]+\))?( )?|[\d\,\.\s]+)" 555 r = re.match(expr, line.strip("\n")) 556 557 return r.groupdict() 558 559 def convert_to_list(self, item, separator): 560 """Converts a string into a list with a separator. 561 562 Args: 563 item: String that needs to be separated into a list by a given separator. 564 List item is also accepted but will take no effect. 565 separator: String with which the `item` will be splited. 566 567 Returns: 568 List that is a splited version of a given input string. 569 e.g. Input: `1.0, 2.0, 3.0` with `, ` separator 570 Output: [1.0, 2.0, 3.0] 571 """ 572 out = None 573 if not isinstance(item, list): 574 if "range" in item: 575 # If arg `item` is a single string, then create a list with just 576 # the item. 577 out = [item] 578 else: 579 # arg `item` can come in as the following: 580 # `1.0, 1.1, 1.2, 1.4` 581 # if requirements were defined without the `range()` format. 582 # In such a case, create a list separated by `separator` which is 583 # an empty string (' ') in this case. 584 out = item.split(separator) 585 for i in range(len(out)): 586 out[i] = out[i].replace(",", "") 587 588 # arg `item` is a list already. 589 else: 590 out = [item] 591 592 return out 593 594 def filter_line(self, line): 595 """Removes `[` or `]` from the input line. 596 597 Args: 598 line: String that is a compatibility specification line from the `.ini` 599 config file. 600 601 Returns: 602 String that is a compatibility specification line without `[` and `]`. 603 """ 604 filtered = [] 605 warn_msg = [] 606 607 splited = line.split("\n") 608 609 # If arg `line` is empty, then requirement might be missing. Add 610 # to warning as this issue will be caught in _Reqs() initialization. 611 if not line and len(splited) < 1: 612 warn_msg = "[Warning] Empty line detected while filtering lines." 613 logging.warning(warn_msg) 614 self.warning_msg.append(warn_msg) 615 616 # In general, first line in requirement definition will include `[` 617 # in the config file (.ini). Remove it. 618 if splited[0] == "[": 619 filtered = splited[1:] 620 elif "[" in splited[0]: 621 splited = splited[0].replace("[", "") 622 filtered = splited 623 # If `[` is missing, then it could be a formatting issue with 624 # config file (.ini.). Add to warning. 625 else: 626 warn_msg = "[Warning] Format error. `[` could be missing in " 627 warn_msg += "the config (.ini) file. (line = %s)" % str(line) 628 logging.warning(warn_msg) 629 self.warning_msg.append(warn_msg) 630 631 # In general, last line in requirement definition will include `]` 632 # in the config file (.ini). Remove it. 633 if filtered[-1] == "]": 634 filtered = filtered[:-1] 635 elif "]" in filtered[-1]: 636 filtered[-1] = filtered[-1].replace("]", "") 637 # If `]` is missing, then it could be a formatting issue with 638 # config file (.ini.). Add to warning. 639 else: 640 warn_msg = "[Warning] Format error. `]` could be missing in " 641 warn_msg += "the config (.ini) file. (line = %s)" % str(line) 642 logging.warning(warn_msg) 643 self.warning_msg.append(warn_msg) 644 645 return filtered 646 647 def in_range(self, ver, req): 648 """Checks if a version satisfies a version and/or compatibility requirement. 649 650 Args: 651 ver: List whose first item is a config version that needs to be checked 652 for support status and version compatibility. 653 e.g. ver = [`1.0`] 654 req: `_Reqs` class instance that represents a configuration version and 655 compatibility specifications. 656 657 Returns: 658 Boolean output of checking if version `ver` meets the requirement 659 stored in `req` (or a `_Reqs` requirements class instance). 660 """ 661 # If `req.exclude` is not empty and `ver` is in `req.exclude`, 662 # no need to proceed to next set of checks as it is explicitly 663 # NOT supported. 664 if req.exclude is not None: 665 for v in ver: 666 if v in req.exclude: 667 return False 668 669 # If `req.include` is not empty and `ver` is in `req.include`, 670 # no need to proceed to next set of checks as it is supported and 671 # NOT unsupported (`req.exclude`). 672 include_checked = False 673 if req.include is not None: 674 for v in ver: 675 if v in req.include: 676 return True 677 678 include_checked = True 679 680 # If `req.range` is not empty, then `ver` is defined with a `range` 681 # syntax. Check whether `ver` falls under the defined supported 682 # range. 683 if req.range != [None, None]: 684 min_v = req.range[0] # minimum supported version 685 max_v = req.range[1] # maximum supported version 686 ver = ver[0] # version to compare 687 lg = _compare_versions(min_v, ver)["larger"] # `ver` should be larger 688 sm = _compare_versions(ver, max_v)["smaller"] # `ver` should be smaller 689 if lg in [ver, "equal"] and sm in [ver, "equal", "inf"]: 690 return True 691 else: 692 err_msg = "[Error] Version is outside of supported range. " 693 err_msg += "(config = %s, " % str(req.config) 694 err_msg += "version = %s, " % str(ver) 695 err_msg += "supported range = %s)" % str(req.range) 696 logging.warning(err_msg) 697 self.warning_msg.append(err_msg) 698 return False 699 700 else: 701 err_msg = "" 702 if include_checked: 703 # user config is not supported as per exclude, include, range 704 # specification. 705 err_msg = "[Error] Version is outside of supported range. " 706 else: 707 # user config is not defined in exclude, include or range. config file 708 # error. 709 err_msg = "[Error] Missing specification. " 710 711 err_msg += "(config = %s, " % str(req.config) 712 err_msg += "version = %s, " % str(ver) 713 err_msg += "supported range = %s)" % str(req.range) 714 logging.warning(err_msg) 715 self.warning_msg.append(err_msg) 716 return False 717 718 def _print(self, *args): 719 """Prints compatibility check status and failure or warning messages. 720 721 Prints to console without using `logging`. 722 723 Args: 724 *args: String(s) that is one of: 725 [`failures`, # all failures 726 `successes`, # all successes 727 `failure_msgs`, # failure message(s) recorded upon failure(s) 728 `warning_msgs`] # warning message(s) recorded upon warning(s) 729 Raises: 730 Exception: If *args not in: 731 [`failures`, `successes`, `failure_msgs`, `warning_msg`] 732 """ 733 734 def _format(name, arr): 735 """Prints compatibility check results with a format. 736 737 Args: 738 name: String that is the title representing list `arr`. 739 arr: List of items to be printed in a certain format. 740 """ 741 title = "### All Compatibility %s ###" % str(name) 742 tlen = len(title) 743 print("-"*tlen) 744 print(title) 745 print("-"*tlen) 746 print(" Total # of %s: %s\n" % (str(name), str(len(arr)))) 747 if arr: 748 for item in arr: 749 detail = "" 750 if isinstance(item[1], list): 751 for itm in item[1]: 752 detail += str(itm) + ", " 753 detail = detail[:-2] 754 else: 755 detail = str(item[1]) 756 print(" %s ('%s')\n" % (str(item[0]), detail)) 757 else: 758 print(" No %s" % name) 759 print("\n") 760 761 for p_item in args: 762 if p_item == "failures": 763 _format("Failures", self.failures) 764 elif p_item == "successes": 765 _format("Successes", self.successes) 766 elif p_item == "failure_msgs": 767 _format("Failure Messages", self.error_msg) 768 elif p_item == "warning_msgs": 769 _format("Warning Messages", self.warning_msg) 770 else: 771 raise Exception( 772 "[Error] Wrong input provided for %s." % _get_func_name()) 773 774 def check_compatibility(self): 775 """Checks version and dependency compatibility for a given configuration. 776 777 `check_compatibility` immediately returns with `False` (or failure status) 778 if any child process or checks fail. For error and warning messages, either 779 print `self.(error_msg|warning_msg)` or call `_print` function. 780 781 Returns: 782 Boolean that is a status of the compatibility check result. 783 """ 784 # Check if all `Required` configs are found in user configs. 785 usr_keys = list(self.usr_config.keys()) 786 787 for k in self.usr_config.keys(): 788 if k not in usr_keys: 789 err_msg = "[Error] Required config not found in user config." 790 err_msg += "(required = %s, " % str(k) 791 err_msg += "user configs = %s)" % str(usr_keys) 792 logging.error(err_msg) 793 self.error_msg.append(err_msg) 794 self.failures.append([k, err_msg]) 795 return False 796 797 # Parse each user config and validate its compatibility. 798 overall_status = True 799 for config_name, spec in self.usr_config.items(): 800 temp_status = True 801 # Check under which section the user config is defined. 802 in_required = config_name in list(self.required.keys()) 803 in_optional = config_name in list(self.optional.keys()) 804 in_unsupported = config_name in list(self.unsupported.keys()) 805 in_dependency = config_name in list(self.dependency.keys()) 806 807 # Add to warning if user config is not specified in the config file. 808 if not (in_required or in_optional or in_unsupported or in_dependency): 809 warn_msg = "[Error] User config not defined in config file." 810 warn_msg += "(user config = %s)" % str(config_name) 811 logging.warning(warn_msg) 812 self.warning_msg.append(warn_msg) 813 self.failures.append([config_name, warn_msg]) 814 temp_status = False 815 else: 816 if in_unsupported: 817 if self.in_range(spec, self.unsupported[config_name]): 818 err_msg = "[Error] User config is unsupported. It is " 819 err_msg += "defined under 'Unsupported' section in the config file." 820 err_msg += " (config = %s, spec = %s)" % (config_name, str(spec)) 821 logging.error(err_msg) 822 self.error_msg.append(err_msg) 823 self.failures.append([config_name, err_msg]) 824 temp_status = False 825 826 if in_required: 827 if not self.in_range(spec, self.required[config_name]): 828 err_msg = "[Error] User config cannot be supported. It is not in " 829 err_msg += "the supported range as defined in the 'Required' " 830 err_msg += "section. (config = %s, " % config_name 831 err_msg += "spec = %s)" % str(spec) 832 logging.error(err_msg) 833 self.error_msg.append(err_msg) 834 self.failures.append([config_name, err_msg]) 835 temp_status = False 836 837 if in_optional: 838 if not self.in_range(spec, self.optional[config_name]): 839 err_msg = "[Error] User config cannot be supported. It is not in " 840 err_msg += "the supported range as defined in the 'Optional' " 841 err_msg += "section. (config = %s, " % config_name 842 err_msg += "spec = %s)" % str(spec) 843 logging.error(err_msg) 844 self.error_msg.append(err_msg) 845 self.failures.append([config_name, err_msg]) 846 temp_status = False 847 848 # If user config and version has a dependency, check both user 849 # config + version and dependency config + version are supported. 850 if in_dependency: 851 # Get dependency information. The information gets retrieved in the 852 # following format: 853 # [`config`, `config _Reqs()`, `dependency`, `dependency _Reqs()`] 854 dep_list = self.dependency[config_name] 855 if dep_list: 856 for rule in dep_list: 857 cfg = rule[0] # config name 858 cfg_req = rule[1] # _Reqs() instance for config requirement 859 dep = rule[2] # dependency name 860 dep_req = rule[3] # _Reqs() instance for dependency requirement 861 862 # Check if user config has a dependency in the following sequence: 863 # [1] Check user config and the config that has dependency 864 # are the same. (This is defined as `cfg_status`.) 865 # [2] Check if dependency is supported. 866 try: 867 cfg_name = self.usr_config[cfg] 868 dep_name = self.usr_config[dep] 869 870 cfg_status = self.in_range(cfg_name, cfg_req) 871 dep_status = self.in_range(dep_name, dep_req) 872 # If both status's are `True`, then user config meets dependency 873 # spec. 874 if cfg_status: 875 if not dep_status: 876 # throw error 877 err_msg = "[Error] User config has a dependency that cannot" 878 err_msg += " be supported. " 879 err_msg += "'%s' has a dependency on " % str(config_name) 880 err_msg += "'%s'." % str(dep) 881 logging.error(err_msg) 882 self.error_msg.append(err_msg) 883 self.failures.append([config_name, err_msg]) 884 temp_status = False 885 886 except KeyError: 887 err_msg = "[Error] Dependency is missing from `Required`. " 888 err_msg += "(config = %s, ""dep = %s)" % (cfg, dep) 889 logging.error(err_msg) 890 self.error_msg.append(err_msg) 891 self.failures.append([config_name, err_msg]) 892 temp_status = False 893 894 # At this point, all requirement related to the user config has been 895 # checked and passed. Append to `successes` list. 896 if temp_status: 897 self.successes.append([config_name, spec]) 898 else: 899 overall_status = False 900 901 return overall_status 902