blob: 64de2dbbf34c1456db2a3bb8eda27b25cd3e4ca5 [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
8# it under the terms of the GNU Affero General Public License as
9# 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
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# 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 Hofmeyr3531a192017-03-28 14:30:28 +020025from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb
26from . import test
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020027
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +020028class Timeout(Exception):
29 pass
30
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020031class Failure(Exception):
32 '''Test failure exception, provided to be raised by tests. fail_type is
33 usually a keyword used to quickly identify the type of failure that
34 occurred. fail_msg is a more extensive text containing information about
35 the issue.'''
36
37 def __init__(self, fail_type, fail_msg):
38 self.fail_type = fail_type
39 self.fail_msg = fail_msg
40
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020042 '''A test suite reserves resources for a number of tests.
43 Each test requires a specific number of modems, BTSs etc., which are
44 reserved beforehand by a test suite. This way several test suites can be
45 scheduled dynamically without resource conflicts arising halfway through
46 the tests.'''
47
48 CONF_FILENAME = 'suite.conf'
49
Neels Hofmeyr3531a192017-03-28 14:30:28 +020050 CONF_SCHEMA = util.dict_add(
51 {
52 'defaults.timeout': schema.STR,
53 },
54 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
55 )
56
57
58 def __init__(self, suite_dir):
59 self.set_log_category(log.C_CNF)
60 self.suite_dir = suite_dir
61 self.set_name(os.path.basename(self.suite_dir))
62 self.read_conf()
63
64 def read_conf(self):
65 with self:
66 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
67 if not os.path.isdir(self.suite_dir):
68 raise RuntimeError('No such directory: %r' % self.suite_dir)
69 self.conf = config.read(os.path.join(self.suite_dir,
70 SuiteDefinition.CONF_FILENAME),
71 SuiteDefinition.CONF_SCHEMA)
72 self.load_tests()
73
Neels Hofmeyr3531a192017-03-28 14:30:28 +020074 def load_tests(self):
75 with self:
76 self.tests = []
77 for basename in sorted(os.listdir(self.suite_dir)):
78 if not basename.endswith('.py'):
79 continue
80 self.tests.append(Test(self, basename))
81
82 def add_test(self, test):
83 with self:
84 if not isinstance(test, Test):
85 raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
86 if test.suite is None:
87 test.suite = self
88 if test.suite is not self:
89 raise ValueError('add_test(): test already belongs to another suite')
90 self.tests.append(test)
91
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092class Test(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020093 UNKNOWN = 'UNKNOWN'
94 SKIP = 'SKIP'
95 PASS = 'PASS'
96 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097
98 def __init__(self, suite, test_basename):
99 self.suite = suite
100 self.basename = test_basename
101 self.path = os.path.join(self.suite.suite_dir, self.basename)
102 super().__init__(self.path)
103 self.set_name(self.basename)
104 self.set_log_category(log.C_TST)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200105 self.status = Test.UNKNOWN
106 self.start_timestamp = 0
107 self.duration = 0
108 self.fail_type = None
109 self.fail_message = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110
111 def run(self, suite_run):
112 assert self.suite is suite_run.definition
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200113 try:
114 with self:
115 self.status = Test.UNKNOWN
116 self.start_timestamp = time.time()
117 test.setup(suite_run, self, ofono_client, sys.modules[__name__])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200118 self.log('START')
119 with self.redirect_stdout():
120 util.run_python_file('%s.%s' % (self.suite.name(), self.name()),
121 self.path)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200122 if self.status == Test.UNKNOWN:
123 self.set_pass()
124 except Exception as e:
125 self.log_exn()
126 if isinstance(e, Failure):
127 ftype = e.fail_type
128 fmsg = e.fail_msg + '\n' + traceback.format_exc().rstrip()
129 else:
130 ftype = type(e).__name__
131 fmsg = repr(e) + '\n' + traceback.format_exc().rstrip()
132 if isinstance(e, resource.NoResourceExn):
133 msg += '\n' + 'Current resource state:\n' + repr(suite_run.reserved_resources)
134 self.set_fail(ftype, fmsg, False)
135
136 finally:
137 if self.status == Test.PASS or self.status == Test.SKIP:
138 self.log(self.status)
139 else:
140 self.log('%s (%s)' % (self.status, self.fail_type))
141 return self.status
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
143 def name(self):
144 l = log.get_line_for_src(self.path)
145 if l is not None:
146 return '%s:%s' % (self._name, l)
147 return super().name()
148
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200149 def set_fail(self, fail_type, fail_message, tb=True):
150 self.status = Test.FAIL
151 self.duration = time.time() - self.start_timestamp
152 self.fail_type = fail_type
153 self.fail_message = fail_message
154 if tb:
155 self.fail_message += '\n' + ''.join(traceback.format_stack()[:-1]).rstrip()
156
157 def set_pass(self):
158 self.status = Test.PASS
159 self.duration = time.time() - self.start_timestamp
160
161 def set_skip(self):
162 self.status = Test.SKIP
163 self.duration = 0
164
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200165class SuiteRun(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200166 UNKNOWN = 'UNKNOWN'
167 PASS = 'PASS'
168 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200169
170 trial = None
171 resources_pool = None
172 reserved_resources = None
173 _resource_requirements = None
174 _config = None
175 _processes = None
176
Your Name44af3412017-04-13 03:11:59 +0200177 def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178 self.trial = current_trial
179 self.definition = suite_definition
180 self.scenarios = scenarios
Your Name44af3412017-04-13 03:11:59 +0200181 self.set_name(suite_scenario_str)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200182 self.set_log_category(log.C_TST)
183 self.resources_pool = resource.ResourcesPool()
184
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200185 def mark_start(self):
186 self.tests = []
187 self.start_timestamp = time.time()
188 self.duration = 0
189 self.test_failed_ctr = 0
190 self.test_skipped_ctr = 0
191 self.status = SuiteRun.UNKNOWN
192
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193 def combined(self, conf_name):
Your Name44af3412017-04-13 03:11:59 +0200194 self.dbg(combining=conf_name)
195 with log.Origin(combining_scenarios=conf_name):
196 combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
197 self.dbg(definition_conf=combination)
198 for scenario in self.scenarios:
199 with scenario:
200 c = scenario.get(conf_name)
201 self.dbg(scenario=scenario.name(), conf=c)
202 if c is None:
203 continue
204 config.combine(combination, c)
205 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206
207 def resource_requirements(self):
208 if self._resource_requirements is None:
209 self._resource_requirements = self.combined('resources')
210 return self._resource_requirements
211
212 def config(self):
213 if self._config is None:
214 self._config = self.combined('config')
215 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200216
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217 def reserve_resources(self):
218 if self.reserved_resources:
219 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
Neels Hofmeyr7e2e8f12017-05-14 03:37:13 +0200220 self.log('reserving resources in', self.resources_pool.state_dir, '...')
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200221 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200222 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200223
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224 def run_tests(self, names=None):
Your Name44af3412017-04-13 03:11:59 +0200225 self.log('Suite run start')
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200226 try:
227 self.mark_start()
228 if not self.reserved_resources:
229 self.reserve_resources()
230 for test in self.definition.tests:
231 if names and not test.name() in names:
232 test.set_skip()
233 self.test_skipped_ctr += 1
234 self.tests.append(test)
235 continue
236 with self:
237 st = test.run(self)
238 if st == Test.FAIL:
239 self.test_failed_ctr += 1
240 self.tests.append(test)
241 finally:
242 # if sys.exit() called from signal handler (e.g. SIGINT), SystemExit
243 # base exception is raised. Make sure to stop processes in this
244 # finally section. Resources are automatically freed with 'atexit'.
245 self.stop_processes()
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200246 self.duration = time.time() - self.start_timestamp
247 if self.test_failed_ctr:
248 self.status = SuiteRun.FAIL
249 else:
250 self.status = SuiteRun.PASS
251 self.log(self.status)
252 return self.status
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200253
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254 def remember_to_stop(self, process):
255 if self._processes is None:
256 self._processes = []
Pau Espin Pedrolecf10792017-05-08 16:56:38 +0200257 self._processes.insert(0, process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200258
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200259 def stop_processes(self):
260 if not self._processes:
261 return
262 for process in self._processes:
263 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200264
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200265 def nitb_iface(self):
266 return self.reserved_resources.get(resource.R_NITB_IFACE)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200267
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200268 def nitb(self, nitb_iface=None):
269 if nitb_iface is None:
270 nitb_iface = self.nitb_iface()
271 return osmo_nitb.OsmoNitb(self, nitb_iface)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200272
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200273 def bts(self):
274 return bts_obj(self, self.reserved_resources.get(resource.R_BTS))
275
276 def modem(self):
277 return modem_obj(self.reserved_resources.get(resource.R_MODEM))
278
Neels Hofmeyrf2d279c2017-05-06 15:05:02 +0200279 def modems(self, count):
280 l = []
281 for i in range(count):
282 l.append(self.modem())
283 return l
284
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200285 def msisdn(self):
286 msisdn = self.resources_pool.next_msisdn(self.origin)
287 self.log('using MSISDN', msisdn)
288 return msisdn
289
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200290 def _wait(self, condition, condition_args, condition_kwargs, timeout, timestep):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200291 if not timeout or timeout < 0:
292 raise RuntimeError('wait() *must* time out at some point. timeout=%r' % timeout)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200293 if timestep < 0.1:
294 timestep = 0.1
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200295
296 started = time.time()
297 while True:
298 self.poll()
299 if condition(*condition_args, **condition_kwargs):
300 return True
301 waited = time.time() - started
302 if waited > timeout:
303 return False
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200304 time.sleep(timestep)
305
306 def wait(self, condition, *condition_args, timeout=300, timestep=1, **condition_kwargs):
307 if not self._wait(condition, condition_args, condition_kwargs, timeout, timestep):
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +0200308 raise Timeout('Timeout expired')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309
310 def sleep(self, seconds):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200311 assert seconds > 0.
312 self._wait(lambda: False, [], {}, timeout=seconds, timestep=min(seconds, 1))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200313
314 def poll(self):
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200315 ofono_client.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200316 if self._processes:
317 for process in self._processes:
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200318 if process.terminated():
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200319 process.log_stdout_tail()
320 process.log_stderr_tail()
321 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200322
323 def prompt(self, *msgs, **msg_details):
324 'ask for user interaction. Do not use in tests that should run automatically!'
325 if msg_details:
326 msgs = list(msgs)
327 msgs.append('{%s}' %
328 (', '.join(['%s=%r' % (k,v)
329 for k,v in sorted(msg_details.items())])))
330 msg = ' '.join(msgs) or 'Hit Enter to continue'
331 self.log('prompt:', msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200332 sys.__stdout__.write('\n\n--- PROMPT ---\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333 sys.__stdout__.write(msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200334 sys.__stdout__.write('\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335 sys.__stdout__.flush()
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200336 entered = util.input_polling('> ', self.poll)
337 self.log('prompt entered:', repr(entered))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338 return entered
339
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340loaded_suite_definitions = {}
341
342def load(suite_name):
343 global loaded_suite_definitions
344
345 suite = loaded_suite_definitions.get(suite_name)
346 if suite is not None:
347 return suite
348
349 suites_dir = config.get_suites_dir()
350 suite_dir = suites_dir.child(suite_name)
351 if not suites_dir.exists(suite_name):
352 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
353 if not suites_dir.isdir(suite_name):
354 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
355
356 suite_def = SuiteDefinition(suite_dir)
357 loaded_suite_definitions[suite_name] = suite_def
358 return suite_def
359
360def parse_suite_scenario_str(suite_scenario_str):
361 tokens = suite_scenario_str.split(':')
362 if len(tokens) > 2:
363 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
364
365 suite_name = tokens[0]
366 if len(tokens) <= 1:
367 scenario_names = []
368 else:
369 scenario_names = tokens[1].split('+')
370
371 return suite_name, scenario_names
372
373def load_suite_scenario_str(suite_scenario_str):
374 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
375 suite = load(suite_name)
376 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200377 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200378
379def bts_obj(suite_run, conf):
380 bts_type = conf.get('type')
381 log.dbg(None, None, 'create BTS object', type=bts_type)
382 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
383 if bts_class is None:
384 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
385 return bts_class(suite_run, conf)
386
387def modem_obj(conf):
388 log.dbg(None, None, 'create Modem object', conf=conf)
389 return ofono_client.Modem(conf)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200390
391# vim: expandtab tabstop=4 shiftwidth=4