From 5354058c75200728caf4dcdcb2c87754b103d2ca Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Mon, 30 Nov 2020 22:04:26 +0100 Subject: [PATCH] osmo_ctrl.py: add RateCounters First user will be the upcoming handover_2G/handover.py test in I0b2671304165a1aaae2b386af46fbd8b098e3bd8. Change-Id: Id799b3bb81eb9c04d13c26ff611e40363920300e --- selftest/rate_ctrs_test/_prep.py | 16 ++ selftest/rate_ctrs_test/rate_ctrs_test.err | 0 selftest/rate_ctrs_test/rate_ctrs_test.ok | 155 ++++++++++++++ selftest/rate_ctrs_test/rate_ctrs_test.py | 56 +++++ src/osmo_gsm_tester/obj/osmo_ctrl.py | 226 +++++++++++++++++++++ 5 files changed, 453 insertions(+) create mode 100644 selftest/rate_ctrs_test/_prep.py create mode 100644 selftest/rate_ctrs_test/rate_ctrs_test.err create mode 100644 selftest/rate_ctrs_test/rate_ctrs_test.ok create mode 100755 selftest/rate_ctrs_test/rate_ctrs_test.py diff --git a/selftest/rate_ctrs_test/_prep.py b/selftest/rate_ctrs_test/_prep.py new file mode 100644 index 00000000..773f1902 --- /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 00000000..e69de29b 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 00000000..489f58f4 --- /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 00000000..935bd9de --- /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 644025f5..6c4ac876 100644 --- a/src/osmo_gsm_tester/obj/osmo_ctrl.py +++ b/src/osmo_gsm_tester/obj/osmo_ctrl.py @@ -238,4 +238,230 @@ class OsmoCtrl(log.Origin): 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