blob: affb9ad84a440dc4689c3637d318fc3ef12ac3cf [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
Neels Hofmeyr3531a192017-03-28 14:30:28 +020024from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb
25from . import test
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020026
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +020027class Timeout(Exception):
28 pass
29
Neels Hofmeyr3531a192017-03-28 14:30:28 +020030class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020031 '''A test suite reserves resources for a number of tests.
32 Each test requires a specific number of modems, BTSs etc., which are
33 reserved beforehand by a test suite. This way several test suites can be
34 scheduled dynamically without resource conflicts arising halfway through
35 the tests.'''
36
37 CONF_FILENAME = 'suite.conf'
38
Neels Hofmeyr3531a192017-03-28 14:30:28 +020039 CONF_SCHEMA = util.dict_add(
40 {
41 'defaults.timeout': schema.STR,
42 },
43 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
44 )
45
46
47 def __init__(self, suite_dir):
48 self.set_log_category(log.C_CNF)
49 self.suite_dir = suite_dir
50 self.set_name(os.path.basename(self.suite_dir))
51 self.read_conf()
52
53 def read_conf(self):
54 with self:
55 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
56 if not os.path.isdir(self.suite_dir):
57 raise RuntimeError('No such directory: %r' % self.suite_dir)
58 self.conf = config.read(os.path.join(self.suite_dir,
59 SuiteDefinition.CONF_FILENAME),
60 SuiteDefinition.CONF_SCHEMA)
61 self.load_tests()
62
Neels Hofmeyr3531a192017-03-28 14:30:28 +020063 def load_tests(self):
64 with self:
65 self.tests = []
66 for basename in sorted(os.listdir(self.suite_dir)):
67 if not basename.endswith('.py'):
68 continue
69 self.tests.append(Test(self, basename))
70
71 def add_test(self, test):
72 with self:
73 if not isinstance(test, Test):
74 raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
75 if test.suite is None:
76 test.suite = self
77 if test.suite is not self:
78 raise ValueError('add_test(): test already belongs to another suite')
79 self.tests.append(test)
80
81
82
83class Test(log.Origin):
84
85 def __init__(self, suite, test_basename):
86 self.suite = suite
87 self.basename = test_basename
88 self.path = os.path.join(self.suite.suite_dir, self.basename)
89 super().__init__(self.path)
90 self.set_name(self.basename)
91 self.set_log_category(log.C_TST)
92
93 def run(self, suite_run):
94 assert self.suite is suite_run.definition
95 with self:
Neels Hofmeyra88b0c72017-05-07 02:15:48 +020096 test.setup(suite_run, self, ofono_client, sys.modules[__name__])
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 success = False
98 try:
99 self.log('START')
100 with self.redirect_stdout():
101 util.run_python_file('%s.%s' % (self.suite.name(), self.name()),
102 self.path)
103 success = True
104 except resource.NoResourceExn:
Neels Hofmeyrc86ab212017-05-06 22:51:29 +0200105 self.err('Current resource state:\n', repr(suite_run.reserved_resources))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200106 raise
107 finally:
108 if success:
109 self.log('PASS')
110 else:
111 self.log('FAIL')
112
113 def name(self):
114 l = log.get_line_for_src(self.path)
115 if l is not None:
116 return '%s:%s' % (self._name, l)
117 return super().name()
118
119class SuiteRun(log.Origin):
120
121 trial = None
122 resources_pool = None
123 reserved_resources = None
124 _resource_requirements = None
125 _config = None
126 _processes = None
127
Your Name44af3412017-04-13 03:11:59 +0200128 def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200129 self.trial = current_trial
130 self.definition = suite_definition
131 self.scenarios = scenarios
Your Name44af3412017-04-13 03:11:59 +0200132 self.set_name(suite_scenario_str)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200133 self.set_log_category(log.C_TST)
134 self.resources_pool = resource.ResourcesPool()
135
136 def combined(self, conf_name):
Your Name44af3412017-04-13 03:11:59 +0200137 self.dbg(combining=conf_name)
138 with log.Origin(combining_scenarios=conf_name):
139 combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
140 self.dbg(definition_conf=combination)
141 for scenario in self.scenarios:
142 with scenario:
143 c = scenario.get(conf_name)
144 self.dbg(scenario=scenario.name(), conf=c)
145 if c is None:
146 continue
147 config.combine(combination, c)
148 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200149
150 def resource_requirements(self):
151 if self._resource_requirements is None:
152 self._resource_requirements = self.combined('resources')
153 return self._resource_requirements
154
155 def config(self):
156 if self._config is None:
157 self._config = self.combined('config')
158 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200159
160 class Results:
161 def __init__(self):
162 self.passed = []
163 self.failed = []
164 self.all_passed = None
165
166 def add_pass(self, test):
167 self.passed.append(test)
168
169 def add_fail(self, test):
170 self.failed.append(test)
171
172 def conclude(self):
173 self.all_passed = bool(self.passed) and not bool(self.failed)
174 return self
175
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176 def __str__(self):
177 if self.failed:
178 return 'FAIL: %d of %d tests failed:\n %s' % (
179 len(self.failed),
180 len(self.failed) + len(self.passed),
181 '\n '.join([t.name() for t in self.failed]))
182 if not self.passed:
183 return 'no tests were run.'
184 return 'pass: all %d tests passed.' % len(self.passed)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200185
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200186 def reserve_resources(self):
187 if self.reserved_resources:
188 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
189 self.log('reserving resources...')
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200190 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200192
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193 def run_tests(self, names=None):
Your Name44af3412017-04-13 03:11:59 +0200194 self.log('Suite run start')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195 if not self.reserved_resources:
196 self.reserve_resources()
197 results = SuiteRun.Results()
198 for test in self.definition.tests:
199 if names and not test.name() in names:
200 continue
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200201 self._run_test(test, results)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 self.stop_processes()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200203 return results.conclude()
204
205 def _run_test(self, test, results):
206 try:
207 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200208 test.run(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200209 results.add_pass(test)
210 except:
211 results.add_fail(test)
212 self.log_exn()
213
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200214 def remember_to_stop(self, process):
215 if self._processes is None:
216 self._processes = []
Pau Espin Pedrolecf10792017-05-08 16:56:38 +0200217 self._processes.insert(0, process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219 def stop_processes(self):
220 if not self._processes:
221 return
222 for process in self._processes:
223 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200224
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225 def nitb_iface(self):
226 return self.reserved_resources.get(resource.R_NITB_IFACE)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200227
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228 def nitb(self, nitb_iface=None):
229 if nitb_iface is None:
230 nitb_iface = self.nitb_iface()
231 return osmo_nitb.OsmoNitb(self, nitb_iface)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 def bts(self):
234 return bts_obj(self, self.reserved_resources.get(resource.R_BTS))
235
236 def modem(self):
237 return modem_obj(self.reserved_resources.get(resource.R_MODEM))
238
Neels Hofmeyrf2d279c2017-05-06 15:05:02 +0200239 def modems(self, count):
240 l = []
241 for i in range(count):
242 l.append(self.modem())
243 return l
244
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200245 def msisdn(self):
246 msisdn = self.resources_pool.next_msisdn(self.origin)
247 self.log('using MSISDN', msisdn)
248 return msisdn
249
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200250 def _wait(self, condition, condition_args, condition_kwargs, timeout, timestep):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200251 if not timeout or timeout < 0:
252 raise RuntimeError('wait() *must* time out at some point. timeout=%r' % timeout)
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200253 if timestep < 0.1:
254 timestep = 0.1
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255
256 started = time.time()
257 while True:
258 self.poll()
259 if condition(*condition_args, **condition_kwargs):
260 return True
261 waited = time.time() - started
262 if waited > timeout:
263 return False
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200264 time.sleep(timestep)
265
266 def wait(self, condition, *condition_args, timeout=300, timestep=1, **condition_kwargs):
267 if not self._wait(condition, condition_args, condition_kwargs, timeout, timestep):
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +0200268 raise Timeout('Timeout expired')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200269
270 def sleep(self, seconds):
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200271 assert seconds > 0.
272 self._wait(lambda: False, [], {}, timeout=seconds, timestep=min(seconds, 1))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200273
274 def poll(self):
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200275 ofono_client.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200276 if self._processes:
277 for process in self._processes:
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200278 if process.terminated():
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200279 process.log_stdout_tail()
280 process.log_stderr_tail()
281 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200282
283 def prompt(self, *msgs, **msg_details):
284 'ask for user interaction. Do not use in tests that should run automatically!'
285 if msg_details:
286 msgs = list(msgs)
287 msgs.append('{%s}' %
288 (', '.join(['%s=%r' % (k,v)
289 for k,v in sorted(msg_details.items())])))
290 msg = ' '.join(msgs) or 'Hit Enter to continue'
291 self.log('prompt:', msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200292 sys.__stdout__.write('\n\n--- PROMPT ---\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200293 sys.__stdout__.write(msg)
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200294 sys.__stdout__.write('\n')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200295 sys.__stdout__.flush()
Neels Hofmeyracf0c932017-05-06 16:05:33 +0200296 entered = util.input_polling('> ', self.poll)
297 self.log('prompt entered:', repr(entered))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200298 return entered
299
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300loaded_suite_definitions = {}
301
302def load(suite_name):
303 global loaded_suite_definitions
304
305 suite = loaded_suite_definitions.get(suite_name)
306 if suite is not None:
307 return suite
308
309 suites_dir = config.get_suites_dir()
310 suite_dir = suites_dir.child(suite_name)
311 if not suites_dir.exists(suite_name):
312 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
313 if not suites_dir.isdir(suite_name):
314 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
315
316 suite_def = SuiteDefinition(suite_dir)
317 loaded_suite_definitions[suite_name] = suite_def
318 return suite_def
319
320def parse_suite_scenario_str(suite_scenario_str):
321 tokens = suite_scenario_str.split(':')
322 if len(tokens) > 2:
323 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
324
325 suite_name = tokens[0]
326 if len(tokens) <= 1:
327 scenario_names = []
328 else:
329 scenario_names = tokens[1].split('+')
330
331 return suite_name, scenario_names
332
333def load_suite_scenario_str(suite_scenario_str):
334 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
335 suite = load(suite_name)
336 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200337 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338
339def bts_obj(suite_run, conf):
340 bts_type = conf.get('type')
341 log.dbg(None, None, 'create BTS object', type=bts_type)
342 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
343 if bts_class is None:
344 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
345 return bts_class(suite_run, conf)
346
347def modem_obj(conf):
348 log.dbg(None, None, 'create Modem object', conf=conf)
349 return ofono_client.Modem(conf)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200350
351# vim: expandtab tabstop=4 shiftwidth=4