feat(model): Hoststack type
[csit.git] / resources / libraries / python / model / ExportResult.py
1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Module with keywords that publish parts of result structure."""
15
16 from robot.libraries.BuiltIn import BuiltIn
17
18 from resources.libraries.python.model.util import descend, get_export_data
19
20
21 def export_dut_type_and_version(dut_type="unknown", dut_version="unknown"):
22     """Export the arguments as dut type and version.
23
24     Robot tends to convert "none" into None, hence the unusual default values.
25
26     If either argument is missing, the value from robot variable is used.
27     If argument is present, the value is also stored to robot suite variable.
28
29     :param dut_type: DUT type, e.g. VPP or DPDK.
30     :param dut_version: DUT version as determined by the caller.
31     :type dut_type: Optional[str]
32     :type dut_version: Optiona[str]
33     :raises RuntimeError: If value is neither in argument not robot variable.
34     """
35     if dut_type == "unknown":
36         dut_type = BuiltIn().get_variable_value("\\${DUT_TYPE}", "unknown")
37         if dut_type == "unknown":
38             raise RuntimeError("Dut type not provided.")
39     else:
40         # We want to set a variable in higher level suite setup
41         # to be available to test setup several levels lower.
42         BuiltIn().set_suite_variable(
43             "\\${DUT_TYPE}", dut_type, "children=True"
44         )
45     if dut_version == "unknown":
46         dut_version = BuiltIn().get_variable_value(
47             "\\${DUT_VERSION}", "unknown"
48         )
49         if dut_type == "unknown":
50             raise RuntimeError("Dut version not provided.")
51     else:
52         BuiltIn().set_suite_variable(
53             "\\${DUT_VERSION}", dut_version, "children=True"
54         )
55     data = get_export_data()
56     data["dut_type"] = dut_type.lower()
57     data["dut_version"] = dut_version
58
59
60 def export_tg_type_and_version(tg_type="unknown", tg_version="unknown"):
61     """Export the arguments as tg type and version.
62
63     Robot tends to convert "none" into None, hence the unusual default values.
64
65     If either argument is missing, the value from robot variable is used.
66     If argument is present, the value is also stored to robot suite variable.
67
68     :param tg_type: TG type, e.g. TREX.
69     :param tg_version: TG version as determined by the caller.
70     :type tg_type: Optional[str]
71     :type tg_version: Optiona[str]
72     :raises RuntimeError: If value is neither in argument not robot variable.
73     """
74     if tg_type == "unknown":
75         tg_type = BuiltIn().get_variable_value("\\${TG_TYPE}", "unknown")
76         if tg_type == "unknown":
77             raise RuntimeError("TG type not provided!")
78     else:
79         # We want to set a variable in higher level suite setup
80         # to be available to test setup several levels lower.
81         BuiltIn().set_suite_variable(
82             "\\${TG_TYPE}", tg_type, "children=True"
83         )
84     if tg_version == "unknown":
85         tg_version = BuiltIn().get_variable_value(
86             "\\${TG_VERSION}", "unknown"
87         )
88         if tg_type == "unknown":
89             raise RuntimeError("TG version not provided!")
90     else:
91         BuiltIn().set_suite_variable(
92             "\\${TG_VERSION}", tg_version, "children=True"
93         )
94     data = get_export_data()
95     data["tg_type"] = tg_type.lower()
96     data["tg_version"] = tg_version
97
98
99 def append_mrr_value(mrr_value, unit):
100     """Store mrr value to proper place so it is dumped into json.
101
102     The value is appended only when unit is not empty.
103
104     :param mrr_value: Forwarding rate from MRR trial.
105     :param unit: Unit of measurement for the rate.
106     :type mrr_value: float
107     :type unit: str
108     """
109     if not unit:
110         return
111     data = get_export_data()
112     data["result"]["type"] = "mrr"
113     rate_node = descend(descend(data["result"], "receive_rate"), "rate")
114     rate_node["unit"] = str(unit)
115     values_list = descend(rate_node, "values", list)
116     values_list.append(float(mrr_value))
117
118
119 def export_search_bound(text, value, unit, bandwidth=None):
120     """Store bound value and unit.
121
122     This function works for both NDRPDR and SOAK, decided by text.
123
124     If a node does not exist, it is created.
125     If a previous value exists, it is overwritten silently.
126     Result type is set (overwritten) to ndrpdr (or soak).
127
128     Text is used to determine whether it is ndr or pdr, upper or lower bound,
129     as the Robot caller has the information only there.
130
131     :param text: Info from Robot caller to determime bound type.
132     :param value: The bound value in packets (or connections) per second.
133     :param unit: Rate unit the bound is measured (or estimated) in.
134     :param bandwidth: The same value recomputed into L1 bits per second.
135     :type text: str
136     :type value: float
137     :type unit: str
138     :type bandwidth: Optional[float]
139     """
140     value = float(value)
141     text = str(text).lower()
142     result_type = "soak" if "plrsearch" in text else "ndrpdr"
143     upper_or_lower = "upper" if "upper" in text else "lower"
144     ndr_or_pdr = "ndr" if "ndr" in text else "pdr"
145
146     result_node = get_export_data()["result"]
147     result_node["type"] = result_type
148     rate_item = dict(rate=dict(value=value, unit=unit))
149     if bandwidth:
150         rate_item["bandwidth"] = dict(value=float(bandwidth), unit="bps")
151     if result_type == "soak":
152         descend(result_node, "critical_rate")[upper_or_lower] = rate_item
153         return
154     descend(result_node, ndr_or_pdr)[upper_or_lower] = rate_item
155
156
157 def _add_latency(result_node, percent, whichward, latency_string):
158     """Descend to a corresponding node and add values from latency string.
159
160     This is an internal block, moved out from export_ndrpdr_latency,
161     as it can be called up to 4 times.
162
163     :param result_node: UTI tree node to descend from.
164     :param percent: Percent value to use in node key (90, 50, 10, 0).
165     :param whichward: "forward" or "reverse".
166     :param latency_item: Unidir output from TRex utility, min/avg/max/hdrh.
167     :type result_node: dict
168     :type percent: int
169     :type whichward: str
170     :latency_string: str
171     """
172     l_min, l_avg, l_max, l_hdrh = latency_string.split("/", 3)
173     whichward_node = descend(result_node, f"latency_{whichward}")
174     percent_node = descend(whichward_node, f"pdr_{percent}")
175     percent_node["min"] = int(l_min)
176     percent_node["avg"] = int(l_avg)
177     percent_node["max"] = int(l_max)
178     percent_node["hdrh"] = l_hdrh
179     percent_node["unit"] = "us"
180
181
182 def export_ndrpdr_latency(text, latency):
183     """Store NDRPDR hdrh latency data.
184
185     If "latency" node does not exist, it is created.
186     If a previous value exists, it is overwritten silently.
187
188     Text is used to determine what percentage of PDR is the load,
189     as the Robot caller has the information only there.
190
191     Reverse data may be missing, we assume the test was unidirectional.
192
193     :param text: Info from Robot caller to determime load.
194     :param latency: Output from TRex utility, min/avg/max/hdrh.
195     :type text: str
196     :type latency: 1-tuple or 2-tuple of str
197     """
198     result_node = get_export_data()["result"]
199     percent = 0
200     if "90" in text:
201         percent = 90
202     elif "50" in text:
203         percent = 50
204     elif "10" in text:
205         percent = 10
206     _add_latency(result_node, percent, "forward", latency[0])
207     # Else TRex does not support latency measurement for this traffic profile.
208     if len(latency) < 2:
209         return
210     _add_latency(result_node, percent, "reverse", latency[1])
211
212
213 def export_reconf_result(packet_rate, packet_loss, bandwidth):
214     """Export the RECONF type results.
215
216     Result type is set to reconf.
217
218     :param packet_rate: Aggregate offered load in packets per second.
219     :param packet_loss: How many of the packets were dropped or unsent.
220     :param bandwidth: The offered load recomputed into L1 bits per second.
221     :type packet_rate: float
222     :type packet_loss: int
223     :type bandwidth: float
224     """
225     result_node = get_export_data()["result"]
226     result_node["type"] = "reconf"
227
228     time_loss = int(packet_loss) / float(packet_rate)
229     result_node["aggregate_rate"] = dict(
230         bandwidth=dict(
231             unit="bps",
232             value=float(bandwidth)
233         ),
234         rate=dict(
235             unit="pps",
236             value=float(packet_rate)
237         )
238     )
239     result_node["loss"] = dict(
240         packet=dict(
241             unit="packets",
242             value=int(packet_loss)
243         ),
244         time=dict(
245             unit="s",
246             value=time_loss
247         )
248     )
249
250
251 def export_hoststack_results(
252         bandwidth, rate=None, rate_unit=None, latency=None,
253         failed_requests=None, completed_requests=None, retransmits=None,
254         duration=None
255 ):
256     """Export the HOSTSTACK type results.
257
258     Result type is set to hoststack.
259
260     :param bandwidth: Measured transfer rate using bps as a unit.
261     :param rate: Resulting rate measured by the test. [Optional]
262     :param rate_unit: CPS or RPS. [Optional]
263     :param latency: Measure latency. [Optional]
264     :param failed_requests: Number of failed requests. [Optional]
265     :param completed_requests: Number of completed requests. [Optional]
266     :param retransmits: Retransmitted TCP packets. [Optional]
267     :param duration: Measurment duration. [Optional]
268     :type bandwidth: float
269     :type rate: float
270     :type rate_unit: str
271     :type latency: float
272     :type failed_requests: int
273     :type completed_requests: int
274     :type retransmits: int
275     :type duration: float
276     """
277     result_node = get_export_data()["result"]
278     result_node["type"] = "hoststack"
279
280     result_node["bandwidth"] = dict(unit="bps", value=bandwidth)
281     if rate is not None:
282         result_node["rate"] = \
283             dict(unit=rate_unit, value=rate)
284     if latency is not None:
285         result_node["latency"] = \
286             dict(unit="ms", value=latency)
287     if failed_requests is not None:
288         result_node["failed_requests"] = \
289             dict(unit="requests", value=failed_requests)
290     if completed_requests is not None:
291         result_node["completed_requests"] = \
292             dict(unit="requests", value=completed_requests)
293     if retransmits is not None:
294         result_node["retransmits"] = \
295             dict(unit="packets", value=retransmits)
296     if duration is not None:
297         result_node["duration"] = \
298             dict(unit="s", value=duration)
299
300
301 def append_telemetry(telemetry_item):
302     """Append telemetry entry to proper place so it is dumped into json.
303
304     :param telemetry_item: Telemetry entry.
305     :type telemetry_item: str
306     """
307     data = get_export_data()
308     data["telemetry"].append(telemetry_item)