blob: 2994907d950ce6c17dbed42862a2a331ef54de28 [file] [log] [blame]
George Keishinge7e91712021-09-03 11:28:44 -05001#!/usr/bin/env python3
Michael Walshfefcbca2016-11-11 14:28:34 -06002
3r"""
4Define the tally_sheet class.
5"""
6
Michael Walshfefcbca2016-11-11 14:28:34 -06007import collections
8import copy
9import re
Patrick Williams20f38712022-12-08 06:18:26 -060010import sys
Michael Walshfefcbca2016-11-11 14:28:34 -060011
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:
Michael Walshfefcbca2016-11-11 14:28:34 -060021 r"""
George Keishing39967042023-02-16 00:43:51 -060022 This class is the implementation of a tally sheet.
23 The sheet can be viewed as rows and columns. Each
Michael Walsh410b1782019-10-22 15:56:18 -050024 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
Patrick Williams20f38712022-12-08 06:18:26 -060066 def __init__(
67 self,
68 row_key_field_name="Description",
69 init_fields_dict=dict(),
70 obj_name="tally_sheet",
71 ):
Michael Walshfefcbca2016-11-11 14:28:34 -060072 r"""
73 Create a tally sheet object.
74
75 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -050076 row_key_field_name The name of the row key field (e.g. boot_type, team_name, etc.)
77 init_fields_dict A dictionary which contains field names/initial values.
Michael Walshfefcbca2016-11-11 14:28:34 -060078 obj_name The name of the tally sheet.
79 """
80
81 self.__obj_name = obj_name
82 # The row key field uniquely identifies the row.
83 self.__row_key_field_name = row_key_field_name
84 # Create a "table" which is an ordered dictionary.
George Keishing39967042023-02-16 00:43:51 -060085 # If we're running python 2.7 or later, collections has an OrderedDict
86 # we can use. Otherwise, we'll try to use the DotDict (a robot library).
87 # If neither of those are available, we fail.
Michael Walshfefcbca2016-11-11 14:28:34 -060088 try:
89 self.__table = collections.OrderedDict()
90 except AttributeError:
91 self.__table = DotDict()
92 # Save the initial fields dictionary.
93 self.__init_fields_dict = init_fields_dict
94 self.__totals_line = init_fields_dict
95 self.__sum_fields = []
96 self.__calc_fields = []
97
Patrick Williams20f38712022-12-08 06:18:26 -060098 def init(
99 self, row_key_field_name, init_fields_dict, obj_name="tally_sheet"
100 ):
101 self.__init__(
102 row_key_field_name, init_fields_dict, obj_name="tally_sheet"
103 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600104
105 def set_sum_fields(self, sum_fields):
Michael Walshfefcbca2016-11-11 14:28:34 -0600106 r"""
George Keishing39967042023-02-16 00:43:51 -0600107 Set the sum fields, i.e. create a list of field names which are to be
108 summed and included on the totals line of reports.
Michael Walshfefcbca2016-11-11 14:28:34 -0600109
110 Description of arguments:
111 sum_fields A list of field names.
112 """
113
114 self.__sum_fields = sum_fields
115
116 def set_calc_fields(self, calc_fields):
Michael Walshfefcbca2016-11-11 14:28:34 -0600117 r"""
George Keishing39967042023-02-16 00:43:51 -0600118 Set the calc fields, i.e. create a list of field names within a given
119 row which are to be calculated
Michael Walsh410b1782019-10-22 15:56:18 -0500120 for the user.
Michael Walshfefcbca2016-11-11 14:28:34 -0600121
122 Description of arguments:
George Keishing39967042023-02-16 00:43:51 -0600123 calc_fields A string expression such as 'total=pass+fail'
124 which shows which field on a given row is
125 derived from other fields in the same row.
Michael Walshfefcbca2016-11-11 14:28:34 -0600126 """
127
128 self.__calc_fields = calc_fields
129
130 def add_row(self, row_key, init_fields_dict=None):
Michael Walshfefcbca2016-11-11 14:28:34 -0600131 r"""
132 Add a row to the tally sheet.
133
134 Description of arguments:
George Keishing39967042023-02-16 00:43:51 -0600135 row_key A unique key value.
136 init_fields_dict A dictionary of field names/initial values.
137 The number of fields in this dictionary must
138 be the same as what was specified when the
139 tally sheet was created. If no value is passed,
140 the value used to create the tally sheet will
141 be used.
Michael Walshfefcbca2016-11-11 14:28:34 -0600142 """
143
Michael Walsh5f4bce82019-07-16 16:33:01 -0500144 if row_key in self.__table:
145 # If we allow this, the row values get re-initialized.
Patrick Williams20f38712022-12-08 06:18:26 -0600146 message = 'An entry for "' + row_key + '" already exists in'
Michael Walsh5f4bce82019-07-16 16:33:01 -0500147 message += " tally sheet."
148 raise ValueError(message)
Michael Walshfefcbca2016-11-11 14:28:34 -0600149 if init_fields_dict is None:
150 init_fields_dict = self.__init_fields_dict
151 try:
152 self.__table[row_key] = collections.OrderedDict(init_fields_dict)
153 except AttributeError:
154 self.__table[row_key] = DotDict(init_fields_dict)
155
156 def update_row_field(self, row_key, field_key, value):
Michael Walshfefcbca2016-11-11 14:28:34 -0600157 r"""
158 Update a field in a row with the specified value.
159
160 Description of arguments:
George Keishing39967042023-02-16 00:43:51 -0600161 row_key A unique key value that identifies the row to
162 be updated.
163 field_key The key that identifies which field in the row
164 that is to be updated.
165 value The value to set into the specified row/field.
Michael Walshfefcbca2016-11-11 14:28:34 -0600166 """
167
168 self.__table[row_key][field_key] = value
169
170 def inc_row_field(self, row_key, field_key):
Michael Walshfefcbca2016-11-11 14:28:34 -0600171 r"""
George Keishing39967042023-02-16 00:43:51 -0600172 Increment the value of the specified field in the specified row.
173 The value of the field must be numeric.
Michael Walshfefcbca2016-11-11 14:28:34 -0600174
175 Description of arguments:
George Keishing39967042023-02-16 00:43:51 -0600176 row_key A unique key value that identifies the row to
177 be updated.
178 field_key The key that identifies which field in the row
179 that is to be updated.
Michael Walshfefcbca2016-11-11 14:28:34 -0600180 """
181
182 self.__table[row_key][field_key] += 1
183
184 def dec_row_field(self, row_key, field_key):
Michael Walshfefcbca2016-11-11 14:28:34 -0600185 r"""
George Keishing39967042023-02-16 00:43:51 -0600186 Decrement the value of the specified field in the specified row.
187 The value of the field must be
Michael Walsh410b1782019-10-22 15:56:18 -0500188 numeric.
Michael Walshfefcbca2016-11-11 14:28:34 -0600189
190 Description of arguments:
George Keishing39967042023-02-16 00:43:51 -0600191 row_key A unique key value that identifies the row to
192 be updated.
193 field_key The key that identifies which field in the row
194 that is to be updated.
Michael Walshfefcbca2016-11-11 14:28:34 -0600195 """
196
197 self.__table[row_key][field_key] -= 1
198
199 def calc(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600200 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500201 Calculate totals and row calc fields. Also, return totals_line dictionary.
Michael Walshfefcbca2016-11-11 14:28:34 -0600202 """
203
204 self.__totals_line = copy.deepcopy(self.__init_fields_dict)
205 # Walk through the rows of the table.
206 for row_key, value in self.__table.items():
207 # Walk through the calc fields and process them.
208 for calc_field in self.__calc_fields:
Patrick Williams20f38712022-12-08 06:18:26 -0600209 tokens = [i for i in re.split(r"(\d+|\W+)", calc_field) if i]
Michael Walshfefcbca2016-11-11 14:28:34 -0600210 cmd_buf = ""
211 for token in tokens:
212 if token in ("=", "+", "-", "*", "/"):
213 cmd_buf += token + " "
214 else:
215 # Note: Using "mangled" name for the sake of the exec
216 # statement (below).
Patrick Williams20f38712022-12-08 06:18:26 -0600217 cmd_buf += (
218 "self._"
219 + self.__class__.__name__
220 + "__table['"
221 + row_key
222 + "']['"
223 + token
224 + "'] "
225 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600226 exec(cmd_buf)
227
228 for field_key, sub_value in value.items():
229 if field_key in self.__sum_fields:
230 self.__totals_line[field_key] += sub_value
231
232 return self.__totals_line
233
234 def sprint_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600235 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500236 sprint the fields of this object. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600237 """
238
239 buffer = ""
240
241 buffer += "class name: " + self.__class__.__name__ + "\n"
242 buffer += gp.sprint_var(self.__obj_name)
243 buffer += gp.sprint_var(self.__row_key_field_name)
244 buffer += gp.sprint_var(self.__table)
245 buffer += gp.sprint_var(self.__init_fields_dict)
246 buffer += gp.sprint_var(self.__sum_fields)
247 buffer += gp.sprint_var(self.__totals_line)
248 buffer += gp.sprint_var(self.__calc_fields)
249 buffer += gp.sprint_var(self.__table)
250
251 return buffer
252
253 def print_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600254 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500255 print the fields of this object to stdout. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600256 """
257
258 sys.stdout.write(self.sprint_obj())
259
260 def sprint_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600261 r"""
262 sprint the tally sheet in a formatted way.
263 """
264
265 buffer = ""
266 # Build format strings.
267 col_names = [self.__row_key_field_name.title()]
Michael Walsh86f029f2018-03-27 11:58:30 -0500268 report_width = 40
269 key_width = 40
Patrick Williams20f38712022-12-08 06:18:26 -0600270 format_string = "{0:<" + str(key_width) + "}"
271 dash_format_string = "{0:-<" + str(key_width) + "}"
Michael Walshfefcbca2016-11-11 14:28:34 -0600272 field_num = 0
273
Michael Walsh5f4bce82019-07-16 16:33:01 -0500274 try:
275 first_rec = next(iter(self.__table.items()))
276 for row_key, value in first_rec[1].items():
277 field_num += 1
278 if isinstance(value, int):
Patrick Williams20f38712022-12-08 06:18:26 -0600279 align = ":>"
Michael Walsh5f4bce82019-07-16 16:33:01 -0500280 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600281 align = ":<"
282 format_string += (
283 " {" + str(field_num) + align + str(len(row_key)) + "}"
284 )
285 dash_format_string += (
286 " {" + str(field_num) + ":->" + str(len(row_key)) + "}"
287 )
Michael Walsh5f4bce82019-07-16 16:33:01 -0500288 report_width += 1 + len(row_key)
289 col_names.append(row_key.title())
290 except StopIteration:
291 pass
Michael Walshfefcbca2016-11-11 14:28:34 -0600292 num_fields = field_num + 1
Patrick Williams20f38712022-12-08 06:18:26 -0600293 totals_line_fmt = "{0:=<" + str(report_width) + "}"
Michael Walshfefcbca2016-11-11 14:28:34 -0600294
295 buffer += format_string.format(*col_names) + "\n"
Patrick Williams20f38712022-12-08 06:18:26 -0600296 buffer += dash_format_string.format(*([""] * num_fields)) + "\n"
Michael Walshfefcbca2016-11-11 14:28:34 -0600297 for row_key, value in self.__table.items():
298 buffer += format_string.format(row_key, *value.values()) + "\n"
299
Patrick Williams20f38712022-12-08 06:18:26 -0600300 buffer += totals_line_fmt.format("") + "\n"
301 buffer += (
302 format_string.format("Totals", *self.__totals_line.values()) + "\n"
303 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600304
305 return buffer
306
307 def print_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600308 r"""
309 print the tally sheet in a formatted way.
310 """
311
312 sys.stdout.write(self.sprint_report())