blob: 5d03b9583288648948e09797c40fb49d743f5cb4 [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 Hofmeyr2d1d5612017-05-22 20:02:41 +020025import pprint
Pau Espin Pedrol927344b2017-05-22 16:38:49 +020026from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb, event_loop
Neels Hofmeyr3531a192017-03-28 14:30:28 +020027from . import test
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020028
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +020029class Timeout(Exception):
30 pass
31
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020032class Failure(Exception):
33 '''Test failure exception, provided to be raised by tests. fail_type is
34 usually a keyword used to quickly identify the type of failure that
35 occurred. fail_msg is a more extensive text containing information about
36 the issue.'''
37
38 def __init__(self, fail_type, fail_msg):
39 self.fail_type = fail_type
40 self.fail_msg = fail_msg
41
Neels Hofmeyr3531a192017-03-28 14:30:28 +020042class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020043 '''A test suite reserves resources for a number of tests.
44 Each test requires a specific number of modems, BTSs etc., which are
45 reserved beforehand by a test suite. This way several test suites can be
46 scheduled dynamically without resource conflicts arising halfway through
47 the tests.'''
48
49 CONF_FILENAME = 'suite.conf'
50
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051 CONF_SCHEMA = util.dict_add(
52 {
53 'defaults.timeout': schema.STR,
54 },
55 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
56 )
57
58
59 def __init__(self, suite_dir):
60 self.set_log_category(log.C_CNF)
61 self.suite_dir = suite_dir
62 self.set_name(os.path.basename(self.suite_dir))
63 self.read_conf()
64
65 def read_conf(self):
66 with self:
67 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
68 if not os.path.isdir(self.suite_dir):
69 raise RuntimeError('No such directory: %r' % self.suite_dir)
70 self.conf = config.read(os.path.join(self.suite_dir,
71 SuiteDefinition.CONF_FILENAME),
72 SuiteDefinition.CONF_SCHEMA)
73 self.load_tests()
74
Neels Hofmeyr3531a192017-03-28 14:30:28 +020075 def load_tests(self):
76 with self:
77 self.tests = []
78 for basename in sorted(os.listdir(self.suite_dir)):
79 if not basename.endswith('.py'):
80 continue
81 self.tests.append(Test(self, basename))
82
83 def add_test(self, test):
84 with self:
85 if not isinstance(test, Test):
86 raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
87 if test.suite is None:
88 test.suite = self
89 if test.suite is not self:
90 raise ValueError('add_test(): test already belongs to another suite')
91 self.tests.append(test)
92
Neels Hofmeyr3531a192017-03-28 14:30:28 +020093class Test(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020094 UNKNOWN = 'UNKNOWN'
95 SKIP = 'SKIP'
96 PASS = 'PASS'
97 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020098
99 def __init__(self, suite, test_basename):
100 self.suite = suite
101 self.basename = test_basename
102 self.path = os.path.join(self.suite.suite_dir, self.basename)
103 super().__init__(self.path)
104 self.set_name(self.basename)
105 self.set_log_category(log.C_TST)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200106 self.status = Test.UNKNOWN
107 self.start_timestamp = 0
108 self.duration = 0
109 self.fail_type = None
110 self.fail_message = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200111
112 def run(self, suite_run):
113 assert self.suite is suite_run.definition
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200114 try:
115 with self:
116 self.status = Test.UNKNOWN
117 self.start_timestamp = time.time()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200118 test.setup(suite_run, self, ofono_client, sys.modules[__name__], event_loop)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119 self.log('START')
120 with self.redirect_stdout():
121 util.run_python_file('%s.%s' % (self.suite.name(), self.name()),
122 self.path)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200123 if self.status == Test.UNKNOWN:
124 self.set_pass()
125 except Exception as e:
126 self.log_exn()
127 if isinstance(e, Failure):
128 ftype = e.fail_type
129 fmsg = e.fail_msg + '\n' + traceback.format_exc().rstrip()
130 else:
131 ftype = type(e).__name__
132 fmsg = repr(e) + '\n' + traceback.format_exc().rstrip()
133 if isinstance(e, resource.NoResourceExn):
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200134 fmsg += suite_run.resource_status_str()
135
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200136 self.set_fail(ftype, fmsg, False)
137
138 finally:
139 if self.status == Test.PASS or self.status == Test.SKIP:
140 self.log(self.status)
141 else:
142 self.log('%s (%s)' % (self.status, self.fail_type))
143 return self.status
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144
145 def name(self):
146 l = log.get_line_for_src(self.path)
147 if l is not None:
148 return '%s:%s' % (self._name, l)
149 return super().name()
150
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200151 def set_fail(self, fail_type, fail_message, tb=True):
152 self.status = Test.FAIL
153 self.duration = time.time() - self.start_timestamp
154 self.fail_type = fail_type
155 self.fail_message = fail_message
156 if tb:
157 self.fail_message += '\n' + ''.join(traceback.format_stack()[:-1]).rstrip()
158
159 def set_pass(self):
160 self.status = Test.PASS
161 self.duration = time.time() - self.start_timestamp
162
163 def set_skip(self):
164 self.status = Test.SKIP
165 self.duration = 0
166
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167class SuiteRun(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200168 UNKNOWN = 'UNKNOWN'
169 PASS = 'PASS'
170 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171
172 trial = None
173 resources_pool = None
174 reserved_resources = None
175 _resource_requirements = None
176 _config = None
177 _processes = None
178
Your Name44af3412017-04-13 03:11:59 +0200179 def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180 self.trial = current_trial
181 self.definition = suite_definition
182 self.scenarios = scenarios
Your Name44af3412017-04-13 03:11:59 +0200183 self.set_name(suite_scenario_str)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200184 self.set_log_category(log.C_TST)
185 self.resources_pool = resource.ResourcesPool()
186
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200187 def mark_start(self):
188 self.tests = []
189 self.start_timestamp = time.time()
190 self.duration = 0
191 self.test_failed_ctr = 0
192 self.test_skipped_ctr = 0
193 self.status = SuiteRun.UNKNOWN
194
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195 def combined(self, conf_name):
Your Name44af3412017-04-13 03:11:59 +0200196 self.dbg(combining=conf_name)
197 with log.Origin(combining_scenarios=conf_name):
198 combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
199 self.dbg(definition_conf=combination)
200 for scenario in self.scenarios:
201 with scenario:
202 c = scenario.get(conf_name)
203 self.dbg(scenario=scenario.name(), conf=c)
204 if c is None:
205 continue
206 config.combine(combination, c)
207 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200208
209 def resource_requirements(self):
210 if self._resource_requirements is None:
211 self._resource_requirements = self.combined('resources')
212 return self._resource_requirements
213
214 def config(self):
215 if self._config is None:
216 self._config = self.combined('config')
217 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219 def reserve_resources(self):
220 if self.reserved_resources:
221 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
Neels Hofmeyr7e2e8f12017-05-14 03:37:13 +0200222 self.log('reserving resources in', self.resources_pool.state_dir, '...')
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200223 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200225
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200226 def run_tests(self, names=None):
Your Name44af3412017-04-13 03:11:59 +0200227 self.log('Suite run start')
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200228 try:
229 self.mark_start()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200230 event_loop.register_poll_func(self.poll)
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200231 if not self.reserved_resources:
232 self.reserve_resources()
233 for test in self.definition.tests:
234 if names and not test.name() in names:
235 test.set_skip()
236 self.test_skipped_ctr += 1
237 self.tests.append(test)
238 continue
239 with self:
240 st = test.run(self)
241 if st == Test.FAIL:
242 self.test_failed_ctr += 1
243 self.tests.append(test)
244 finally:
245 # if sys.exit() called from signal handler (e.g. SIGINT), SystemExit
246 # base exception is raised. Make sure to stop processes in this
247 # finally section. Resources are automatically freed with 'atexit'.
248 self.stop_processes()
Pau Espin Pedrol927344b2017-05-22 16:38:49 +0200249 event_loop.unregister_poll_func(self.poll)
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200250 self.duration = time.time() - self.start_timestamp
251 if self.test_failed_ctr:
252 self.status = SuiteRun.FAIL
253 else:
254 self.status = SuiteRun.PASS
255 self.log(self.status)
256 return self.status
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200257
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200258 def remember_to_stop(self, process):
259 if self._processes is None:
260 self._processes = []
Pau Espin Pedrolecf10792017-05-08 16:56:38 +0200261 self._processes.insert(0, process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200262
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200263 def stop_processes(self):
264 if not self._processes:
265 return
266 for process in self._processes:
267 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200268
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200269 def nitb_iface(self):
270 return self.reserved_resources.get(resource.R_NITB_IFACE)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200271
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200272 def nitb(self, nitb_iface=None):
273 if nitb_iface is None:
274 nitb_iface = self.nitb_iface()
275 return osmo_nitb.OsmoNitb(self, nitb_iface)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200276
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200277 def bts(self):
278 return bts_obj(self, self.reserved_resources.get(resource.R_BTS))
279
280 def modem(self):
281 return modem_obj(self.reserved_resources.get(resource.R_MODEM))
282
Neels Hofmeyrf2d279c2017-05-06 15:05:02 +0200283 def modems(self, count):
284 l = []
285 for i in range(count):
286 l.append(self.modem())
287 return l
288
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200289 def msisdn(self):
290 msisdn = self.resources_pool.next_msisdn(self.origin)
291 self.log('using MSISDN', msisdn)
292 return msisdn
293
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200294 def poll(self):
295 if self._processes:
296 for process in self._processes:
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200297 if process.terminated():
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200298 process.log_stdout_tail()
299 process.log_stderr_tail()
300 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301
302 def prompt(self, *msgs, **msg_details):
303 'ask for user interaction. Do not use in tests that should run automatically!'
304 if msg_details:
305 msgs = list(msgs)
306 msgs.append('{%s}' %
307 (', '.join(['%s=%r' % (k,v)
308 for k,v in sorted(msg_details.items())])))
309 msg = ' '.join(msgs) or 'Hit Enter to continue'
310 self.log('prompt:', msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200311 sys.__stdout__.write('\n\n--- PROMPT ---\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200312 sys.__stdout__.write(msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200313 sys.__stdout__.write('\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314 sys.__stdout__.flush()
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200315 entered = util.input_polling('> ', self.poll)
316 self.log('prompt entered:', repr(entered))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 return entered
318
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200319 def resource_status_str(self):
320 return '\n'.join(('',
321 'SUITE RUN: %s' % self.origin_id(),
322 'ASKED FOR:', pprint.pformat(self._resource_requirements),
323 'RESERVED COUNT:', pprint.pformat(self.reserved_resources.counts()),
324 'RESOURCES STATE:', repr(self.reserved_resources)))
325
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200326loaded_suite_definitions = {}
327
328def load(suite_name):
329 global loaded_suite_definitions
330
331 suite = loaded_suite_definitions.get(suite_name)
332 if suite is not None:
333 return suite
334
335 suites_dir = config.get_suites_dir()
336 suite_dir = suites_dir.child(suite_name)
337 if not suites_dir.exists(suite_name):
338 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
339 if not suites_dir.isdir(suite_name):
340 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
341
342 suite_def = SuiteDefinition(suite_dir)
343 loaded_suite_definitions[suite_name] = suite_def
344 return suite_def
345
346def parse_suite_scenario_str(suite_scenario_str):
347 tokens = suite_scenario_str.split(':')
348 if len(tokens) > 2:
349 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
350
351 suite_name = tokens[0]
352 if len(tokens) <= 1:
353 scenario_names = []
354 else:
355 scenario_names = tokens[1].split('+')
356
357 return suite_name, scenario_names
358
359def load_suite_scenario_str(suite_scenario_str):
360 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
361 suite = load(suite_name)
362 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200363 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200364
365def bts_obj(suite_run, conf):
366 bts_type = conf.get('type')
367 log.dbg(None, None, 'create BTS object', type=bts_type)
368 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
369 if bts_class is None:
370 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
371 return bts_class(suite_run, conf)
372
373def modem_obj(conf):
374 log.dbg(None, None, 'create Modem object', conf=conf)
375 return ofono_client.Modem(conf)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200376
377# vim: expandtab tabstop=4 shiftwidth=4