Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 1 | |
| 2 | # osmo_gsm_tester: specifics for running a sysmoBTS |
| 3 | # |
| 4 | # Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH |
| 5 | # |
| 6 | # Author: Neels Hofmeyr <neels@hofmeyr.de> |
| 7 | # |
| 8 | # This program is free software: you can redistribute it and/or modify |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 9 | # it under the terms of the GNU General Public License as |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 10 | # published by the Free Software Foundation, either version 3 of the |
| 11 | # License, or (at your option) any later version. |
| 12 | # |
| 13 | # This program is distributed in the hope that it will be useful, |
| 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 16 | # GNU General Public License for more details. |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 17 | # |
Harald Welte | 2720534 | 2017-06-03 09:51:45 +0200 | [diff] [blame] | 18 | # You should have received a copy of the GNU General Public License |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 20 | |
| 21 | import socket |
| 22 | import struct |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 23 | import re |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 24 | |
Pau Espin Pedrol | e1a58bd | 2020-04-10 20:46:07 +0200 | [diff] [blame] | 25 | from ..core import log |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 26 | from ..core.event_loop import MainLoop |
| 27 | |
| 28 | VERB_SET = 'SET' |
| 29 | VERB_GET = 'GET' |
| 30 | VERB_SET_REPLY = 'SET_REPLY' |
| 31 | VERB_GET_REPLY = 'GET_REPLY' |
| 32 | VERB_TRAP = 'TRAP' |
| 33 | VERB_ERROR = 'ERROR' |
| 34 | RECV_VERBS = (VERB_GET_REPLY, VERB_SET_REPLY, VERB_TRAP, VERB_ERROR) |
| 35 | recv_re = re.compile('(%s) ([0-9]+) (.*)' % ('|'.join(RECV_VERBS)), |
| 36 | re.MULTILINE + re.DOTALL) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 37 | |
| 38 | class CtrlInterfaceExn(Exception): |
| 39 | pass |
| 40 | |
| 41 | class OsmoCtrl(log.Origin): |
Neels Hofmeyr | f80f7cc | 2020-12-06 22:51:13 +0100 | [diff] [blame] | 42 | _next_id = 1 |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 43 | |
| 44 | def __init__(self, host, port): |
Neels Hofmeyr | 1a7a3f0 | 2017-06-10 01:18:27 +0200 | [diff] [blame] | 45 | super().__init__(log.C_BUS, 'Ctrl', host=host, port=port) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 46 | self.host = host |
| 47 | self.port = port |
| 48 | self.sck = None |
Neels Hofmeyr | f79a86f | 2020-11-30 22:04:41 +0100 | [diff] [blame] | 49 | |
| 50 | def next_id(self): |
Neels Hofmeyr | f80f7cc | 2020-12-06 22:51:13 +0100 | [diff] [blame] | 51 | ret = OsmoCtrl._next_id |
| 52 | OsmoCtrl._next_id += 1 |
Neels Hofmeyr | f79a86f | 2020-11-30 22:04:41 +0100 | [diff] [blame] | 53 | return ret |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 54 | |
| 55 | def prefix_ipa_ctrl_header(self, data): |
| 56 | if isinstance(data, str): |
| 57 | data = data.encode('utf-8') |
| 58 | s = struct.pack(">HBB", len(data)+1, 0xee, 0) |
| 59 | return s + data |
| 60 | |
| 61 | def remove_ipa_ctrl_header(self, data): |
| 62 | if (len(data) < 4): |
| 63 | raise CtrlInterfaceExn("Answer too short!") |
| 64 | (plen, ipa_proto, osmo_proto) = struct.unpack(">HBB", data[:4]) |
| 65 | if (plen + 3 > len(data)): |
| 66 | self.err('Warning: Wrong payload length', expected=plen, got=len(data)-3) |
| 67 | if (ipa_proto != 0xee or osmo_proto != 0): |
| 68 | raise CtrlInterfaceExn("Wrong protocol in answer!") |
| 69 | return data[4:plen+3], data[plen+3:] |
| 70 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 71 | def try_connect(self): |
| 72 | '''Do a connection attempt, return True when successful, False otherwise. |
| 73 | Does not raise exceptions, but logs them to the debug log.''' |
| 74 | assert self.sck is None |
| 75 | try: |
| 76 | self.dbg('Connecting') |
| 77 | sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 78 | try: |
| 79 | sck.connect((self.host, self.port)) |
| 80 | except: |
| 81 | sck.close() |
| 82 | raise |
| 83 | # set self.sck only after the connect was successful |
| 84 | self.sck = sck |
| 85 | return True |
| 86 | except: |
| 87 | self.dbg('Failed to connect', sys.exc_info()[0]) |
| 88 | return False |
| 89 | |
| 90 | def connect(self, timeout=30): |
| 91 | '''Connect to the CTRL self.host and self.port, retry for 'timeout' seconds.''' |
| 92 | MainLoop.wait(self.try_connect, timestep=3, timeout=timeout) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 93 | self.sck.setblocking(1) |
Neels Hofmeyr | 05439d7 | 2020-12-01 03:52:55 +0100 | [diff] [blame] | 94 | self.sck.settimeout(10) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 95 | |
| 96 | def disconnect(self): |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 97 | if self.sck is None: |
| 98 | return |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 99 | self.dbg('Disconnecting') |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 100 | self.sck.close() |
| 101 | self.sck = None |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 102 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 103 | def _recv(self, verbs, match_args=None, match_id=None, attempts=10, length=1024): |
| 104 | '''Receive until a response matching the verbs / args / msg-id is obtained from CTRL. |
| 105 | The general socket timeout applies for each attempt made, see connect(). |
| 106 | Multiple attempts may be necessary if, for example, intermediate |
| 107 | messages are received that do not relate to what is expected, like |
| 108 | TRAPs that are not interesting. |
| 109 | |
| 110 | To receive a GET_REPLY / SET_REPLY: |
| 111 | verb, rx_id, val = _recv(('GET_REPLY', 'ERROR'), match_id=used_id) |
| 112 | if verb == 'ERROR': |
| 113 | raise CtrlInterfaceExn() |
| 114 | print(val) |
| 115 | |
| 116 | To receive a TRAP: |
| 117 | verb, rx_id, val = _recv('TRAP', 'bts_connection_status connected') |
| 118 | # val == 'bts_connection_status connected' |
| 119 | |
| 120 | If the CTRL is not connected yet, open and close a connection for |
| 121 | this operation only. |
| 122 | ''' |
| 123 | |
| 124 | # allow calling for both already connected VTY as well as establishing |
| 125 | # a connection just for this command. |
| 126 | if self.sck is None: |
| 127 | with self: |
| 128 | return self._recv(verbs, match_args=match_args, |
| 129 | match_id=match_id, attempts=attempts, length=length) |
| 130 | |
| 131 | if isinstance(verbs, str): |
| 132 | verbs = (verbs, ) |
| 133 | |
| 134 | for i in range(attempts): |
| 135 | data = self.sck.recv(length) |
| 136 | self.dbg('Receiving', data=data) |
| 137 | while len(data) > 0: |
| 138 | msg, data = self.remove_ipa_ctrl_header(data) |
| 139 | msg_str = msg.decode('utf-8') |
| 140 | |
| 141 | m = recv_re.fullmatch(msg_str) |
| 142 | if m is None: |
| 143 | raise CtrlInterfaceExn('Received garbage: %r' % data) |
| 144 | |
| 145 | rx_verb, rx_id, rx_args = m.groups() |
| 146 | rx_id = int(rx_id) |
| 147 | |
| 148 | if match_id is not None and match_id != rx_id: |
| 149 | continue |
| 150 | |
| 151 | if verbs and rx_verb not in verbs: |
| 152 | continue |
| 153 | |
| 154 | if match_args and not rx_args.startswith(match_args): |
| 155 | continue |
| 156 | |
| 157 | return rx_verb, rx_id, rx_args |
| 158 | raise CtrlInterfaceExn('No answer found: ' + reply_header) |
| 159 | |
| 160 | def _sendrecv(self, verb, send_args, *recv_args, use_id=None, **recv_kwargs): |
| 161 | '''Send a request and receive a matching response. |
| 162 | If the CTRL is not connected yet, open and close a connection for |
| 163 | this operation only. |
| 164 | ''' |
| 165 | if self.sck is None: |
| 166 | with self: |
| 167 | return self._sendrecv(verb, send_args, *recv_args, use_id=use_id, **recv_kwargs) |
| 168 | |
| 169 | if use_id is None: |
| 170 | use_id = self.next_id() |
| 171 | |
| 172 | # send |
| 173 | data = '{verb} {use_id} {send_args}'.format(**locals()) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 174 | self.dbg('Sending', data=data) |
| 175 | data = self.prefix_ipa_ctrl_header(data) |
| 176 | self.sck.send(data) |
| 177 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 178 | # receive reply |
| 179 | recv_kwargs['match_id'] = use_id |
| 180 | return self._recv(*recv_args, **recv_kwargs) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 181 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 182 | def set_var(self, var, value): |
| 183 | '''Set the value of a specific variable on a CTRL interface, and return the response, e.g.: |
| 184 | assert set_var('subscriber-modify-v1', '901701234567,2342') == 'OK' |
| 185 | If the CTRL is not connected yet, open and close a connection for |
| 186 | this operation only. |
| 187 | ''' |
| 188 | verb, rx_id, args = self._sendrecv(VERB_SET, '%s %s' % (var, value), (VERB_SET_REPLY, VERB_ERROR)) |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 189 | |
Neels Hofmeyr | 5b04ef2 | 2020-12-01 04:48:38 +0100 | [diff] [blame] | 190 | if verb == VERB_ERROR: |
| 191 | raise CtrlInterfaceExn('SET %s = %s returned %r' % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 192 | |
| 193 | var_and_space = var + ' ' |
| 194 | if not args.startswith(var_and_space): |
| 195 | raise CtrlInterfaceExn('SET %s = %s returned SET_REPLY for different var: %r' |
| 196 | % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 197 | |
| 198 | return args[len(var_and_space):] |
| 199 | |
| 200 | def get_var(self, var): |
| 201 | '''Get the value of a specific variable from a CTRL interface: |
| 202 | assert get_var('bts.0.oml-connection-state') == 'connected' |
| 203 | If the CTRL is not connected yet, open and close a connection for |
| 204 | this operation only. |
| 205 | ''' |
| 206 | verb, rx_id, args = self._sendrecv(VERB_GET, var, (VERB_GET_REPLY, VERB_ERROR)) |
| 207 | |
| 208 | if verb == VERB_ERROR: |
| 209 | raise CtrlInterfaceExn('GET %s returned %r' % (var, ' '.join((verb, str(rx_id), args)))) |
| 210 | |
| 211 | var_and_space = var + ' ' |
| 212 | if not args.startswith(var_and_space): |
| 213 | raise CtrlInterfaceExn('GET %s returned GET_REPLY for different var: %r' |
| 214 | % (var, value, ' '.join((verb, str(rx_id), args)))) |
| 215 | |
| 216 | return args[len(var_and_space):] |
| 217 | |
| 218 | def get_int_var(self, var): |
| 219 | '''Same as get_var() but return an int''' |
| 220 | return int(self.get_var(var)) |
| 221 | |
| 222 | def get_trap(self, name): |
| 223 | '''Read from CTRL until a TRAP of this name is received. |
| 224 | If name is None, any TRAP is returned. |
| 225 | If the CTRL is not connected yet, open and close a connection for |
| 226 | this operation only. |
| 227 | ''' |
| 228 | verb, rx_id, args = self._recv(VERB_TRAP, name) |
| 229 | name_and_space = var + ' ' |
| 230 | # _recv() should ensure this: |
| 231 | assert args.startswith(name_and_space) |
| 232 | return args[len(name_and_space):] |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 233 | |
| 234 | def __enter__(self): |
| 235 | self.connect() |
| 236 | return self |
| 237 | |
| 238 | def __exit__(self, *exc_info): |
| 239 | self.disconnect() |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 240 | |
Neels Hofmeyr | 5354058 | 2020-11-30 22:04:26 +0100 | [diff] [blame] | 241 | class RateCountersExn(log.Error): |
| 242 | pass |
| 243 | |
| 244 | class RateCounters(dict): |
| 245 | '''Usage example: |
| 246 | counter_names = ( |
| 247 | 'handover:completed', |
| 248 | 'handover:stopped', |
| 249 | 'handover:no_channel', |
| 250 | 'handover:timeout', |
| 251 | 'handover:failed', |
| 252 | 'handover:error', |
| 253 | ) |
| 254 | |
| 255 | # initialize the listing of CTRL vars of the counters to watch. |
| 256 | # First on the 'bsc' node: |
| 257 | # rate_ctr.abs.bsc.0.handover:completed |
| 258 | # rate_ctr.abs.bsc.0.handover:stopped |
| 259 | # ... |
| 260 | counters = RateCounters('bsc', counter_names, from_ctrl=bsc.ctrl) |
| 261 | |
| 262 | # And also add counters for two 'bts' instances: |
| 263 | # rate_ctr.abs.bts.0.handover:completed |
| 264 | # rate_ctr.abs.bts.0.handover:stopped |
| 265 | # ... |
| 266 | # rate_ctr.abs.bts.1.handover:completed |
| 267 | # ... |
| 268 | counters.add(RateCounters('bts', counter_names, instances=(0, 1))) |
| 269 | |
| 270 | # read initial counter values, from the bsc_ctrl, as set in |
| 271 | # counters.from_ctrl in the RateCounters() constructor above. |
| 272 | counters.read() |
| 273 | |
| 274 | # Do some actions that should increment counters in the SUT |
| 275 | do_a_handover() |
| 276 | |
| 277 | if approach_without_wait: |
| 278 | # increment the counters as expected |
| 279 | counters.inc('bts', 'handover:completed') |
| 280 | |
| 281 | # read counters from CTRL again, and fail if they differ |
| 282 | counters.verify() |
| 283 | |
| 284 | if approach_with_wait: |
| 285 | # you can wait for counters to change. counters.changed() does not |
| 286 | # modify counters' values, just reads values from CTRL and stores |
| 287 | # the changes in counters.diff. |
| 288 | wait(counters.changed, timeout=20) |
| 289 | |
| 290 | # log which counters changed by how much, found in counters.diff |
| 291 | # after each counters.changed() call: |
| 292 | print(counters.diff.str(skip_zero_vals=True)) |
| 293 | |
| 294 | if check_all_vals: |
| 295 | # Assert all values: |
| 296 | expected_diff = counters.copy().clear() |
| 297 | expected_diff.inc('bts', 'handover:completed', instances=(0, 1)) |
| 298 | counters.diff.expect(expected_diff) |
| 299 | else: |
| 300 | # Assert only some specific counters: |
| 301 | expected_diff = RateCounters() |
| 302 | expected_diff.inc('bts', 'handover:completed', instances=(0, 1)) |
| 303 | counters.diff.expect(expected_diff) |
| 304 | |
| 305 | # update counters to the last read values if desired |
| 306 | counters.add(counters.diff) |
| 307 | ''' |
| 308 | |
| 309 | def __init__(self, instance_names=(), counter_names=(), instances=0, kinds='abs', init_val=0, from_ctrl=None): |
| 310 | def init_cb(var): |
| 311 | self[var] = init_val |
| 312 | RateCounters.for_each(init_cb, instance_names, counter_names, instances, kinds, results=False) |
| 313 | self.from_ctrl = from_ctrl |
| 314 | self.diff = None |
| 315 | |
| 316 | @staticmethod |
| 317 | def for_each(callback_func, instance_names, counter_names, instances=0, kinds='abs', results=True): |
| 318 | '''Call callback_func for a set of rate counter var names, mostly |
| 319 | called by more convenient functions. See inc() for a comprehensive |
| 320 | explanation. |
| 321 | ''' |
| 322 | if type(instance_names) is str: |
| 323 | instance_names = (instance_names, ) |
| 324 | if type(counter_names) is str: |
| 325 | counter_names = (counter_names, ) |
| 326 | if type(kinds) is str: |
| 327 | kinds = (kinds, ) |
| 328 | if type(instances) is int: |
| 329 | instances = (instances, ) |
| 330 | if results is True: |
| 331 | results = RateCounters() |
| 332 | elif results is False: |
| 333 | results = None |
| 334 | for instance_name in instance_names: |
| 335 | for instance_nr in instances: |
| 336 | for counter_name in counter_names: |
| 337 | for kind in kinds: |
| 338 | var = 'rate_ctr.{kind}.{instance_name}.{instance_nr}.{counter_name}'.format(**locals()) |
| 339 | result = callback_func(var) |
| 340 | if results is not None: |
| 341 | results[var] = result |
| 342 | return results |
| 343 | |
| 344 | def __str__(self): |
| 345 | return self.str(', ', '') |
| 346 | |
| 347 | def str(self, sep='\n| ', prefix='\n| ', vals=None, skip_zero_vals=False): |
| 348 | '''The 'vals' arg is useful to print a plain dict() of counter values like a RateCounters class. |
| 349 | By default print self.''' |
| 350 | if vals is None: |
| 351 | vals = self |
| 352 | return prefix + sep.join('%s = %d' % (var, val) for var, val in sorted(vals.items()) |
| 353 | if (not skip_zero_vals) or (val != 0)) |
| 354 | |
| 355 | def inc(self, instance_names, counter_names, inc=1, instances=0, kinds='abs'): |
| 356 | '''Increment a set of counters. |
| 357 | inc('xyz', 'val') --> rate_ctr.abs.xyz.0.val += 1 |
| 358 | |
| 359 | inc('xyz', ('foo', 'bar')) --> rate_ctr.abs.xyz.0.foo += 1 |
| 360 | rate_ctr.abs.xyz.0.bar += 1 |
| 361 | |
| 362 | inc(('xyz', 'pqr'), 'val') --> rate_ctr.abs.xyz.0.val += 1 |
| 363 | rate_ctr.abs.pqr.0.val += 1 |
| 364 | |
| 365 | inc('xyz', 'val', instances=range(3)) |
| 366 | --> rate_ctr.abs.xyz.0.val += 1 |
| 367 | rate_ctr.abs.xyz.1.val += 1 |
| 368 | rate_ctr.abs.xyz.2.val += 1 |
| 369 | ''' |
| 370 | def inc_cb(var): |
| 371 | val = self.get(var, 0) |
| 372 | val += inc |
| 373 | self[var] = val |
| 374 | return val |
| 375 | RateCounters.for_each(inc_cb, instance_names, counter_names, instances, kinds, results=False) |
| 376 | return self |
| 377 | |
| 378 | def add(self, rate_counters): |
| 379 | '''Add the given values up to the values in self. |
| 380 | rate_counters can be a RateCounters instance or a plain dict of CTRL |
| 381 | var as key and counter integer as value. |
| 382 | ''' |
| 383 | for var, add_val in rate_counters.items(): |
| 384 | val = self.get(var, 0) |
| 385 | val += add_val |
| 386 | self[var] = val |
| 387 | return self |
| 388 | |
| 389 | def subtract(self, rate_counters): |
| 390 | '''Same as add(), but subtract values from self instead. |
| 391 | Useful to verify counters relative to an arbitrary reference.''' |
| 392 | for var, subtract_val in rate_counters.items(): |
| 393 | val = self.get(var, 0) |
| 394 | val -= subtract_val |
| 395 | self[var] = val |
| 396 | return self |
| 397 | |
| 398 | |
| 399 | def clear(self, val=0): |
| 400 | '''Set all counts to 0 (or a specific value)''' |
| 401 | for var in self.keys(): |
| 402 | self[var] = val |
| 403 | return self |
| 404 | |
| 405 | def copy(self): |
| 406 | '''Return a copy of all keys and values stored in self.''' |
| 407 | cpy = RateCounters(from_ctrl = self.from_ctrl) |
| 408 | cpy.update(self) |
| 409 | return cpy |
| 410 | |
| 411 | def read(self): |
| 412 | '''Read all counters from the CTRL connection passed to RateCounters(from_ctrl=x). |
| 413 | The CTRL must be connected, e.g. |
| 414 | with bsc.ctrl() as ctrl: |
| 415 | counters = RateCounters(ctrl) |
| 416 | counters.read() |
| 417 | ''' |
| 418 | for var in self.keys(): |
| 419 | self[var] = self.from_ctrl.get_int_var(var) |
| 420 | self.from_ctrl.dbg('Read counters:', self.str()) |
| 421 | return self |
| 422 | |
| 423 | def verify(self): |
| 424 | '''Read counters from CTRL and assert that they match the current counts''' |
| 425 | got_vals = self.copy() |
| 426 | got_vals.read() |
| 427 | got_vals.expect(self) |
| 428 | |
| 429 | def changed(self): |
| 430 | '''Read counters from CTRL, and return True if anyone is different now. |
| 431 | Store the difference in counts in self.diff (replace self.diff for |
| 432 | each changed() call). The counts in self are never modified.''' |
| 433 | self.diff = None |
| 434 | got_vals = self.copy() |
| 435 | got_vals.read() |
| 436 | if self != got_vals: |
| 437 | self.diff = got_vals |
| 438 | self.diff.subtract(self) |
| 439 | self.from_ctrl.dbg('Changed counters:', self.diff.str(skip_zero_vals=True)) |
| 440 | return True |
| 441 | return False |
| 442 | |
| 443 | def expect(self, expect_vals): |
| 444 | '''Iterate expect_vals and fail if any counter value differs from self. |
| 445 | expect_vals can be a RateCounters instance or a plain dict of CTRL |
| 446 | var as key and counter integer as value. |
| 447 | ''' |
| 448 | ok = 0 |
| 449 | errs = [] |
| 450 | for var, expect_val in expect_vals.items(): |
| 451 | got_val = self.get(var) |
| 452 | if got_val is None: |
| 453 | errs.append('expected {var} == {expect_val}, but no such value found'.format(**locals())) |
| 454 | continue |
| 455 | if got_val != expect_val: |
| 456 | errs.append('expected {var} == {expect_val}, but is {got_val}'.format(**locals())) |
| 457 | continue |
| 458 | ok += 1 |
| 459 | if errs: |
| 460 | self.from_ctrl.dbg('Expected rate counters:', self.str(vals=expect_vals)) |
| 461 | self.from_ctrl.dbg('Got rate counters:', self.str()) |
| 462 | raise RateCountersExn('%d of %d rate counters mismatch:' % (len(errs), len(errs) + ok), '\n| ' + '\n| '.join(errs)) |
| 463 | else: |
| 464 | self.from_ctrl.log('Verified %d rate counters' % ok) |
| 465 | self.from_ctrl.dbg('Verified %d rate counters:' % ok, expect_vals) |
| 466 | |
Neels Hofmeyr | 3531a19 | 2017-03-28 14:30:28 +0200 | [diff] [blame] | 467 | # vim: expandtab tabstop=4 shiftwidth=4 |