| # Copyright (C) 2019 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import json |
| |
| from resultsdbpy.flask_support.util import FlaskJSONEncoder |
| |
| |
| class Configuration(object): |
| """ |
| This class is designed to use a partial configuration to match more complete configurations. Generally, an instance |
| of this class will match another instance if all members match or are None. This means that, for example, a |
| Configuration object with a platform of 'Mac' and all other members set to None will match any Configuration object |
| with a platform of 'Mac', regardless of the values of the Configuration's other members. |
| """ |
| |
| VERSION_OFFSET_CONSTANT = 1000 |
| |
| REQUIRED_MEMBERS = ['platform', 'is_simulator', 'version', 'architecture'] |
| OPTIONAL_MEMBERS = ['version_name', 'model', 'style', 'flavor'] |
| FILTERING_MEMBERS = ['sdk'] |
| |
| @classmethod |
| def from_json(cls, data): |
| data = data if isinstance(data, dict) else json.loads(data) |
| return Configuration(**{key: data.get(key) for key in cls.REQUIRED_MEMBERS + cls.OPTIONAL_MEMBERS + ['sdk']}) |
| |
| def __init__(self, platform=None, version=None, sdk=None, version_name=None, is_simulator=None, architecture=None, model=None, style=None, flavor=None): |
| self.platform = None if platform is None else str(platform) |
| self.version = None if version is None else self.version_to_integer(version) |
| self.version_name = None if version_name is None else str(version_name) |
| self.is_simulator = None if is_simulator is None else bool(is_simulator) |
| self.architecture = None if architecture is None else str(architecture) |
| self.model = None if model is None else str(model) |
| self.style = None if style is None else str(style) |
| self.flavor = None if flavor is None else str(flavor) |
| |
| # Configurations which are identical, except for their sdk, should be considered identical |
| self.sdk = None |
| if sdk and sdk != '?': |
| self.sdk = str(sdk) |
| |
| @classmethod |
| def version_to_integer(cls, version): |
| if isinstance(version, int): |
| return version |
| elif isinstance(version, str): |
| version_list = version.split('.') |
| elif isinstance(version, list) or isinstance(version, tuple): |
| version_list = version |
| else: |
| raise TypeError(f'{type(version)} cannot be converted to a version integer') |
| if len(version_list) < 1 or len(version_list) > 3: |
| raise ValueError(f'{version} cannot be converted to a version integer') |
| |
| index = 0 |
| result = 0 |
| while index < 3: |
| result *= cls.VERSION_OFFSET_CONSTANT |
| if index < len(version_list): |
| result += int(version_list[index]) |
| index += 1 |
| |
| return result |
| |
| @classmethod |
| def integer_to_version(cls, version): |
| assert isinstance(version, int) |
| result = [0, 0, 0] |
| for index in range(3): |
| result[-(index + 1)] = version % cls.VERSION_OFFSET_CONSTANT |
| version //= cls.VERSION_OFFSET_CONSTANT |
| return f'{result[0]}.{result[1]}.{result[2]}' |
| |
| def is_complete(self): |
| return not any([getattr(self, member) is None for member in self.REQUIRED_MEMBERS]) |
| |
| def __eq__(self, other): |
| if not isinstance(other, type(self)): |
| return False |
| for member in self.REQUIRED_MEMBERS + self.OPTIONAL_MEMBERS: |
| if getattr(self, member) is None or getattr(other, member) is None: |
| continue |
| if getattr(self, member) == getattr(other, member): |
| continue |
| return False |
| return True |
| |
| def __ne__(self, other): |
| return not (self == other) |
| |
| def __lt__(self, other): |
| if not isinstance(other, type(self)): |
| raise TypeError(f'Cannot compare {type(other)} and {type(self)}') |
| for member in ['version', 'platform', 'version_name', 'is_simulator', 'model', 'architecture', 'style', 'flavor']: |
| mine = getattr(self, member) |
| theirs = getattr(other, member) |
| if mine is None and theirs is None: |
| continue |
| if mine is None and theirs is not None: |
| return True |
| if mine is not None and theirs is None: |
| return False |
| if mine < theirs: |
| return True |
| if mine > theirs: |
| return False |
| return False |
| |
| def __le__(self, other): |
| return self.__lt__(other) or self.__eq__(other) |
| |
| def __gt__(self, other): |
| if not isinstance(other, type(self)): |
| raise TypeError(f'Cannot compare {type(other)} and {type(self)}') |
| for member in ['version', 'platform', 'version_name', 'is_simulator', 'model', 'architecture', 'style', 'flavor']: |
| mine = getattr(self, member) |
| theirs = getattr(other, member) |
| if mine is None and theirs is None: |
| continue |
| if mine is None and theirs is not None: |
| return False |
| if mine is not None and theirs is None: |
| return True |
| if mine < theirs: |
| return False |
| if mine > theirs: |
| return True |
| return False |
| |
| def __ge__(self, other): |
| return self.__gt__(other) or self.__eq__(other) |
| |
| def __hash__(self): |
| result = 0 |
| for member in self.REQUIRED_MEMBERS + self.OPTIONAL_MEMBERS + self.FILTERING_MEMBERS: |
| if member == 'version_name' and getattr(self, 'version'): |
| continue |
| result ^= hash(getattr(self, member)) |
| return result |
| |
| def __repr__(self): |
| result = '' |
| if self.platform is not None: |
| result += ' ' + self.platform |
| if self.version is not None: |
| result += ' ' + self.integer_to_version(self.version) |
| elif self.version_name is not None: |
| result += ' ' + self.version_name |
| if self.sdk: |
| result += f' ({self.sdk})' |
| if self.is_simulator is not None and self.is_simulator: |
| result += ' Simulator' |
| if self.style is not None: |
| result += ' ' + self.style |
| if self.flavor is not None: |
| result += ' ' + self.flavor |
| if self.model is not None: |
| result += ' running on ' + self.model |
| if self.architecture is not None: |
| result += ' using ' + self.architecture |
| |
| return result[1:] |
| |
| def to_json(self, pretty_print=False): |
| return json.dumps(self, cls=self.Encoder, sort_keys=pretty_print, indent=4 if pretty_print else None) |
| |
| def to_query(self): |
| query = '' |
| for member in self.REQUIRED_MEMBERS + self.OPTIONAL_MEMBERS + self.FILTERING_MEMBERS: |
| value = getattr(self, member) |
| if value is None: |
| continue |
| |
| if member == 'version': |
| query += f'{member}={self.integer_to_version(value)}&' |
| elif member == 'is_simulator': |
| query += f"{member}={'True' if value else 'False'}&" |
| else: |
| query += f'{member}={value}&' |
| if query: |
| return query[:-1] |
| return '' |
| |
| class Encoder(FlaskJSONEncoder): |
| |
| def default(self, obj): |
| if isinstance(obj, Configuration): |
| result = {} |
| for key, value in obj.__dict__.items(): |
| if value is None: |
| continue |
| result[key] = value |
| return result |
| return super(Configuration.Encoder, self).default(obj) |