osmo_ctrl.py: add RateCounters
First user will be the upcoming handover_2G/handover.py test in
I0b2671304165a1aaae2b386af46fbd8b098e3bd8.
Change-Id: Id799b3bb81eb9c04d13c26ff611e40363920300e
diff --git a/src/osmo_gsm_tester/obj/osmo_ctrl.py b/src/osmo_gsm_tester/obj/osmo_ctrl.py
index 644025f..6c4ac87 100644
--- a/src/osmo_gsm_tester/obj/osmo_ctrl.py
+++ b/src/osmo_gsm_tester/obj/osmo_ctrl.py
@@ -238,4 +238,230 @@
def __exit__(self, *exc_info):
self.disconnect()
+class RateCountersExn(log.Error):
+ pass
+
+class RateCounters(dict):
+ '''Usage example:
+ counter_names = (
+ 'handover:completed',
+ 'handover:stopped',
+ 'handover:no_channel',
+ 'handover:timeout',
+ 'handover:failed',
+ 'handover:error',
+ )
+
+ # initialize the listing of CTRL vars of the counters to watch.
+ # First on the 'bsc' node:
+ # rate_ctr.abs.bsc.0.handover:completed
+ # rate_ctr.abs.bsc.0.handover:stopped
+ # ...
+ counters = RateCounters('bsc', counter_names, from_ctrl=bsc.ctrl)
+
+ # And also add counters for two 'bts' instances:
+ # rate_ctr.abs.bts.0.handover:completed
+ # rate_ctr.abs.bts.0.handover:stopped
+ # ...
+ # rate_ctr.abs.bts.1.handover:completed
+ # ...
+ counters.add(RateCounters('bts', counter_names, instances=(0, 1)))
+
+ # read initial counter values, from the bsc_ctrl, as set in
+ # counters.from_ctrl in the RateCounters() constructor above.
+ counters.read()
+
+ # Do some actions that should increment counters in the SUT
+ do_a_handover()
+
+ if approach_without_wait:
+ # increment the counters as expected
+ counters.inc('bts', 'handover:completed')
+
+ # read counters from CTRL again, and fail if they differ
+ counters.verify()
+
+ if approach_with_wait:
+ # you can wait for counters to change. counters.changed() does not
+ # modify counters' values, just reads values from CTRL and stores
+ # the changes in counters.diff.
+ wait(counters.changed, timeout=20)
+
+ # log which counters changed by how much, found in counters.diff
+ # after each counters.changed() call:
+ print(counters.diff.str(skip_zero_vals=True))
+
+ if check_all_vals:
+ # Assert all values:
+ expected_diff = counters.copy().clear()
+ expected_diff.inc('bts', 'handover:completed', instances=(0, 1))
+ counters.diff.expect(expected_diff)
+ else:
+ # Assert only some specific counters:
+ expected_diff = RateCounters()
+ expected_diff.inc('bts', 'handover:completed', instances=(0, 1))
+ counters.diff.expect(expected_diff)
+
+ # update counters to the last read values if desired
+ counters.add(counters.diff)
+ '''
+
+ def __init__(self, instance_names=(), counter_names=(), instances=0, kinds='abs', init_val=0, from_ctrl=None):
+ def init_cb(var):
+ self[var] = init_val
+ RateCounters.for_each(init_cb, instance_names, counter_names, instances, kinds, results=False)
+ self.from_ctrl = from_ctrl
+ self.diff = None
+
+ @staticmethod
+ def for_each(callback_func, instance_names, counter_names, instances=0, kinds='abs', results=True):
+ '''Call callback_func for a set of rate counter var names, mostly
+ called by more convenient functions. See inc() for a comprehensive
+ explanation.
+ '''
+ if type(instance_names) is str:
+ instance_names = (instance_names, )
+ if type(counter_names) is str:
+ counter_names = (counter_names, )
+ if type(kinds) is str:
+ kinds = (kinds, )
+ if type(instances) is int:
+ instances = (instances, )
+ if results is True:
+ results = RateCounters()
+ elif results is False:
+ results = None
+ for instance_name in instance_names:
+ for instance_nr in instances:
+ for counter_name in counter_names:
+ for kind in kinds:
+ var = 'rate_ctr.{kind}.{instance_name}.{instance_nr}.{counter_name}'.format(**locals())
+ result = callback_func(var)
+ if results is not None:
+ results[var] = result
+ return results
+
+ def __str__(self):
+ return self.str(', ', '')
+
+ def str(self, sep='\n| ', prefix='\n| ', vals=None, skip_zero_vals=False):
+ '''The 'vals' arg is useful to print a plain dict() of counter values like a RateCounters class.
+ By default print self.'''
+ if vals is None:
+ vals = self
+ return prefix + sep.join('%s = %d' % (var, val) for var, val in sorted(vals.items())
+ if (not skip_zero_vals) or (val != 0))
+
+ def inc(self, instance_names, counter_names, inc=1, instances=0, kinds='abs'):
+ '''Increment a set of counters.
+ inc('xyz', 'val') --> rate_ctr.abs.xyz.0.val += 1
+
+ inc('xyz', ('foo', 'bar')) --> rate_ctr.abs.xyz.0.foo += 1
+ rate_ctr.abs.xyz.0.bar += 1
+
+ inc(('xyz', 'pqr'), 'val') --> rate_ctr.abs.xyz.0.val += 1
+ rate_ctr.abs.pqr.0.val += 1
+
+ inc('xyz', 'val', instances=range(3))
+ --> rate_ctr.abs.xyz.0.val += 1
+ rate_ctr.abs.xyz.1.val += 1
+ rate_ctr.abs.xyz.2.val += 1
+ '''
+ def inc_cb(var):
+ val = self.get(var, 0)
+ val += inc
+ self[var] = val
+ return val
+ RateCounters.for_each(inc_cb, instance_names, counter_names, instances, kinds, results=False)
+ return self
+
+ def add(self, rate_counters):
+ '''Add the given values up to the values in self.
+ rate_counters can be a RateCounters instance or a plain dict of CTRL
+ var as key and counter integer as value.
+ '''
+ for var, add_val in rate_counters.items():
+ val = self.get(var, 0)
+ val += add_val
+ self[var] = val
+ return self
+
+ def subtract(self, rate_counters):
+ '''Same as add(), but subtract values from self instead.
+ Useful to verify counters relative to an arbitrary reference.'''
+ for var, subtract_val in rate_counters.items():
+ val = self.get(var, 0)
+ val -= subtract_val
+ self[var] = val
+ return self
+
+
+ def clear(self, val=0):
+ '''Set all counts to 0 (or a specific value)'''
+ for var in self.keys():
+ self[var] = val
+ return self
+
+ def copy(self):
+ '''Return a copy of all keys and values stored in self.'''
+ cpy = RateCounters(from_ctrl = self.from_ctrl)
+ cpy.update(self)
+ return cpy
+
+ def read(self):
+ '''Read all counters from the CTRL connection passed to RateCounters(from_ctrl=x).
+ The CTRL must be connected, e.g.
+ with bsc.ctrl() as ctrl:
+ counters = RateCounters(ctrl)
+ counters.read()
+ '''
+ for var in self.keys():
+ self[var] = self.from_ctrl.get_int_var(var)
+ self.from_ctrl.dbg('Read counters:', self.str())
+ return self
+
+ def verify(self):
+ '''Read counters from CTRL and assert that they match the current counts'''
+ got_vals = self.copy()
+ got_vals.read()
+ got_vals.expect(self)
+
+ def changed(self):
+ '''Read counters from CTRL, and return True if anyone is different now.
+ Store the difference in counts in self.diff (replace self.diff for
+ each changed() call). The counts in self are never modified.'''
+ self.diff = None
+ got_vals = self.copy()
+ got_vals.read()
+ if self != got_vals:
+ self.diff = got_vals
+ self.diff.subtract(self)
+ self.from_ctrl.dbg('Changed counters:', self.diff.str(skip_zero_vals=True))
+ return True
+ return False
+
+ def expect(self, expect_vals):
+ '''Iterate expect_vals and fail if any counter value differs from self.
+ expect_vals can be a RateCounters instance or a plain dict of CTRL
+ var as key and counter integer as value.
+ '''
+ ok = 0
+ errs = []
+ for var, expect_val in expect_vals.items():
+ got_val = self.get(var)
+ if got_val is None:
+ errs.append('expected {var} == {expect_val}, but no such value found'.format(**locals()))
+ continue
+ if got_val != expect_val:
+ errs.append('expected {var} == {expect_val}, but is {got_val}'.format(**locals()))
+ continue
+ ok += 1
+ if errs:
+ self.from_ctrl.dbg('Expected rate counters:', self.str(vals=expect_vals))
+ self.from_ctrl.dbg('Got rate counters:', self.str())
+ raise RateCountersExn('%d of %d rate counters mismatch:' % (len(errs), len(errs) + ok), '\n| ' + '\n| '.join(errs))
+ else:
+ self.from_ctrl.log('Verified %d rate counters' % ok)
+ self.from_ctrl.dbg('Verified %d rate counters:' % ok, expect_vals)
+
# vim: expandtab tabstop=4 shiftwidth=4