blob: 52ed27954fcb605db638a26100db6de7a8dff211 [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"""
Michael Walsh410b1782019-10-22 15:56:18 -050022 This class is the implementation of a tally sheet. The sheet can be viewed as rows and columns. Each
23 row has a unique key field.
Michael Walshfefcbca2016-11-11 14:28:34 -060024
25 This class provides methods to tally the results (totals, etc.).
26
27 Example code:
28
29 # Create an ordered dict to represent your field names/initial values.
30 try:
Michael Walsh410b1782019-10-22 15:56:18 -050031 boot_results_fields = collections.OrderedDict([('total', 0), ('pass', 0), ('fail', 0)])
Michael Walshfefcbca2016-11-11 14:28:34 -060032 except AttributeError:
33 boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)])
34 # Create the tally sheet.
Michael Walsh410b1782019-10-22 15:56:18 -050035 boot_test_results = tally_sheet('boot type', boot_results_fields, 'boot_test_results')
Michael Walshfefcbca2016-11-11 14:28:34 -060036 # Set your sum fields (fields which are to be totalled).
37 boot_test_results.set_sum_fields(['total', 'pass', 'fail'])
Michael Walsh410b1782019-10-22 15:56:18 -050038 # 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 -060039 boot_test_results.set_calc_fields(['total=pass+fail'])
40
41 # Create some records.
42 boot_test_results.add_row('BMC Power On')
43 boot_test_results.add_row('BMC Power Off')
44
45 # Increment field values.
46 boot_test_results.inc_row_field('BMC Power On', 'pass')
47 boot_test_results.inc_row_field('BMC Power Off', 'pass')
48 boot_test_results.inc_row_field('BMC Power On', 'fail')
49 # Have the results tallied...
50 boot_test_results.calc()
51 # And printed...
52 boot_test_results.print_report()
53
54 Example result:
55
Michael Walsh86f029f2018-03-27 11:58:30 -050056 Boot Type Total Pass Fail
57 ----------------------------------- ----- ---- ----
58 BMC Power On 2 1 1
59 BMC Power Off 1 1 0
60 ===================================================
Michael Walsh410b1782019-10-22 15:56:18 -050061 Totals 3 2 1
Michael Walshfefcbca2016-11-11 14:28:34 -060062
63 """
64
Patrick Williams20f38712022-12-08 06:18:26 -060065 def __init__(
66 self,
67 row_key_field_name="Description",
68 init_fields_dict=dict(),
69 obj_name="tally_sheet",
70 ):
Michael Walshfefcbca2016-11-11 14:28:34 -060071 r"""
72 Create a tally sheet object.
73
74 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -050075 row_key_field_name The name of the row key field (e.g. boot_type, team_name, etc.)
76 init_fields_dict A dictionary which contains field names/initial values.
Michael Walshfefcbca2016-11-11 14:28:34 -060077 obj_name The name of the tally sheet.
78 """
79
80 self.__obj_name = obj_name
81 # The row key field uniquely identifies the row.
82 self.__row_key_field_name = row_key_field_name
83 # Create a "table" which is an ordered dictionary.
Michael Walsh410b1782019-10-22 15:56:18 -050084 # If we're running python 2.7 or later, collections has an OrderedDict we can use. Otherwise, we'll
85 # try to use the DotDict (a robot library). If neither of those are available, we fail.
Michael Walshfefcbca2016-11-11 14:28:34 -060086 try:
87 self.__table = collections.OrderedDict()
88 except AttributeError:
89 self.__table = DotDict()
90 # Save the initial fields dictionary.
91 self.__init_fields_dict = init_fields_dict
92 self.__totals_line = init_fields_dict
93 self.__sum_fields = []
94 self.__calc_fields = []
95
Patrick Williams20f38712022-12-08 06:18:26 -060096 def init(
97 self, row_key_field_name, init_fields_dict, obj_name="tally_sheet"
98 ):
99 self.__init__(
100 row_key_field_name, init_fields_dict, obj_name="tally_sheet"
101 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600102
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.
Patrick Williams20f38712022-12-08 06:18:26 -0600140 message = 'An entry for "' + row_key + '" already exists in'
Michael Walsh5f4bce82019-07-16 16:33:01 -0500141 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:
Patrick Williams20f38712022-12-08 06:18:26 -0600196 tokens = [i for i in re.split(r"(\d+|\W+)", calc_field) if i]
Michael Walshfefcbca2016-11-11 14:28:34 -0600197 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).
Patrick Williams20f38712022-12-08 06:18:26 -0600204 cmd_buf += (
205 "self._"
206 + self.__class__.__name__
207 + "__table['"
208 + row_key
209 + "']['"
210 + token
211 + "'] "
212 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600213 exec(cmd_buf)
214
215 for field_key, sub_value in value.items():
216 if field_key in self.__sum_fields:
217 self.__totals_line[field_key] += sub_value
218
219 return self.__totals_line
220
221 def sprint_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600222 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500223 sprint the fields of this object. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600224 """
225
226 buffer = ""
227
228 buffer += "class name: " + self.__class__.__name__ + "\n"
229 buffer += gp.sprint_var(self.__obj_name)
230 buffer += gp.sprint_var(self.__row_key_field_name)
231 buffer += gp.sprint_var(self.__table)
232 buffer += gp.sprint_var(self.__init_fields_dict)
233 buffer += gp.sprint_var(self.__sum_fields)
234 buffer += gp.sprint_var(self.__totals_line)
235 buffer += gp.sprint_var(self.__calc_fields)
236 buffer += gp.sprint_var(self.__table)
237
238 return buffer
239
240 def print_obj(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600241 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500242 print the fields of this object to stdout. This would normally be for debug purposes.
Michael Walshfefcbca2016-11-11 14:28:34 -0600243 """
244
245 sys.stdout.write(self.sprint_obj())
246
247 def sprint_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600248 r"""
249 sprint the tally sheet in a formatted way.
250 """
251
252 buffer = ""
253 # Build format strings.
254 col_names = [self.__row_key_field_name.title()]
Michael Walsh86f029f2018-03-27 11:58:30 -0500255 report_width = 40
256 key_width = 40
Patrick Williams20f38712022-12-08 06:18:26 -0600257 format_string = "{0:<" + str(key_width) + "}"
258 dash_format_string = "{0:-<" + str(key_width) + "}"
Michael Walshfefcbca2016-11-11 14:28:34 -0600259 field_num = 0
260
Michael Walsh5f4bce82019-07-16 16:33:01 -0500261 try:
262 first_rec = next(iter(self.__table.items()))
263 for row_key, value in first_rec[1].items():
264 field_num += 1
265 if isinstance(value, int):
Patrick Williams20f38712022-12-08 06:18:26 -0600266 align = ":>"
Michael Walsh5f4bce82019-07-16 16:33:01 -0500267 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600268 align = ":<"
269 format_string += (
270 " {" + str(field_num) + align + str(len(row_key)) + "}"
271 )
272 dash_format_string += (
273 " {" + str(field_num) + ":->" + str(len(row_key)) + "}"
274 )
Michael Walsh5f4bce82019-07-16 16:33:01 -0500275 report_width += 1 + len(row_key)
276 col_names.append(row_key.title())
277 except StopIteration:
278 pass
Michael Walshfefcbca2016-11-11 14:28:34 -0600279 num_fields = field_num + 1
Patrick Williams20f38712022-12-08 06:18:26 -0600280 totals_line_fmt = "{0:=<" + str(report_width) + "}"
Michael Walshfefcbca2016-11-11 14:28:34 -0600281
282 buffer += format_string.format(*col_names) + "\n"
Patrick Williams20f38712022-12-08 06:18:26 -0600283 buffer += dash_format_string.format(*([""] * num_fields)) + "\n"
Michael Walshfefcbca2016-11-11 14:28:34 -0600284 for row_key, value in self.__table.items():
285 buffer += format_string.format(row_key, *value.values()) + "\n"
286
Patrick Williams20f38712022-12-08 06:18:26 -0600287 buffer += totals_line_fmt.format("") + "\n"
288 buffer += (
289 format_string.format("Totals", *self.__totals_line.values()) + "\n"
290 )
Michael Walshfefcbca2016-11-11 14:28:34 -0600291
292 return buffer
293
294 def print_report(self):
Michael Walshfefcbca2016-11-11 14:28:34 -0600295 r"""
296 print the tally sheet in a formatted way.
297 """
298
299 sys.stdout.write(self.sprint_report())