blob: b88b8b2a2f6ed1c4ff9f3883a9f3a21d69f01d4a [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
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020034class Failure(Exception):
35 '''Test failure exception, provided to be raised by tests. fail_type is
36 usually a keyword used to quickly identify the type of failure that
37 occurred. fail_msg is a more extensive text containing information about
38 the issue.'''
39
40 def __init__(self, fail_type, fail_msg):
41 self.fail_type = fail_type
42 self.fail_msg = fail_msg
43
Neels Hofmeyr3531a192017-03-28 14:30:28 +020044class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020045 '''A test suite reserves resources for a number of tests.
46 Each test requires a specific number of modems, BTSs etc., which are
47 reserved beforehand by a test suite. This way several test suites can be
48 scheduled dynamically without resource conflicts arising halfway through
49 the tests.'''
50
51 CONF_FILENAME = 'suite.conf'
52
Neels Hofmeyr3531a192017-03-28 14:30:28 +020053 CONF_SCHEMA = util.dict_add(
54 {
55 'defaults.timeout': schema.STR,
56 },
57 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
58 )
59
60
61 def __init__(self, suite_dir):
62 self.set_log_category(log.C_CNF)
63 self.suite_dir = suite_dir
64 self.set_name(os.path.basename(self.suite_dir))
65 self.read_conf()
66
67 def read_conf(self):
68 with self:
69 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
70 if not os.path.isdir(self.suite_dir):
71 raise RuntimeError('No such directory: %r' % self.suite_dir)
72 self.conf = config.read(os.path.join(self.suite_dir,
73 SuiteDefinition.CONF_FILENAME),
74 SuiteDefinition.CONF_SCHEMA)
75 self.load_tests()
76
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 def load_tests(self):
78 with self:
79 self.tests = []
80 for basename in sorted(os.listdir(self.suite_dir)):
81 if not basename.endswith('.py'):
82 continue
83 self.tests.append(Test(self, basename))
84
85 def add_test(self, test):
86 with self:
87 if not isinstance(test, Test):
88 raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
89 if test.suite is None:
90 test.suite = self
91 if test.suite is not self:
92 raise ValueError('add_test(): test already belongs to another suite')
93 self.tests.append(test)
94
Neels Hofmeyr3531a192017-03-28 14:30:28 +020095class Test(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020096 UNKNOWN = 'UNKNOWN'
97 SKIP = 'SKIP'
98 PASS = 'PASS'
99 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100
101 def __init__(self, suite, test_basename):
102 self.suite = suite
103 self.basename = test_basename
104 self.path = os.path.join(self.suite.suite_dir, self.basename)
105 super().__init__(self.path)
106 self.set_name(self.basename)
107 self.set_log_category(log.C_TST)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200108 self.status = Test.UNKNOWN
109 self.start_timestamp = 0
110 self.duration = 0
111 self.fail_type = None
112 self.fail_message = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200113
114 def run(self, suite_run):
115 assert self.suite is suite_run.definition
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200116 try:
117 with self:
118 self.status = Test.UNKNOWN
119 self.start_timestamp = time.time()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200120 test.setup(suite_run, self, ofono_client, sys.modules[__name__], event_loop)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 self.log('START')
122 with self.redirect_stdout():
123 util.run_python_file('%s.%s' % (self.suite.name(), self.name()),
124 self.path)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200125 if self.status == Test.UNKNOWN:
126 self.set_pass()
127 except Exception as e:
128 self.log_exn()
129 if isinstance(e, Failure):
130 ftype = e.fail_type
131 fmsg = e.fail_msg + '\n' + traceback.format_exc().rstrip()
132 else:
133 ftype = type(e).__name__
134 fmsg = repr(e) + '\n' + traceback.format_exc().rstrip()
135 if isinstance(e, resource.NoResourceExn):
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200136 fmsg += suite_run.resource_status_str()
137
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200138 self.set_fail(ftype, fmsg, False)
139
140 finally:
141 if self.status == Test.PASS or self.status == Test.SKIP:
142 self.log(self.status)
143 else:
144 self.log('%s (%s)' % (self.status, self.fail_type))
145 return self.status
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200146
147 def name(self):
148 l = log.get_line_for_src(self.path)
149 if l is not None:
150 return '%s:%s' % (self._name, l)
151 return super().name()
152
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200153 def set_fail(self, fail_type, fail_message, tb=True):
154 self.status = Test.FAIL
155 self.duration = time.time() - self.start_timestamp
156 self.fail_type = fail_type
157 self.fail_message = fail_message
158 if tb:
159 self.fail_message += '\n' + ''.join(traceback.format_stack()[:-1]).rstrip()
160
161 def set_pass(self):
162 self.status = Test.PASS
163 self.duration = time.time() - self.start_timestamp
164
165 def set_skip(self):
166 self.status = Test.SKIP
167 self.duration = 0
168
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200169class SuiteRun(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200170 UNKNOWN = 'UNKNOWN'
171 PASS = 'PASS'
172 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173
174 trial = None
175 resources_pool = None
176 reserved_resources = None
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200177 objects_to_clean_up = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178 _resource_requirements = None
179 _config = None
180 _processes = None
181
Your Name44af3412017-04-13 03:11:59 +0200182 def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183 self.trial = current_trial
184 self.definition = suite_definition
185 self.scenarios = scenarios
Your Name44af3412017-04-13 03:11:59 +0200186 self.set_name(suite_scenario_str)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200187 self.set_log_category(log.C_TST)
188 self.resources_pool = resource.ResourcesPool()
189
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200190 def register_for_cleanup(self, *obj):
191 assert all([hasattr(o, 'cleanup') for o in obj])
192 self.objects_to_clean_up = self.objects_to_clean_up or []
193 self.objects_to_clean_up.extend(obj)
194
195 def objects_cleanup(self):
196 while self.objects_to_clean_up:
197 obj = self.objects_to_clean_up.pop()
198 obj.cleanup()
199
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200200 def mark_start(self):
201 self.tests = []
202 self.start_timestamp = time.time()
203 self.duration = 0
204 self.test_failed_ctr = 0
205 self.test_skipped_ctr = 0
206 self.status = SuiteRun.UNKNOWN
207
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200208 def combined(self, conf_name):
Your Name44af3412017-04-13 03:11:59 +0200209 self.dbg(combining=conf_name)
210 with log.Origin(combining_scenarios=conf_name):
211 combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
212 self.dbg(definition_conf=combination)
213 for scenario in self.scenarios:
214 with scenario:
215 c = scenario.get(conf_name)
216 self.dbg(scenario=scenario.name(), conf=c)
217 if c is None:
218 continue
219 config.combine(combination, c)
220 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221
222 def resource_requirements(self):
223 if self._resource_requirements is None:
224 self._resource_requirements = self.combined('resources')
225 return self._resource_requirements
226
227 def config(self):
228 if self._config is None:
229 self._config = self.combined('config')
230 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200231
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200232 def reserve_resources(self):
233 if self.reserved_resources:
234 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
Neels Hofmeyr7e2e8f12017-05-14 03:37:13 +0200235 self.log('reserving resources in', self.resources_pool.state_dir, '...')
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200238
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200239 def run_tests(self, names=None):
Your Name44af3412017-04-13 03:11:59 +0200240 self.log('Suite run start')
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200241 try:
242 self.mark_start()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200243 event_loop.register_poll_func(self.poll)
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200244 if not self.reserved_resources:
245 self.reserve_resources()
246 for test in self.definition.tests:
247 if names and not test.name() in names:
248 test.set_skip()
249 self.test_skipped_ctr += 1
250 self.tests.append(test)
251 continue
252 with self:
253 st = test.run(self)
254 if st == Test.FAIL:
255 self.test_failed_ctr += 1
256 self.tests.append(test)
257 finally:
258 # if sys.exit() called from signal handler (e.g. SIGINT), SystemExit
259 # base exception is raised. Make sure to stop processes in this
260 # finally section. Resources are automatically freed with 'atexit'.
261 self.stop_processes()
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200262 self.objects_cleanup()
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200263 self.free_resources()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200264 event_loop.unregister_poll_func(self.poll)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200265 self.duration = time.time() - self.start_timestamp
266 if self.test_failed_ctr:
267 self.status = SuiteRun.FAIL
268 else:
269 self.status = SuiteRun.PASS
270 self.log(self.status)
271 return self.status
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200272
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200273 def remember_to_stop(self, process):
274 if self._processes is None:
275 self._processes = []
Pau Espin Pedrolecf10792017-05-08 16:56:38 +0200276 self._processes.insert(0, process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200277
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200278 def stop_processes(self):
279 if not self._processes:
280 return
281 for process in self._processes:
282 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200283
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200284 def free_resources(self):
285 if self.reserved_resources is None:
286 return
287 self.reserved_resources.free()
288
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200289 def ip_address(self):
290 return self.reserved_resources.get(resource.R_IP_ADDRESS)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200292 def nitb(self, ip_address=None):
293 if ip_address is None:
294 ip_address = self.ip_address()
295 return osmo_nitb.OsmoNitb(self, ip_address)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200296
Neels Hofmeyr798e5922017-05-18 15:24:02 +0200297 def hlr(self, ip_address=None):
298 if ip_address is None:
299 ip_address = self.ip_address()
300 return osmo_hlr.OsmoHlr(self, ip_address)
301
302 def mgcpgw(self, ip_address=None, bts_ip=None):
303 if ip_address is None:
304 ip_address = self.ip_address()
305 return osmo_mgcpgw.OsmoMgcpgw(self, ip_address, bts_ip)
306
307 def msc(self, hlr, mgcpgw, ip_address=None):
308 if ip_address is None:
309 ip_address = self.ip_address()
310 return osmo_msc.OsmoMsc(self, hlr, mgcpgw, ip_address)
311
312 def bsc(self, msc, ip_address=None):
313 if ip_address is None:
314 ip_address = self.ip_address()
315 return osmo_bsc.OsmoBsc(self, msc, ip_address)
316
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 def bts(self):
318 return bts_obj(self, self.reserved_resources.get(resource.R_BTS))
319
320 def modem(self):
Neels Hofmeyr4d688c22017-05-29 04:13:58 +0200321 conf = self.reserved_resources.get(resource.R_MODEM)
322 self.dbg('create Modem object', conf=conf)
323 modem = ofono_client.Modem(conf)
324 self.register_for_cleanup(modem)
325 return modem
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200326
Neels Hofmeyrf2d279c2017-05-06 15:05:02 +0200327 def modems(self, count):
328 l = []
329 for i in range(count):
330 l.append(self.modem())
331 return l
332
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333 def msisdn(self):
334 msisdn = self.resources_pool.next_msisdn(self.origin)
335 self.log('using MSISDN', msisdn)
336 return msisdn
337
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338 def poll(self):
339 if self._processes:
340 for process in self._processes:
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200341 if process.terminated():
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200342 process.log_stdout_tail()
343 process.log_stderr_tail()
344 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345
346 def prompt(self, *msgs, **msg_details):
347 'ask for user interaction. Do not use in tests that should run automatically!'
348 if msg_details:
349 msgs = list(msgs)
350 msgs.append('{%s}' %
351 (', '.join(['%s=%r' % (k,v)
352 for k,v in sorted(msg_details.items())])))
353 msg = ' '.join(msgs) or 'Hit Enter to continue'
354 self.log('prompt:', msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200355 sys.__stdout__.write('\n\n--- PROMPT ---\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200356 sys.__stdout__.write(msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200357 sys.__stdout__.write('\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200358 sys.__stdout__.flush()
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200359 entered = util.input_polling('> ', self.poll)
360 self.log('prompt entered:', repr(entered))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200361 return entered
362
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200363 def resource_status_str(self):
364 return '\n'.join(('',
365 'SUITE RUN: %s' % self.origin_id(),
366 'ASKED FOR:', pprint.pformat(self._resource_requirements),
367 'RESERVED COUNT:', pprint.pformat(self.reserved_resources.counts()),
368 'RESOURCES STATE:', repr(self.reserved_resources)))
369
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200370loaded_suite_definitions = {}
371
372def load(suite_name):
373 global loaded_suite_definitions
374
375 suite = loaded_suite_definitions.get(suite_name)
376 if suite is not None:
377 return suite
378
379 suites_dir = config.get_suites_dir()
380 suite_dir = suites_dir.child(suite_name)
381 if not suites_dir.exists(suite_name):
382 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
383 if not suites_dir.isdir(suite_name):
384 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
385
386 suite_def = SuiteDefinition(suite_dir)
387 loaded_suite_definitions[suite_name] = suite_def
388 return suite_def
389
390def parse_suite_scenario_str(suite_scenario_str):
391 tokens = suite_scenario_str.split(':')
392 if len(tokens) > 2:
393 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
394
395 suite_name = tokens[0]
396 if len(tokens) <= 1:
397 scenario_names = []
398 else:
399 scenario_names = tokens[1].split('+')
400
401 return suite_name, scenario_names
402
403def load_suite_scenario_str(suite_scenario_str):
404 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
405 suite = load(suite_name)
406 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200407 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200408
409def bts_obj(suite_run, conf):
410 bts_type = conf.get('type')
411 log.dbg(None, None, 'create BTS object', type=bts_type)
412 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
413 if bts_class is None:
414 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
415 return bts_class(suite_run, conf)
416
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200417# vim: expandtab tabstop=4 shiftwidth=4