osmo_ctrl.py: add RateCounters
First user will be the upcoming handover_2G/handover.py test in
I0b2671304165a1aaae2b386af46fbd8b098e3bd8.
Change-Id: Id799b3bb81eb9c04d13c26ff611e40363920300e
diff --git a/selftest/rate_ctrs_test/_prep.py b/selftest/rate_ctrs_test/_prep.py
new file mode 100644
index 0000000..773f190
--- /dev/null
+++ b/selftest/rate_ctrs_test/_prep.py
@@ -0,0 +1,16 @@
+import sys, os
+
+script_dir = sys.path[0]
+top_dir = os.path.join(script_dir, '..', '..')
+src_dir = os.path.join(top_dir, 'src')
+
+# to find the osmo_gsm_tester py module
+sys.path.append(src_dir)
+
+from osmo_gsm_tester.core import log
+
+log.TestsTarget()
+log.set_all_levels(log.L_DBG)
+
+if '-v' in sys.argv:
+ log.style_change(trace=True)
diff --git a/selftest/rate_ctrs_test/rate_ctrs_test.err b/selftest/rate_ctrs_test/rate_ctrs_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/rate_ctrs_test/rate_ctrs_test.err
diff --git a/selftest/rate_ctrs_test/rate_ctrs_test.ok b/selftest/rate_ctrs_test/rate_ctrs_test.ok
new file mode 100644
index 0000000..489f58f
--- /dev/null
+++ b/selftest/rate_ctrs_test/rate_ctrs_test.ok
@@ -0,0 +1,155 @@
+- empty RateCounters()
+|
+- initialized RateCounters, single var
+| rate_ctr.abs.inst.0.var = 0
+- incremented inst.var
+| rate_ctr.abs.inst.0.var = 1
+- incremented inst.var again
+| rate_ctr.abs.inst.0.var = 2
+- incremented inst.var by 5
+| rate_ctr.abs.inst.0.var = 7
+- initialized RateCounters, two vars
+| rate_ctr.abs.inst.0.foo = 0
+| rate_ctr.abs.inst.0.var = 0
+- incremented foo and var
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 1
+- incremented var again
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 2
+- incremented foo by 5
+| rate_ctr.abs.inst.0.foo = 6
+| rate_ctr.abs.inst.0.var = 2
+- initialized RateCounters, two vars, three instances
+| rate_ctr.abs.inst.0.foo = 0
+| rate_ctr.abs.inst.0.var = 0
+| rate_ctr.abs.inst.1.foo = 0
+| rate_ctr.abs.inst.1.var = 0
+| rate_ctr.abs.inst.2.foo = 0
+| rate_ctr.abs.inst.2.var = 0
+- incremented foo and var on separate instances
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 0
+| rate_ctr.abs.inst.1.foo = 0
+| rate_ctr.abs.inst.1.var = 1
+| rate_ctr.abs.inst.2.foo = 0
+| rate_ctr.abs.inst.2.var = 0
+- incremented var on instance 2
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 0
+| rate_ctr.abs.inst.1.foo = 0
+| rate_ctr.abs.inst.1.var = 1
+| rate_ctr.abs.inst.2.foo = 0
+| rate_ctr.abs.inst.2.var = 1
+- incremented foo by 5 on instances 1,2
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 0
+| rate_ctr.abs.inst.1.foo = 5
+| rate_ctr.abs.inst.1.var = 1
+| rate_ctr.abs.inst.2.foo = 5
+| rate_ctr.abs.inst.2.var = 1
+- copy
+| rate_ctr.abs.inst.0.foo = 1
+| rate_ctr.abs.inst.0.var = 0
+| rate_ctr.abs.inst.1.foo = 5
+| rate_ctr.abs.inst.1.var = 1
+| rate_ctr.abs.inst.2.foo = 5
+| rate_ctr.abs.inst.2.var = 1
+- increment two vars by 100 on all three instances
+| rate_ctr.abs.inst.0.foo = 101
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 105
+| rate_ctr.abs.inst.1.var = 101
+| rate_ctr.abs.inst.2.foo = 105
+| rate_ctr.abs.inst.2.var = 101
+- subtract original copy
+| rate_ctr.abs.inst.0.foo = 100
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 100
+| rate_ctr.abs.inst.1.var = 100
+| rate_ctr.abs.inst.2.foo = 100
+| rate_ctr.abs.inst.2.var = 100
+- add original copy
+| rate_ctr.abs.inst.0.foo = 101
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 105
+| rate_ctr.abs.inst.1.var = 101
+| rate_ctr.abs.inst.2.foo = 105
+| rate_ctr.abs.inst.2.var = 101
+- increment types per_hour, per_day by 23
+| rate_ctr.abs.inst.0.foo = 101
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 105
+| rate_ctr.abs.inst.1.var = 101
+| rate_ctr.abs.inst.2.foo = 105
+| rate_ctr.abs.inst.2.var = 101
+| rate_ctr.per_day.inst.0.foo = 23
+| rate_ctr.per_day.inst.0.moo = 23
+| rate_ctr.per_day.inst.0.var = 23
+| rate_ctr.per_day.inst.1.foo = 23
+| rate_ctr.per_day.inst.1.moo = 23
+| rate_ctr.per_day.inst.1.var = 23
+| rate_ctr.per_day.inst.2.foo = 23
+| rate_ctr.per_day.inst.2.moo = 23
+| rate_ctr.per_day.inst.2.var = 23
+| rate_ctr.per_hour.inst.0.foo = 23
+| rate_ctr.per_hour.inst.0.moo = 23
+| rate_ctr.per_hour.inst.0.var = 23
+| rate_ctr.per_hour.inst.1.foo = 23
+| rate_ctr.per_hour.inst.1.moo = 23
+| rate_ctr.per_hour.inst.1.var = 23
+| rate_ctr.per_hour.inst.2.foo = 23
+| rate_ctr.per_hour.inst.2.moo = 23
+| rate_ctr.per_hour.inst.2.var = 23
+- copy
+| rate_ctr.abs.inst.0.foo = 101
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 105
+| rate_ctr.abs.inst.1.var = 101
+| rate_ctr.abs.inst.2.foo = 105
+| rate_ctr.abs.inst.2.var = 101
+| rate_ctr.per_day.inst.0.foo = 23
+| rate_ctr.per_day.inst.0.moo = 23
+| rate_ctr.per_day.inst.0.var = 23
+| rate_ctr.per_day.inst.1.foo = 23
+| rate_ctr.per_day.inst.1.moo = 23
+| rate_ctr.per_day.inst.1.var = 23
+| rate_ctr.per_day.inst.2.foo = 23
+| rate_ctr.per_day.inst.2.moo = 23
+| rate_ctr.per_day.inst.2.var = 23
+| rate_ctr.per_hour.inst.0.foo = 23
+| rate_ctr.per_hour.inst.0.moo = 23
+| rate_ctr.per_hour.inst.0.var = 23
+| rate_ctr.per_hour.inst.1.foo = 23
+| rate_ctr.per_hour.inst.1.moo = 23
+| rate_ctr.per_hour.inst.1.var = 23
+| rate_ctr.per_hour.inst.2.foo = 23
+| rate_ctr.per_hour.inst.2.moo = 23
+| rate_ctr.per_hour.inst.2.var = 23
+- match? True
+- increment foo
+| rate_ctr.abs.inst.0.foo = 102
+| rate_ctr.abs.inst.0.var = 100
+| rate_ctr.abs.inst.1.foo = 105
+| rate_ctr.abs.inst.1.var = 101
+| rate_ctr.abs.inst.2.foo = 105
+| rate_ctr.abs.inst.2.var = 101
+| rate_ctr.per_day.inst.0.foo = 23
+| rate_ctr.per_day.inst.0.moo = 23
+| rate_ctr.per_day.inst.0.var = 23
+| rate_ctr.per_day.inst.1.foo = 23
+| rate_ctr.per_day.inst.1.moo = 23
+| rate_ctr.per_day.inst.1.var = 23
+| rate_ctr.per_day.inst.2.foo = 23
+| rate_ctr.per_day.inst.2.moo = 23
+| rate_ctr.per_day.inst.2.var = 23
+| rate_ctr.per_hour.inst.0.foo = 23
+| rate_ctr.per_hour.inst.0.moo = 23
+| rate_ctr.per_hour.inst.0.var = 23
+| rate_ctr.per_hour.inst.1.foo = 23
+| rate_ctr.per_hour.inst.1.moo = 23
+| rate_ctr.per_hour.inst.1.var = 23
+| rate_ctr.per_hour.inst.2.foo = 23
+| rate_ctr.per_hour.inst.2.moo = 23
+| rate_ctr.per_hour.inst.2.var = 23
+- match? False
diff --git a/selftest/rate_ctrs_test/rate_ctrs_test.py b/selftest/rate_ctrs_test/rate_ctrs_test.py
new file mode 100755
index 0000000..935bd9d
--- /dev/null
+++ b/selftest/rate_ctrs_test/rate_ctrs_test.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+import _prep
+
+from osmo_gsm_tester.obj.osmo_ctrl import *
+
+rc = RateCounters()
+print('- empty RateCounters()' + rc.str())
+
+rc = RateCounters('inst', 'var')
+print('- initialized RateCounters, single var' + rc.str())
+rc.inc('inst', 'var')
+print('- incremented inst.var' + rc.str())
+rc.inc('inst', 'var')
+print('- incremented inst.var again' + rc.str())
+rc.inc('inst', 'var', 5)
+print('- incremented inst.var by 5' + rc.str())
+
+rc = RateCounters('inst', ('foo', 'var'))
+print('- initialized RateCounters, two vars' + rc.str())
+rc.inc('inst', ('foo', 'var'))
+print('- incremented foo and var' + rc.str())
+rc.inc('inst', 'var')
+print('- incremented var again' + rc.str())
+rc.inc('inst', 'foo', 5)
+print('- incremented foo by 5' + rc.str())
+
+rc = RateCounters('inst', ('foo', 'var'), instances=range(3))
+print('- initialized RateCounters, two vars, three instances' + rc.str())
+rc.inc('inst', 'foo', instances=0)
+rc.inc('inst', 'var', instances=1)
+print('- incremented foo and var on separate instances' + rc.str())
+rc.inc('inst', 'var', instances=2)
+print('- incremented var on instance 2' + rc.str())
+rc.inc('inst', 'foo', 5, instances=(1,2))
+print('- incremented foo by 5 on instances 1,2' + rc.str())
+
+rc_rel = rc.copy()
+print('- copy' + rc_rel.str())
+rc.inc('inst', ('foo', 'var'), 100, instances=range(3))
+print('- increment two vars by 100 on all three instances' + rc.str())
+rc.subtract(rc_rel)
+print('- subtract original copy' + rc.str())
+rc.add(rc_rel)
+print('- add original copy' + rc.str())
+
+rc.inc('inst', ('foo', 'var', 'moo'), 23, instances=range(3), kinds=('per_hour', 'per_day'))
+print('- increment types per_hour, per_day by 23' + rc.str())
+
+rc2 = rc.copy()
+print('- copy' + rc2.str())
+print('- match? ', (rc == rc2))
+rc2.inc('inst', 'foo')
+print('- increment foo' + rc2.str())
+print('- match? ', (rc == rc2))
+
+# vim: expandtab tabstop=4 shiftwidth=4
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