blob: e6ae809d421bd86423f860b5d8f0dc3a3e7c58d3 [file] [log] [blame]
Greg Edelston64fdc2e2020-11-19 15:04:18 -07001#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Determine which platform/model configs match a set of field conditions.
7
8Each condition passed in as a command-line arg must be formatted as
9${FIELD}${OPERATOR}${VALUE}, where:
10 ${FIELD} is the name of the field being matched
Greg Edelstonb3efad22020-11-24 11:09:39 -070011 ${OPERATOR} can be any of the following:
12 = | field equals
13 != | field does not equal
14 : | array contains
15 !: | array does not contain
Greg Edelston6f4f2602020-11-25 16:36:11 -070016 < | number is less than
17 <= | number is less than or equal to
18 > | number is greater than
19 >= | number is greater than or equal to
Greg Edelston64fdc2e2020-11-19 15:04:18 -070020 ${VALUE} is the field-value being compared against
21
Greg Edelstonb3efad22020-11-24 11:09:39 -070022Note that Bash tends to misinterpret '!', so users are advised to wrap
23conditions in quotes, as in the following example:
24 ./query_by_field.py 'ec_capability!:x86'
Greg Edelston64fdc2e2020-11-19 15:04:18 -070025"""
26
27import argparse
28import re
29import sys
30
31import platform_json
32
33
Greg Edelston6f4f2602020-11-25 16:36:11 -070034# Regex to match an entire condition: field name, operator, expected value.
35RE_CONDITION = re.compile(r'^(\w+)(!?[=:]|[<>]=?)(\w+)$')
Greg Edelston64fdc2e2020-11-19 15:04:18 -070036
37
38class ConditionError(ValueError):
39 """Error class for when a condition argument is malformed."""
40 pass
41
42
43class UnrecognizedOperatorError(ValueError):
44 """Error class for when a query contains an unexpected operator."""
45
46
Greg Edelstonb3efad22020-11-24 11:09:39 -070047class NotIterableError(ValueError):
48 """Error class for querying for a member of a non-iterable field."""
49 pass
50
51
Greg Edelston6f4f2602020-11-25 16:36:11 -070052class NotNumericError(ValueError):
53 """Error class for when a non-number is used for numeric comparison."""
54
55
Greg Edelston64fdc2e2020-11-19 15:04:18 -070056class Condition(object):
57 """A condition for whether a config's field matches an expected rule."""
58
59 def __init__(self, string_condition):
60 """Parse string_condition to capture the field, operator, and value."""
61 match = RE_CONDITION.match(string_condition)
62 if match is None:
63 raise ConditionError(string_condition)
64 self.field, self.operator, self.value = match.groups()
65 # All string comparisons are case-insensitive, so normalize to lower.
66 self.value = self.value.lower()
67
68 def evaluate(self, config_dict):
69 """Determine whether a config dict satisfies this Condition."""
70 if self.field not in config_dict:
71 raise platform_json.FieldNotFoundError('%s (platform=%s)' % (
72 self.field, config_dict['platform']))
Greg Edelstonb3efad22020-11-24 11:09:39 -070073 actual_value = config_dict[self.field]
Greg Edelston64fdc2e2020-11-19 15:04:18 -070074 if self.operator == '=':
Greg Edelstonb3efad22020-11-24 11:09:39 -070075 return str(actual_value).lower() == self.value
Greg Edelston64fdc2e2020-11-19 15:04:18 -070076 elif self.operator == '!=':
Greg Edelstonb3efad22020-11-24 11:09:39 -070077 return str(actual_value).lower() != self.value
Greg Edelston6f4f2602020-11-25 16:36:11 -070078 elif self.operator in (':', '!:'):
79 return self._evaluate_array_membership(actual_value)
80 elif self.operator in ('<', '>', '<=', '>='):
81 return self._evaluate_numeric(actual_value)
Greg Edelston64fdc2e2020-11-19 15:04:18 -070082 else:
Greg Edelston6f4f2602020-11-25 16:36:11 -070083 raise UnrecognizedOperatorError('%s (expect =/!=/:/!:/</<=/>/>=)' %
Greg Edelston64fdc2e2020-11-19 15:04:18 -070084 self.operator)
85
Greg Edelston6f4f2602020-11-25 16:36:11 -070086 def _evaluate_array_membership(self, actual_value):
87 """Handler for operators that expect array-type config values."""
88 if not isinstance(actual_value, list):
89 msg = 'field %s, actual value %s' % (self.field, actual_value)
90 raise NotIterableError(msg)
91 array_contains_expected_element = False
92 for elem in actual_value:
93 if str(elem).lower() == self.value:
94 array_contains_expected_element = True
95 break
96 if self.operator == ':':
97 return array_contains_expected_element
98 elif self.operator == '!:':
99 return not array_contains_expected_element
100 raise UnrecognizedOperatorError(self.operator)
101
102 def _evaluate_numeric(self, actual_value):
103 """Handler for operators that expect numeric config values."""
104 try:
105 actual_value = float(actual_value)
106 except (ValueError, TypeError):
107 msg = 'field %s, actual value %s' % (self.field, actual_value)
108 raise NotNumericError(msg)
109 try:
110 expected_value = float(self.value)
111 except ValueError:
112 msg = 'field %s, expected value %s' % (self.field, self.value)
113 raise NotNumericError(msg)
114 return eval('%s %s %s' % (actual_value, self.operator, expected_value))
115
116
Greg Edelston64fdc2e2020-11-19 15:04:18 -0700117 def __repr__(self):
118 """Represent this condition as a string."""
119 return '{Condition:%s%s%s}' % (self.field, self.operator, self.value)
120
121
122def parse_args(argv):
123 """Parse command-line args."""
124 parser = argparse.ArgumentParser()
125 parser.add_argument('conditions', nargs='+',
126 help='Field-matching requirements, each of the form '
Greg Edelstoncc8d7632020-12-07 15:09:20 -0700127 '${FIELD}${OPERATOR}${VALUE}. Valid operators '
128 'are: = (match), != (not match), : (array '
129 'contains), !: (array does not contain), and '
130 'numeric comparions <, >, <=, >=. Note: String '
Greg Edelston64fdc2e2020-11-19 15:04:18 -0700131 'comparisons are always case-insensitive.')
132 parser.add_argument('-c', '--consolidated', default='CONSOLIDATED.json',
133 help='The filepath to CONSOLIDATED.json')
134 args = parser.parse_args(argv)
135 return args
136
137
138def all_satisfied(conditions, config):
139 """Determine whether the config satisfies all Conditions."""
140 return all([cond.evaluate(config) for cond in conditions])
141
142
143def main(argv):
144 """Determine which platforms/models satisfy the passed-in conditions."""
145 args = parse_args(argv)
146 conditions = [Condition(str_cond) for str_cond in args.conditions]
147 consolidated_json = platform_json.load_consolidated_json(args.consolidated)
148
149 outputs = []
150 for platform in consolidated_json:
151 # Determine whether the platform satisfies all conditions.
152 platform_config = platform_json.calculate_config(platform, None,
153 consolidated_json)
154 platform_satisfied = all_satisfied(conditions, platform_config)
155
156 # Check whether any of the platform's models are exceptions.
157 models = consolidated_json[platform].get('models', {})
158 exceptions = []
159 for model in models:
160 model_config = platform_json.calculate_config(platform, model,
161 consolidated_json)
162 if all_satisfied(conditions, model_config) != platform_satisfied:
163 exceptions.append(model)
164
165 # Print if the platform or any models satisfy the conditions.
166 if platform_satisfied and exceptions:
167 outputs.append('%s (except %s)' % (platform, ', '.join(exceptions)))
168 elif exceptions:
169 outputs.append('%s (only %s)' % (platform, ', '.join(exceptions)))
170 elif platform_satisfied:
171 outputs.append(platform)
172
173 if outputs:
174 print('\n'.join(outputs))
175 else:
176 print('No platforms matched.')
177
178
179if __name__ == '__main__':
180 main(sys.argv[1:])