blob: 55f81b16add4e0a357608a64908ac22120f1da17 [file] [log] [blame]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02001# osmo_gsm_tester: test suite
2#
3# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Neels Hofmeyr <neels@hofmeyr.de>
6#
7# This program is free software: you can redistribute it and/or modify
Harald Welte27205342017-06-03 09:51:45 +02008# it under the terms of the GNU General Public License as
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02009# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Harald Welte27205342017-06-03 09:51:45 +020015# GNU General Public License for more details.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020016#
Harald Welte27205342017-06-03 09:51:45 +020017# You should have received a copy of the GNU General Public License
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020018# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20import os
Neels Hofmeyr3531a192017-03-28 14:30:28 +020021import sys
22import time
Your Name44af3412017-04-13 03:11:59 +020023import copy
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020024import traceback
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +020025import pprint
Neels Hofmeyr798e5922017-05-18 15:24:02 +020026from . import config, log, template, util, resource, schema, ofono_client, event_loop
27from . import osmo_nitb
28from . import osmo_hlr, osmo_mgcpgw, osmo_msc, osmo_bsc
Neels Hofmeyr3531a192017-03-28 14:30:28 +020029from . import test
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020030
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +020031class Timeout(Exception):
32 pass
33
Neels Hofmeyr3531a192017-03-28 14:30:28 +020034class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020035 '''A test suite reserves resources for a number of tests.
36 Each test requires a specific number of modems, BTSs etc., which are
37 reserved beforehand by a test suite. This way several test suites can be
38 scheduled dynamically without resource conflicts arising halfway through
39 the tests.'''
40
41 CONF_FILENAME = 'suite.conf'
42
Neels Hofmeyr3531a192017-03-28 14:30:28 +020043 CONF_SCHEMA = util.dict_add(
44 {
45 'defaults.timeout': schema.STR,
46 },
47 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
48 )
49
50
51 def __init__(self, suite_dir):
52 self.set_log_category(log.C_CNF)
53 self.suite_dir = suite_dir
54 self.set_name(os.path.basename(self.suite_dir))
55 self.read_conf()
56
57 def read_conf(self):
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020058 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
59 if not os.path.isdir(self.suite_dir):
60 raise RuntimeError('No such directory: %r' % self.suite_dir)
61 self.conf = config.read(os.path.join(self.suite_dir,
62 SuiteDefinition.CONF_FILENAME),
63 SuiteDefinition.CONF_SCHEMA)
64 self.load_test_basenames()
Neels Hofmeyr3531a192017-03-28 14:30:28 +020065
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020066 def load_test_basenames(self):
67 self.test_basenames = []
68 for basename in sorted(os.listdir(self.suite_dir)):
69 if not basename.endswith('.py'):
70 continue
71 self.test_basenames.append(basename)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020072
Neels Hofmeyr3531a192017-03-28 14:30:28 +020073
Neels Hofmeyr3531a192017-03-28 14:30:28 +020074class Test(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020075 UNKNOWN = 'UNKNOWN'
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020076 SKIP = 'skip'
77 PASS = 'pass'
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020078 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020080 def __init__(self, suite_run, test_basename):
81 self.suite_run = suite_run
Neels Hofmeyr3531a192017-03-28 14:30:28 +020082 self.basename = test_basename
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020083 self.path = os.path.join(self.suite_run.definition.suite_dir, self.basename)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020084 super().__init__(self.path)
85 self.set_name(self.basename)
86 self.set_log_category(log.C_TST)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020087 self.status = Test.UNKNOWN
88 self.start_timestamp = 0
89 self.duration = 0
90 self.fail_type = None
91 self.fail_message = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020093 def run(self):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020094 try:
95 with self:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020096 log.large_separator(self.suite_run.trial.name(), self.suite_run.name(), self.name(), sublevel=3)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020097 self.status = Test.UNKNOWN
98 self.start_timestamp = time.time()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020099 test.setup(self.suite_run, self, ofono_client, sys.modules[__name__], event_loop)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100 with self.redirect_stdout():
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200101 util.run_python_file('%s.%s' % (self.suite_run.definition.name(), self.basename),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200102 self.path)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200103 if self.status == Test.UNKNOWN:
104 self.set_pass()
105 except Exception as e:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200106 if hasattr(e, 'msg'):
107 msg = e.msg
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200108 else:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200109 msg = str(e)
110 if isinstance(e, AssertionError):
111 # AssertionError lacks further information on what was
112 # asserted. Find the line where the code asserted:
113 msg += log.get_src_from_tb(sys.exc_info()[2])
114 # add source file information to failure report
115 if hasattr(e, 'origins'):
116 msg += ' [%s]' % e.origins
117 tb_str = traceback.format_exc()
118 if isinstance(e, resource.NoResourceExn):
119 tb_str += self.suite_run.resource_status_str()
120 self.set_fail(type(e).__name__, msg, tb_str)
121 except BaseException as e:
122 # when the program is aborted by a signal (like Ctrl-C), escalate to abort all.
123 self.err('TEST RUN ABORTED: %s' % type(e).__name__)
124 raise
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125
126 def name(self):
127 l = log.get_line_for_src(self.path)
128 if l is not None:
129 return '%s:%s' % (self._name, l)
130 return super().name()
131
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200132 def set_fail(self, fail_type, fail_message, tb_str=None):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200133 self.status = Test.FAIL
134 self.duration = time.time() - self.start_timestamp
135 self.fail_type = fail_type
136 self.fail_message = fail_message
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200137
138 if tb_str is None:
139 # populate an exception-less call to set_fail() with traceback info
140 tb_str = ''.join(traceback.format_stack()[:-1])
141
142 self.fail_tb = tb_str
143 self.err('%s: %s' % (self.fail_type, self.fail_message))
144 if self.fail_tb:
145 self.trace(self.fail_tb)
146 self.log('Test FAILED (%.1f sec)' % self.duration)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200147
148 def set_pass(self):
149 self.status = Test.PASS
150 self.duration = time.time() - self.start_timestamp
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200151 self.log('Test passed (%.1f sec)' % self.duration)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200152
153 def set_skip(self):
154 self.status = Test.SKIP
155 self.duration = 0
156
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157class SuiteRun(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200158 UNKNOWN = 'UNKNOWN'
159 PASS = 'PASS'
160 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161
162 trial = None
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200163 status = None
Neels Hofmeyrf8e61862017-06-06 23:08:07 +0200164 start_timestamp = None
165 duration = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200166 resources_pool = None
167 reserved_resources = None
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200168 objects_to_clean_up = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200169 _resource_requirements = None
170 _config = None
171 _processes = None
172
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200173 def __init__(self, trial, suite_scenario_str, suite_definition, scenarios=[]):
174 self.trial = trial
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175 self.definition = suite_definition
176 self.scenarios = scenarios
Your Name44af3412017-04-13 03:11:59 +0200177 self.set_name(suite_scenario_str)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178 self.set_log_category(log.C_TST)
179 self.resources_pool = resource.ResourcesPool()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200180 self.status = SuiteRun.UNKNOWN
181 self.load_tests()
182
183 def load_tests(self):
184 self.tests = []
185 for test_basename in self.definition.test_basenames:
186 self.tests.append(Test(self, test_basename))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200187
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200188 def register_for_cleanup(self, *obj):
189 assert all([hasattr(o, 'cleanup') for o in obj])
190 self.objects_to_clean_up = self.objects_to_clean_up or []
191 self.objects_to_clean_up.extend(obj)
192
193 def objects_cleanup(self):
194 while self.objects_to_clean_up:
195 obj = self.objects_to_clean_up.pop()
196 obj.cleanup()
197
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200198 def mark_start(self):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200199 self.start_timestamp = time.time()
200 self.duration = 0
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200201 self.status = SuiteRun.UNKNOWN
202
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200203 def combined(self, conf_name):
Your Name44af3412017-04-13 03:11:59 +0200204 self.dbg(combining=conf_name)
205 with log.Origin(combining_scenarios=conf_name):
206 combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
207 self.dbg(definition_conf=combination)
208 for scenario in self.scenarios:
209 with scenario:
210 c = scenario.get(conf_name)
211 self.dbg(scenario=scenario.name(), conf=c)
212 if c is None:
213 continue
214 config.combine(combination, c)
215 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216
217 def resource_requirements(self):
218 if self._resource_requirements is None:
219 self._resource_requirements = self.combined('resources')
220 return self._resource_requirements
221
222 def config(self):
223 if self._config is None:
224 self._config = self.combined('config')
225 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200226
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227 def reserve_resources(self):
228 if self.reserved_resources:
229 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
Neels Hofmeyr7e2e8f12017-05-14 03:37:13 +0200230 self.log('reserving resources in', self.resources_pool.state_dir, '...')
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200231 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 def run_tests(self, names=None):
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200234 try:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200235 with self:
236 log.large_separator(self.trial.name(), self.name(), sublevel=2)
237 self.mark_start()
238 event_loop.register_poll_func(self.poll)
239 if not self.reserved_resources:
240 self.reserve_resources()
241 for test in self.tests:
242 if names and not test.name() in names:
243 test.set_skip()
244 continue
245 test.run()
246 except Exception:
247 self.log_exn()
248 except BaseException as e:
249 # when the program is aborted by a signal (like Ctrl-C), escalate to abort all.
250 self.err('SUITE RUN ABORTED: %s' % type(e).__name__)
251 raise
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200252 finally:
253 # if sys.exit() called from signal handler (e.g. SIGINT), SystemExit
254 # base exception is raised. Make sure to stop processes in this
255 # finally section. Resources are automatically freed with 'atexit'.
256 self.stop_processes()
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200257 self.objects_cleanup()
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200258 self.free_resources()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200259 event_loop.unregister_poll_func(self.poll)
260 self.duration = time.time() - self.start_timestamp
261
262 passed, skipped, failed = self.count_test_results()
263 # if no tests ran, count it as failure
264 if passed and not failed:
265 self.status = SuiteRun.PASS
266 else:
267 self.status = SuiteRun.FAIL
268
269 log.large_separator(self.trial.name(), self.name(), self.status, sublevel=2, space_above=False)
270
271 def passed(self):
272 return self.status == SuiteRun.PASS
273
274 def count_test_results(self):
275 passed = 0
276 skipped = 0
277 failed = 0
278 for test in self.tests:
279 if test.status == Test.PASS:
280 passed += 1
281 elif test.status == Test.FAIL:
282 failed += 1
283 else:
284 skipped += 1
285 return (passed, skipped, failed)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200286
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200287 def remember_to_stop(self, process):
288 if self._processes is None:
289 self._processes = []
Pau Espin Pedrolecf10792017-05-08 16:56:38 +0200290 self._processes.insert(0, process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200292 def stop_processes(self):
293 if not self._processes:
294 return
295 for process in self._processes:
296 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200297
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200298 def free_resources(self):
299 if self.reserved_resources is None:
300 return
301 self.reserved_resources.free()
302
Neels Hofmeyrb902b292017-06-06 21:52:03 +0200303 def ip_address(self, specifics=None):
304 return self.reserved_resources.get(resource.R_IP_ADDRESS, specifics=specifics)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200305
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200306 def nitb(self, ip_address=None):
307 if ip_address is None:
308 ip_address = self.ip_address()
309 return osmo_nitb.OsmoNitb(self, ip_address)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200310
Neels Hofmeyr798e5922017-05-18 15:24:02 +0200311 def hlr(self, ip_address=None):
312 if ip_address is None:
313 ip_address = self.ip_address()
314 return osmo_hlr.OsmoHlr(self, ip_address)
315
316 def mgcpgw(self, ip_address=None, bts_ip=None):
317 if ip_address is None:
318 ip_address = self.ip_address()
319 return osmo_mgcpgw.OsmoMgcpgw(self, ip_address, bts_ip)
320
321 def msc(self, hlr, mgcpgw, ip_address=None):
322 if ip_address is None:
323 ip_address = self.ip_address()
324 return osmo_msc.OsmoMsc(self, hlr, mgcpgw, ip_address)
325
326 def bsc(self, msc, ip_address=None):
327 if ip_address is None:
328 ip_address = self.ip_address()
329 return osmo_bsc.OsmoBsc(self, msc, ip_address)
330
Neels Hofmeyrb902b292017-06-06 21:52:03 +0200331 def bts(self, specifics=None):
332 return bts_obj(self, self.reserved_resources.get(resource.R_BTS, specifics=specifics))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333
Neels Hofmeyrb902b292017-06-06 21:52:03 +0200334 def modem(self, specifics=None):
335 conf = self.reserved_resources.get(resource.R_MODEM, specifics=specifics)
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200336 self.dbg('create Modem object', conf=conf)
337 modem = ofono_client.Modem(conf)
338 self.register_for_cleanup(modem)
339 return modem
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340
Neels Hofmeyrf2d279c2017-05-06 15:05:02 +0200341 def modems(self, count):
342 l = []
343 for i in range(count):
344 l.append(self.modem())
345 return l
346
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 def msisdn(self):
348 msisdn = self.resources_pool.next_msisdn(self.origin)
349 self.log('using MSISDN', msisdn)
350 return msisdn
351
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200352 def poll(self):
353 if self._processes:
354 for process in self._processes:
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200355 if process.terminated():
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200356 process.log_stdout_tail()
357 process.log_stderr_tail()
358 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200359
360 def prompt(self, *msgs, **msg_details):
361 'ask for user interaction. Do not use in tests that should run automatically!'
362 if msg_details:
363 msgs = list(msgs)
364 msgs.append('{%s}' %
365 (', '.join(['%s=%r' % (k,v)
366 for k,v in sorted(msg_details.items())])))
367 msg = ' '.join(msgs) or 'Hit Enter to continue'
368 self.log('prompt:', msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200369 sys.__stdout__.write('\n\n--- PROMPT ---\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200370 sys.__stdout__.write(msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200371 sys.__stdout__.write('\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200372 sys.__stdout__.flush()
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200373 entered = util.input_polling('> ', self.poll)
374 self.log('prompt entered:', repr(entered))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375 return entered
376
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200377 def resource_status_str(self):
378 return '\n'.join(('',
379 'SUITE RUN: %s' % self.origin_id(),
380 'ASKED FOR:', pprint.pformat(self._resource_requirements),
381 'RESERVED COUNT:', pprint.pformat(self.reserved_resources.counts()),
382 'RESOURCES STATE:', repr(self.reserved_resources)))
383
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384loaded_suite_definitions = {}
385
386def load(suite_name):
387 global loaded_suite_definitions
388
389 suite = loaded_suite_definitions.get(suite_name)
390 if suite is not None:
391 return suite
392
393 suites_dir = config.get_suites_dir()
394 suite_dir = suites_dir.child(suite_name)
395 if not suites_dir.exists(suite_name):
396 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
397 if not suites_dir.isdir(suite_name):
398 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
399
400 suite_def = SuiteDefinition(suite_dir)
401 loaded_suite_definitions[suite_name] = suite_def
402 return suite_def
403
404def parse_suite_scenario_str(suite_scenario_str):
405 tokens = suite_scenario_str.split(':')
406 if len(tokens) > 2:
407 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
408
409 suite_name = tokens[0]
410 if len(tokens) <= 1:
411 scenario_names = []
412 else:
413 scenario_names = tokens[1].split('+')
414
415 return suite_name, scenario_names
416
417def load_suite_scenario_str(suite_scenario_str):
418 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
419 suite = load(suite_name)
420 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200421 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422
423def bts_obj(suite_run, conf):
424 bts_type = conf.get('type')
425 log.dbg(None, None, 'create BTS object', type=bts_type)
426 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
427 if bts_class is None:
428 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
429 return bts_class(suite_run, conf)
430
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200431# vim: expandtab tabstop=4 shiftwidth=4