blob: c48e161bf8f86632ee3fb6294c3f4b850aed26e9 [file] [log] [blame]
Michael Walshfefcbca2016-11-11 14:28:34 -06001#!/usr/bin/env python
2
3r"""
4Define the tally_sheet class.
5"""
6
7import sys
8import collections
9import copy
10import re
11
12try:
13 from robot.utils import DotDict
14except ImportError:
15 pass
16
17import gen_print as gp
18
19
Michael Walshfefcbca2016-11-11 14:28:34 -060020class tally_sheet:
21
22 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050023 This class is the implementation of a tally sheet. The sheet can be viewed as rows and columns. Each
24 row has a unique key field.
Michael Walshfefcbca2016-11-11 14:28:34 -060025
26 This class provides methods to tally the results (totals, etc.).
27
28 Example code:
29
30 # Create an ordered dict to represent your field names/initial values.
31 try:
Michael Walsh410b1782019-10-22 15:56:18 -050032 boot_results_fields = collections.OrderedDict([('total', 0), ('pass', 0), ('fail', 0)])
Michael Walshfefcbca2016-11-11 14:28:34 -060033 except AttributeError:
34 boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)])
35 # Create the tally sheet.
Michael Walsh410b1782019-10-22 15:56:18 -050036 boot_test_results = tally_sheet('boot type', boot_results_fields, 'boot_test_results')
Michael Walshfefcbca2016-11-11 14:28:34 -060037 # Set your sum fields (fields which are to be totalled).
38 boot_test_results.set_sum_fields(['total', 'pass', 'fail'])
Michael Walsh410b1782019-10-22 15:56:18 -050039 # Set calc fields (within a row, a certain field can be derived from other fields in the row.
Michael Walshfefcbca2016-11-11 14:28:34 -060040 boot_test_results.set_calc_fields(['total=pass+fail'])
41
42 # Create some records.
43 boot_test_results.add_row('BMC Power On')
44 boot_test_results.add_row('BMC Power Off')
45
46 # Increment field values.
47 boot_test_results.inc_row_field('BMC Power On', 'pass')
48 boot_test_results.inc_row_field('BMC Power Off', 'pass')
49 boot_test_results.inc_row_field('BMC Power On', 'fail')
50 # Have the results tallied...
51 boot_test_results.calc()
52 # And printed...
53 boot_test_results.print_report()
54
55 Example result:
56
Michael Walsh86f029f2018-03-27 11:58:30 -050057 Boot Type Total Pass Fail
58 ----------------------------------- ----- ---- ----
59 BMC Power On 2 1 1
60 BMC Power Off 1 1 0
61 ===================================================
Michael Walsh410b1782019-10-22 15:56:18 -050062 Totals 3 2 1
Michael Walshfefcbca2016-11-11 14:28:34 -060063
64 """
65
66 def __init__(self,
67 row_key_field_name='Description',
68 init_fields_dict=dict(),
69 obj_name='tally_sheet'):
Michael Walshfefcbca2016-11-11 14:28:34 -060070 r"""
71 Create a tally sheet object.
72
73 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -050074 row_key_field_name The name of the row key field (e.g. boot_type, team_name, etc.)
75 init_fields_dict A dictionary which contains field names/initial values.
Michael Walshfefcbca2016-11-11 14:28:34 -060076 obj_name The name of the tally sheet.
77 """
78
79 self.__obj_name = obj_name
80 # The row key field uniquely identifies the row.
81 self.__row_key_field_name = row_key_field_name
82 # Create a "table" which is an ordered dictionary.
Michael Walsh410b1782019-10-22 15:56:18 -050083 # If we're running python 2.7 or later, collections has an OrderedDict we can use. Otherwise, we'll
84 # try to use the DotDict (a robot library). If neither of those are available, we fail.
Michael Walshfefcbca2016-11-11 14:28:34 -060085 try:
86 self.__table = collections.OrderedDict()
87 except AttributeError:
88 self.__table = DotDict()
89 # Save the initial fields dictionary.
90 self.__init_fields_dict = init_fields_dict
91 self.__totals_line = init_fields_dict
92 self.__sum_fields = []
93 self.__calc_fields = []
94
95 def init(self,
96 row_key_field_name,
97 init_fields_dict,
98 obj_name='tally_sheet'):
99 self.__init__(row_key_field_name,
100 init_fields_dict,
101 obj_name='tally_sheet')
102
103 def set_sum_fields(self, sum_fields):
Michael Walshfefcbca2016-11-11 14:28:34 -0600104 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500105 Set the sum fields, i.e. create a list of field names which are to be summed and included on the
106 totals line of reports.
Michael Walshfefcbca2016-11-11 14:28:34 -0600107
108 Description of arguments:
109 sum_fields A list of field names.
110 """
111
112 self.__sum_fields = sum_fields
113
114 def set_calc_fields(self, calc_fields):
Michael Walshfefcbca2016-11-11 14:28:34 -0600115 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500116 Set the calc fields, i.e. create a list of field names within a given row which are to be calculated
117 for the user.
Michael Walshfefcbca2016-11-11 14:28:34 -0600118
119 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500120 calc_fields A string expression such as 'total=pass+fail' which shows which field on
121 a given row is derived from other fields in the same row.
Michael Walshfefcbca2016-11-11 14:28:34 -0600122 """
123
124 self.__calc_fields = calc_fields
125
126 def add_row(self, row_key, init_fields_dict=None):
Michael Walshfefcbca2016-11-11 14:28:34 -0600127 r"""
128 Add a row to the tally sheet.
129
130 Description of arguments:
131 row_key A unique key value.
Michael Walsh410b1782019-10-22 15:56:18 -0500132 init_fields_dict A dictionary of field names/initial values. The number of fields in this
133 dictionary must be the same as what was specified when the tally sheet
134 was created. If no value is passed, the value used to create the tally
135 sheet will be used.
Michael Walshfefcbca2016-11-11 14:28:34 -0600136 """
137
Michael Walsh5f4bce82019-07-16 16:33:01 -0500138 if row_key in self.__table:
139 # If we allow this, the row values get re-initialized.
140 message = "An entry for \"" + row_key + "\" already exists in"
141 message += " tally sheet."
142 raise ValueError(message)
Michael Walshfefcbca2016-11-11 14:28:34 -0600143 if init_fields_dict is None:
144 init_fields_dict = self.__init_fields_dict
145 try:
146 self.__table[row_key] = collections.OrderedDict(init_fields_dict)
147 except AttributeError:
148 self.__table[row_key] = DotDict(init_fields_dict)
149
150 def update_row_field(self, row_key, field_key, value):
Michael Walshfefcbca2016-11-11 14:28:34 -0600151 r"""
152 Update a field in a row with the specified value.
153
154 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500155 row_key A unique key value that identifies the row to be updated.
156 field_key The key that identifies which field in the row that is to be updated.
157 value The value to set into the specified row/field.
Michael Walshfefcbca2016-11-11 14:28:34 -0600158 """
159
160 self.__table[row_key][field_key] = value
161
162 def inc_row_field(self, row_key, field_key):
Michael Walshfefcbca2016-11-11 14:28:34 -0600163 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500164 Increment the value of the specified field in the specified row. The value of the field must be
165 numeric.
Michael Walshfefcbca2016-11-11 14:28:34 -0600166
167 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500168 row_key A unique key value that identifies the row to be updated.
169 field_key The key that identifies which field in the row that is to be updated.
Michael Walshfefcbca2016-11-11 14:28:34 -0600170 """
171
172 self.__table[row_key][field_key] += 1
173
174 def dec_row_field(self, row_key, field_key):
Michael Walshfefcbca2016-11-11 14:28:34 -0600175 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500176 Decrement the value of the specified field in the specified row. The value of the field must be
177 numeric.
Michael Walshfefcbca2016-11-11 14:28:34 -0600178
179 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500180 row_key A unique key value that identifies the row to be updated.
181 field_key The key that identifies which field in the row that is to be updated.
Michael Walshfefcbca2016-11-11 14:28:34 -0600182 """
183
184 self.__table[row_key][field_key] -= 1
185
186 def calc(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600187 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500188 Calculate totals and row calc fields. Also, return totals_line dictionary.
Michael Walshfefcbca2016-11-11 14:28:34 -0600189 """
190
191 self.__totals_line = copy.deepcopy(self.__init_fields_dict)
192 # Walk through the rows of the table.
193 for row_key, value in self.__table.items():
194 # Walk through the calc fields and process them.
195 for calc_field in self.__calc_fields:
196 tokens = [i for i in re.split(r'(\d+|\W+)', calc_field) if i]
197 cmd_buf = ""
198 for token in tokens:
199 if token in ("=", "+", "-", "*", "/"):
200 cmd_buf += token + " "
201 else:
202 # Note: Using "mangled" name for the sake of the exec
203 # statement (below).
204 cmd_buf += "self._" + self.__class__.__name__ +\
205 "__table['" + row_key + "']['" +\
206 token + "'] "
207 exec(cmd_buf)
208
209 for field_key, sub_value in value.items():
210 if field_key in self.__sum_fields:
211 self.__totals_line[field_key] += sub_value
212
213 return self.__totals_line
214
215 def sprint_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600216 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500217 sprint the fields of this object. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600218 """
219
220 buffer = ""
221
222 buffer += "class name: " + self.__class__.__name__ + "\n"
223 buffer += gp.sprint_var(self.__obj_name)
224 buffer += gp.sprint_var(self.__row_key_field_name)
225 buffer += gp.sprint_var(self.__table)
226 buffer += gp.sprint_var(self.__init_fields_dict)
227 buffer += gp.sprint_var(self.__sum_fields)
228 buffer += gp.sprint_var(self.__totals_line)
229 buffer += gp.sprint_var(self.__calc_fields)
230 buffer += gp.sprint_var(self.__table)
231
232 return buffer
233
234 def print_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600235 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500236 print the fields of this object to stdout. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600237 """
238
239 sys.stdout.write(self.sprint_obj())
240
241 def sprint_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600242 r"""
243 sprint the tally sheet in a formatted way.
244 """
245
246 buffer = ""
247 # Build format strings.
248 col_names = [self.__row_key_field_name.title()]
Michael Walsh86f029f2018-03-27 11:58:30 -0500249 report_width = 40
250 key_width = 40
Michael Walshfefcbca2016-11-11 14:28:34 -0600251 format_string = '{0:<' + str(key_width) + '}'
252 dash_format_string = '{0:-<' + str(key_width) + '}'
253 field_num = 0
254
Michael Walsh5f4bce82019-07-16 16:33:01 -0500255 try:
256 first_rec = next(iter(self.__table.items()))
257 for row_key, value in first_rec[1].items():
258 field_num += 1
259 if isinstance(value, int):
260 align = ':>'
261 else:
262 align = ':<'
263 format_string += ' {' + str(field_num) + align +\
264 str(len(row_key)) + '}'
265 dash_format_string += ' {' + str(field_num) + ':->' +\
266 str(len(row_key)) + '}'
267 report_width += 1 + len(row_key)
268 col_names.append(row_key.title())
269 except StopIteration:
270 pass
Michael Walshfefcbca2016-11-11 14:28:34 -0600271 num_fields = field_num + 1
272 totals_line_fmt = '{0:=<' + str(report_width) + '}'
273
274 buffer += format_string.format(*col_names) + "\n"
275 buffer += dash_format_string.format(*([''] * num_fields)) + "\n"
276 for row_key, value in self.__table.items():
277 buffer += format_string.format(row_key, *value.values()) + "\n"
278
279 buffer += totals_line_fmt.format('') + "\n"
280 buffer += format_string.format('Totals',
281 *self.__totals_line.values()) + "\n"
282
283 return buffer
284
285 def print_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600286 r"""
287 print the tally sheet in a formatted way.
288 """
289
290 sys.stdout.write(self.sprint_report())