Change in osmo-gsm-tester[master]: Introduce scenario modifiers

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

Pau Espin Pedrol gerrit-no-reply at lists.osmocom.org
Thu Aug 23 15:51:15 UTC 2018


Pau Espin Pedrol has submitted this change and it was merged. ( https://gerrit.osmocom.org/10562 )

Change subject: Introduce scenario modifiers
......................................................................

Introduce scenario modifiers

Before this patch, scenarios were only used to select resources with
specific attributes. This commit introduces "modifiers" in scenarios,
which allows setting or modifing config attributes of resources once
they have been reserved. This way same test can be run selecting same
resources but modifying its configuration, allowing for instance running
different number of TRX, different timeslot configuration, etc.

Modifiers are described by placing a "modifiers" dictionary in any
scenario file, similar to the current "resources" one used to select
requird resources. The "modifiers" dictionary is overlaid on top of the
"resources" one resulting from combining all the "resources" dictionary
of all scenario files.

Change-Id: If8c422c67d9a971d9ce2c72594f55cde2db7550d
---
A example/scenarios/mod-bts0-numtrx2.conf
M selftest/resource_test.ok
M selftest/resource_test.py
M selftest/suite_test.ok
M selftest/suite_test.ok.ign
M selftest/suite_test.py
M src/osmo_gsm_tester/resource.py
M src/osmo_gsm_tester/suite.py
8 files changed, 226 insertions(+), 12 deletions(-)

Approvals:
  Jenkins Builder: Verified
  Neels Hofmeyr: Looks good to me, approved



diff --git a/example/scenarios/mod-bts0-numtrx2.conf b/example/scenarios/mod-bts0-numtrx2.conf
new file mode 100644
index 0000000..eb24fd3
--- /dev/null
+++ b/example/scenarios/mod-bts0-numtrx2.conf
@@ -0,0 +1,3 @@
+modifiers:
+  bts:
+  - num_trx: 2
diff --git a/selftest/resource_test.ok b/selftest/resource_test.ok
index 884c6b3..3ec922b 100644
--- a/selftest/resource_test.ok
+++ b/selftest/resource_test.ok
@@ -222,6 +222,54 @@
 
 ~~~ end: currently reserved
 
+~~~ with modifiers:
+resources(testowner)={'arfcn': [{'_hash': 'e620569450f8259b3f0212ec19c285dd07df063c',
+            '_reserved_by': 'testowner-123-1490837279',
+            'arfcn': '512',
+            'band': 'GSM-1800'},
+           {'_hash': '022621e513c5a5bf33b77430a1e9c886be676fa1',
+            '_reserved_by': 'testowner-123-1490837279',
+            'arfcn': '514',
+            'band': 'GSM-1800'}],
+ 'bts': [{'_hash': 'd2aa7c1124943de352351b650ca0c751784da6b6',
+          '_reserved_by': 'testowner-123-1490837279',
+          'addr': '10.42.42.114',
+          'band': 'GSM-1800',
+          'ciphers': ['a5_0', 'a5_1'],
+          'direct_pcu': 'True',
+          'ipa_unit_id': '1',
+          'label': 'sysmoBTS 1002',
+          'type': 'osmo-bts-sysmo'},
+         {'_hash': '6a9c9fbd364e1563a5b9f0826030a7888fd19575',
+          '_reserved_by': 'testowner-123-1490837279',
+          'addr': '10.42.42.50',
+          'band': 'GSM-1800',
+          'ciphers': ['a5_0', 'a5_1'],
+          'ipa_unit_id': '6',
+          'label': 'Ettus B200',
+          'launch_trx': 'True',
+          'num_trx': 2,
+          'type': 'osmo-bts-trx'}],
+ 'ip_address': [{'_hash': 'fd103b22c7cf2480d609150e06f4bbd92ac78d8c',
+                 '_reserved_by': 'testowner-123-1490837279',
+                 'addr': '10.42.42.2'}],
+ 'modem': [{'_hash': '0b538cb6ad799fbd7c2953fd3b4463a76c7cc9c0',
+            '_reserved_by': 'testowner-123-1490837279',
+            'auth_algo': 'comp128v1',
+            'ciphers': ['a5_0', 'a5_1'],
+            'imsi': '901700000009031',
+            'ki': '80A37E6FDEA931EAC92FFA5F671EFEAD',
+            'label': 'sierra_1',
+            'path': '/sierra_1'},
+           {'_hash': '3a6e7747dfe7dfdf817bd3351031bd08051605c3',
+            '_reserved_by': 'testowner-123-1490837279',
+            'auth_algo': 'comp128v1',
+            'ciphers': ['a5_0', 'a5_1'],
+            'imsi': '901700000009029',
+            'ki': '00969E283349D354A8239E877F2E0866',
+            'label': 'sierra_2',
+            'path': '/sierra_2'}]}
+~~~ end: with modifiers:
 ~~~ currently reserved:
 {}
 
diff --git a/selftest/resource_test.py b/selftest/resource_test.py
index 52d8e47..cdfe021 100755
--- a/selftest/resource_test.py
+++ b/selftest/resource_test.py
@@ -77,16 +77,22 @@
        'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
        'modem': [ { 'times': 2 , 'ciphers': ['a5_0', 'a5_1']} ],
      }
