• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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