-
+modifiers = {
+    'bts': [ {}, {'num_trx': 2 }],
+}
 origin = log.Origin(None, 'testowner')
 
-resources = pool.reserve(origin, config.replicate_times(want))
+resources = pool.reserve(origin, config.replicate_times(want), config.replicate_times(modifiers))
 
 print('~~~ currently reserved:')
 with open(rrfile, 'r') as f:
     print(f.read())
 print('~~~ end: currently reserved\n')
 
+print('~~~ with modifiers:')
+print(repr(resources))
+print('~~~ end: with modifiers:')
+
 resources.free()
 
 print('~~~ currently reserved:')
diff --git a/selftest/suite_test.ok b/selftest/suite_test.ok
index 79c37cc..b792b57 100644
--- a/selftest/suite_test.ok
+++ b/selftest/suite_test.ok
@@ -34,6 +34,8 @@
 tst test_suite: reserving resources in [PATH]/selftest/suite_test/test_work/state_dir ...
 tst test_suite: DBG: {combining='resources'}
 tst {combining_scenarios='resources'}: DBG: {definition_conf={bts=[{'label': 'sysmoCell 5000'}, {'label': 'sysmoCell 5000'}, {'type': 'sysmo'}], ip_address=[{}], modem=[{}, {}]}}  [test_suite↪{combining_scenarios='resources'}]
+tst test_suite: DBG: {combining='modifiers'}
+tst {combining_scenarios='modifiers'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='modifiers'}]
 tst test_suite: Reserving 3 x bts (candidates: 6)
 tst test_suite: DBG: Picked - _hash: 076ff06a4b719e61779492d3fb99f42a6635bb72
   addr: 10.42.42.53
@@ -177,6 +179,9 @@
 tst test_suite: DBG: {combining='resources'}  [suite.py:[LINENR]]
 tst {combining_scenarios='resources'}: DBG: {definition_conf={bts=[{'label': 'sysmoCell 5000'}, {'label': 'sysmoCell 5000'}, {'type': 'sysmo'}], ip_address=[{}], modem=[{}, {}]}}  [test_suite↪{combining_scenarios='resources'}]  [suite.py:[LINENR]]
 tst {combining_scenarios='resources', scenario='foo'}: [RESOURCE_DICT]
+tst test_suite: DBG: {combining='modifiers'}  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='modifiers'}]  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers', scenario='foo'}: DBG: {conf={}, scenario='foo'}  [test_suite↪{combining_scenarios='modifiers', scenario='foo'}]  [suite.py:[LINENR]]
 tst test_suite: Reserving 3 x bts (candidates: 6)  [resource.py:[LINENR]]
 tst test_suite: DBG: Picked - _hash: 076ff06a4b719e61779492d3fb99f42a6635bb72
   addr: 10.42.42.53
@@ -255,6 +260,9 @@
 tst test_suite: DBG: {combining='resources'}  [suite.py:[LINENR]]
 tst {combining_scenarios='resources'}: DBG: {definition_conf={bts=[{'label': 'sysmoCell 5000'}, {'label': 'sysmoCell 5000'}, {'type': 'sysmo'}], ip_address=[{}], modem=[{}, {}]}}  [test_suite↪{combining_scenarios='resources'}]  [suite.py:[LINENR]]
 tst {combining_scenarios='resources', scenario='foo'}: [RESOURCE_DICT]
+tst test_suite: DBG: {combining='modifiers'}  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='modifiers'}]  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers', scenario='foo'}: DBG: {conf={}, scenario='foo'}  [test_suite↪{combining_scenarios='modifiers', scenario='foo'}]  [suite.py:[LINENR]]
 tst test_suite: Reserving 3 x bts (candidates: 6)  [resource.py:[LINENR]]
 tst test_suite: DBG: Picked - _hash: 076ff06a4b719e61779492d3fb99f42a6635bb72
   addr: 10.42.42.53
@@ -322,5 +330,128 @@
     skip: test_error.py
     skip: test_fail.py
     skip: test_fail_raise.py
+- test with scenario and modifiers
+cnf ResourcesPool: DBG: Found config file resources.conf as [PATH]/selftest/suite_test/resources.conf in ./suite_test which is [PATH]/selftest/suite_test  [config.py:[LINENR]]
+cnf ResourcesPool: DBG: Found path state_dir as [PATH]/selftest/suite_test/test_work/state_dir  [config.py:[LINENR]]
+tst test_suite: reserving resources in [PATH]/selftest/suite_test/test_work/state_dir ...  [suite.py:[LINENR]]
+tst test_suite: DBG: {combining='resources'}  [suite.py:[LINENR]]
+tst {combining_scenarios='resources'}: DBG: {definition_conf={bts=[{'label': 'sysmoCell 5000'}, {'label': 'sysmoCell 5000'}, {'type': 'sysmo'}], ip_address=[{}], modem=[{}, {}]}}  [test_suite↪{combining_scenarios='resources'}]  [suite.py:[LINENR]]
+tst {combining_scenarios='resources', scenario='foo'}: [RESOURCE_DICT]
+tst test_suite: DBG: {combining='modifiers'}  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='modifiers'}]  [suite.py:[LINENR]]
+tst {combining_scenarios='modifiers', scenario='foo'}: DBG: {conf={bts=[{'trx_list': [{'nominal_power': '20'}, {'nominal_power': '20'}]}, {'trx_list': [{'nominal_power': '20'}, {'nominal_power': '20'}]}, {'type': 'sysmo'}]}, scenario='foo'}  [test_suite↪{combining_scenarios='modifiers', scenario='foo'}]  [suite.py:[LINENR]]
+tst test_suite: Reserving 3 x bts (candidates: 6)  [resource.py:[LINENR]]
+tst test_suite: DBG: Picked - _hash: 076ff06a4b719e61779492d3fb99f42a6635bb72
+  addr: 10.42.42.53
+  band: GSM-1800
+  ipa_unit_id: '7'
+  label: sysmoCell 5000
+  trx_list:
+  - max_power_red: '3'
+    nominal_power: '10'
+  - max_power_red: '0'
+    nominal_power: '12'
+  trx_remote_ip: 10.42.42.112
+  type: osmo-bts-trx
+- _hash: 9eaa928b04ce04b19dbae972f9bfc3eea6f5e249
+  addr: 10.42.42.53
+  band: GSM-1800
+  ipa_unit_id: '7'
+  label: sysmoCell 5000
+  trx_list:
+  - nominal_power: '10'
+  - max_power_red: '1'
+    nominal_power: '12'
+  trx_remote_ip: 10.42.42.112
+  type: osmo-bts-trx
+- _hash: 07d9c8aaa940b674efcbbabdd69f58a6ce4e94f9
+  addr: 10.42.42.114
+  band: GSM-1800
+  ipa_unit_id: '1'
+  label: sysmoBTS 1002
+  type: sysmo
+  [resource.py:[LINENR]]
+tst test_suite: Reserving 1 x ip_address (candidates: 3)  [resource.py:[LINENR]]
+tst test_suite: DBG: Picked - _hash: cde1debf28f07f94f92c761b4b7c6bf35785ced4
+  addr: 10.42.42.1
+  [resource.py:[LINENR]]
+tst test_suite: Reserving 2 x modem (candidates: 16)  [resource.py:[LINENR]]
+tst test_suite: DBG: Picked - _hash: 19c69e45aa090fb511446bd00797690aa82ff52f
+  imsi: '901700000007801'
+  ki: D620F48487B1B782DA55DF6717F08FF9
+  label: m7801
+  path: /wavecom_0
+- _hash: e1a46516a1fb493b2617ab14fc1693a9a45ec254
+  imsi: '901700000007802'
+  ki: 47FDB2D55CE6A10A85ABDAD034A5B7B3
+  label: m7802
+  path: /wavecom_1
+  [resource.py:[LINENR]]
+resources(test_suite)={'bts': [{'_hash': '076ff06a4b719e61779492d3fb99f42a6635bb72',
+          '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+          'addr': '10.42.42.53',
+          'band': 'GSM-1800',
+          'ipa_unit_id': '7',
+          'label': 'sysmoCell 5000',
+          'trx_list': [{'max_power_red': '3', 'nominal_power': '20'},
+                       {'max_power_red': '0', 'nominal_power': '20'}],
+          'trx_remote_ip': '10.42.42.112',
+          'type': 'osmo-bts-trx'},
+         {'_hash': '9eaa928b04ce04b19dbae972f9bfc3eea6f5e249',
+          '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+          'addr': '10.42.42.53',
+          'band': 'GSM-1800',
+          'ipa_unit_id': '7',
+          'label': 'sysmoCell 5000',
+          'trx_list': [{'nominal_power': '20'},
+                       {'max_power_red': '1', 'nominal_power': '20'}],
+          'trx_remote_ip': '10.42.42.112',
+          'type': 'osmo-bts-trx'},
+         {'_hash': '07d9c8aaa940b674efcbbabdd69f58a6ce4e94f9',
+          '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+          'addr': '10.42.42.114',
+          'band': 'GSM-1800',
+          'ipa_unit_id': '1',
+          'label': 'sysmoBTS 1002',
+          'type': 'sysmo'}],
+ 'ip_address': [{'_hash': 'cde1debf28f07f94f92c761b4b7c6bf35785ced4',
+                 '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+                 'addr': '10.42.42.1'}],
+ 'modem': [{'_hash': '19c69e45aa090fb511446bd00797690aa82ff52f',
+            '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+            'imsi': '901700000007801',
+            'ki': 'D620F48487B1B782DA55DF6717F08FF9',
+            'label': 'm7801',
+            'path': '/wavecom_0'},
+           {'_hash': 'e1a46516a1fb493b2617ab14fc1693a9a45ec254',
+            '_reserved_by': 'test_suite-[ID_NUM]-[ID_NUM]',
+            'imsi': '901700000007802',
+            'ki': '47FDB2D55CE6A10A85ABDAD034A5B7B3',
+            'label': 'm7802',
+            'path': '/wavecom_1'}]}
+
+---------------------------------------------------------------------
+trial test_suite
+---------------------------------------------------------------------
+
+----------------------------------------------
+trial test_suite hello_world.py
+----------------------------------------------
+tst hello_world.py:[LINENR]: hello world  [test_suite↪hello_world.py:[LINENR]]  [hello_world.py:[LINENR]]
+tst hello_world.py:[LINENR]: I am 'test_suite' / 'hello_world.py:[LINENR]'  [test_suite↪hello_world.py:[LINENR]]  [hello_world.py:[LINENR]]
+tst hello_world.py:[LINENR]: one  [test_suite↪hello_world.py:[LINENR]]  [hello_world.py:[LINENR]]
+tst hello_world.py:[LINENR]: two  [test_suite↪hello_world.py:[LINENR]]  [hello_world.py:[LINENR]]
+tst hello_world.py:[LINENR]: three  [test_suite↪hello_world.py:[LINENR]]  [hello_world.py:[LINENR]]
+tst hello_world.py:[LINENR] Test passed (N.N sec)  [test_suite↪hello_world.py]  [test.py:[LINENR]]
+---------------------------------------------------------------------
+trial test_suite PASS
+---------------------------------------------------------------------
+PASS: test_suite (pass: 1, skip: 5)
+    pass: hello_world.py (N.N sec)
+    skip: mo_mt_sms.py
+    skip: mo_sms.py
+    skip: test_error.py
+    skip: test_fail.py
+    skip: test_fail_raise.py
 
 - graceful exit.
diff --git a/selftest/suite_test.ok.ign b/selftest/suite_test.ok.ign
index 49bd9eb..9bd168f 100644
--- a/selftest/suite_test.ok.ign
+++ b/selftest/suite_test.ok.ign
@@ -2,3 +2,4 @@
 \.py:[0-9]*	.py:[LINENR]
 \([0-9.]+ sec\)	(N.N sec)
 {combining_scenarios='resources', scenario='foo'}:.*	{combining_scenarios='resources', scenario='foo'}: [RESOURCE_DICT]
+test_suite-[0-9]*-[0-9]*	test_suite-[ID_NUM]-[ID_NUM]
diff --git a/selftest/suite_test.py b/selftest/suite_test.py
index a8b0f37..db19ccc 100755
--- a/selftest/suite_test.py
+++ b/selftest/suite_test.py
@@ -58,5 +58,16 @@
 results = s.run_tests('hello_world.py')
 print(report.suite_to_text(s))
 
+print('- test with scenario and modifiers')
+trial = log.Origin(log.C_TST, 'trial')
+scenario = config.Scenario('foo', 'bar')
+scenario['resources'] = { 'bts': [{ 'times': '2', 'type': 'osmo-bts-trx', 'trx_list': [{'nominal_power': '10'}, {'nominal_power': '12'}]}, {'type': 'sysmo'}] }
+scenario['modifiers'] = { 'bts': [{ 'times': '2', 'trx_list': [{'nominal_power': '20'}, {'nominal_power': '20'}]}, {'type': 'sysmo'}] }
+s = suite.SuiteRun(trial, 'test_suite', s_def, [scenario])
+s.reserve_resources()
+print(repr(s.reserved_resources))
+results = s.run_tests('hello_world.py')
+print(report.suite_to_text(s))
+
 print('\n- graceful exit.')
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py
index dca8090..5204b61 100644
--- a/src/osmo_gsm_tester/resource.py
+++ b/src/osmo_gsm_tester/resource.py
@@ -87,7 +87,8 @@
 
 CONF_SCHEMA = util.dict_add(
     { 'defaults.timeout': schema.STR },
-    dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
+    dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
+    dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
 
 KNOWN_BTS_TYPES = {
         'osmo-bts-sysmo': bts_sysmo.SysmoBts,
@@ -113,7 +114,7 @@
         self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
         self.all_resources.set_hashes()
 
-    def reserve(self, origin, want):
+    def reserve(self, origin, want, modifiers):
         '''
         attempt to reserve the resources specified in the dict 'want' for
         'origin'. Obtain a lock on the resources lock dir, verify that all
@@ -125,7 +126,11 @@
 
         'origin' should be an Origin() instance.
 
-        'want' is a dict matching RESOURCES_SCHEMA.
+        'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
+        reserve.
+
+        'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
+        of 'want'.
 
         If an entry has no attribute set, any of the resources may be
         reserved without further limitations.
@@ -142,6 +147,7 @@
          }
         '''
         schema.validate(want, RESOURCES_SCHEMA)
+        schema.validate(modifiers, RESOURCES_SCHEMA)
 
         origin_id = origin.origin_id()
 
@@ -156,7 +162,7 @@
             config.write(rrfile_path, reserved)
 
             self.remember_to_free(to_be_reserved)
-            return ReservedResources(self, origin, to_be_reserved)
+            return ReservedResources(self, origin, to_be_reserved, modifiers)
 
     def free(self, origin, to_be_freed):
         log.ctx(origin)
@@ -491,10 +497,12 @@
     dependencies from so far unused (but reserved) resource.
     '''
 
-    def __init__(self, resources_pool, origin, reserved):
+    def __init__(self, resources_pool, origin, reserved, modifiers):
         self.resources_pool = resources_pool
         self.origin = origin
-        self.reserved = reserved
+        self.reserved_original = reserved
+        self.reserved = copy.deepcopy(self.reserved_original)
+        config.overlay(self.reserved, modifiers)
 
     def __repr__(self):
         return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
@@ -551,9 +559,9 @@
                 item.pop(USED_KEY, None)
 
     def free(self):
-        if self.reserved:
-            self.resources_pool.free(self.origin, self.reserved)
-        self.reserved = None
+        if self.reserved_original:
+            self.resources_pool.free(self.origin, self.reserved_original)
+        self.reserved_original = None
 
     def counts(self):
         counts = {}
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index c712a75..fa86f96 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -71,6 +71,7 @@
         self.objects_to_clean_up = None
         self.test_import_modules_to_clean_up = []
         self._resource_requirements = None
+        self._resource_modifiers = None
         self._config = None
         self._processes = None
         self._run_dir = None
@@ -154,6 +155,11 @@
             self._resource_requirements = self.combined('resources')
         return self._resource_requirements
 
+    def resource_modifiers(self):
+        if self._resource_modifiers is None:
+            self._resource_modifiers = self.combined('modifiers')
+        return self._resource_modifiers
+
     def config(self):
         if self._config is None:
             self._config = self.combined('config')
@@ -163,7 +169,7 @@
         if self.reserved_resources:
             raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
         self.log('reserving resources in', self.resources_pool.state_dir, '...')
-        self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
+        self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements(), self.resource_modifiers())
 
     def run_tests(self, names=None):
         suite_libdir = os.path.join(self.definition.suite_dir, 'lib')

-- 
To view, visit https://gerrit.osmocom.org/10562
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-gsm-tester
Gerrit-Branch: master
Gerrit-MessageType: merged
Gerrit-Change-Id: If8c422c67d9a971d9ce2c72594f55cde2db7550d
Gerrit-Change-Number: 10562
Gerrit-PatchSet: 2
Gerrit-Owner: Pau Espin Pedrol <pespin at sysmocom.de>
Gerrit-Reviewer: Jenkins Builder (1000002)
Gerrit-Reviewer: Neels Hofmeyr <nhofmeyr at sysmocom.de>
Gerrit-Reviewer: Pau Espin Pedrol <pespin at sysmocom.de>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20180823/f57804e0/attachment.htm>


More information about the gerrit-log mailing list