blob: 2d6c67b8b18b32156740ace1c7ad8df69a81e050 [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
23from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb
24from . import test
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020025
Neels Hofmeyr3531a192017-03-28 14:30:28 +020026class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020027 '''A test suite reserves resources for a number of tests.
28 Each test requires a specific number of modems, BTSs etc., which are
29 reserved beforehand by a test suite. This way several test suites can be
30 scheduled dynamically without resource conflicts arising halfway through
31 the tests.'''
32
33 CONF_FILENAME = 'suite.conf'
34
Neels Hofmeyr3531a192017-03-28 14:30:28 +020035 CONF_SCHEMA = util.dict_add(
36 {
37 'defaults.timeout': schema.STR,
38 },
39 dict([('resources.%s' % k, t) for k,t in resource.WANT_SCHEMA.items()])
40 )
41
42
43 def __init__(self, suite_dir):
44 self.set_log_category(log.C_CNF)
45 self.suite_dir = suite_dir
46 self.set_name(os.path.basename(self.suite_dir))
47 self.read_conf()
48
49 def read_conf(self):
50 with self:
51 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
52 if not os.path.isdir(self.suite_dir):
53 raise RuntimeError('No such directory: %r' % self.suite_dir)
54 self.conf = config.read(os.path.join(self.suite_dir,
55 SuiteDefinition.CONF_FILENAME),
56 SuiteDefinition.CONF_SCHEMA)
57 self.load_tests()
58
59
60 def load_tests(self):
61 with self:
62 self.tests = []
63 for basename in sorted(os.listdir(self.suite_dir)):
64 if not basename.endswith('.py'):
65 continue
66 self.tests.append(Test(self, basename))
67
68 def add_test(self, test):
69 with self:
70 if not isinstance(test, Test):
71 raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
72 if test.suite is None:
73 test.suite = self
74 if test.suite is not self:
75 raise ValueError('add_test(): test already belongs to another suite')
76 self.tests.append(test)
77
78
79
80class Test(log.Origin):
81
82 def __init__(self, suite, test_basename):
83 self.suite = suite
84 self.basename = test_basename
85 self.path = os.path.join(self.suite.suite_dir, self.basename)
86 super().__init__(self.path)
87 self.set_name(self.basename)
88 self.set_log_category(log.C_TST)
89
90 def run(self, suite_run):
91 assert self.suite is suite_run.definition
92 with self:
93 test.setup(suite_run, self, ofono_client)
94 success = False
95 try:
96 self.log('START')
97 with self.redirect_stdout():
98 util.run_python_file('%s.%s' % (self.suite.name(), self.name()),
99 self.path)
100 success = True
101 except resource.NoResourceExn:
102 self.err('Current resource state:\n', repr(reserved_resources))
103 raise
104 finally:
105 if success:
106 self.log('PASS')
107 else:
108 self.log('FAIL')
109
110 def name(self):
111 l = log.get_line_for_src(self.path)
112 if l is not None:
113 return '%s:%s' % (self._name, l)
114 return super().name()
115
116class SuiteRun(log.Origin):
117
118 trial = None
119 resources_pool = None
120 reserved_resources = None
121 _resource_requirements = None
122 _config = None
123 _processes = None
124
125 def __init__(self, current_trial, suite_definition, scenarios=[]):
126 self.trial = current_trial
127 self.definition = suite_definition
128 self.scenarios = scenarios
129 self.set_name(suite_definition.name())
130 self.set_log_category(log.C_TST)
131 self.resources_pool = resource.ResourcesPool()
132
133 def combined(self, conf_name):
134 combination = self.definition.conf.get(conf_name) or {}
135 for scenario in self.scenarios:
136 c = scenario.get(conf_name)
137 if c is None:
138 continue
139 config.combine(combination, c)
140 return combination
141
142 def resource_requirements(self):
143 if self._resource_requirements is None:
144 self._resource_requirements = self.combined('resources')
145 return self._resource_requirements
146
147 def config(self):
148 if self._config is None:
149 self._config = self.combined('config')
150 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200151
152 class Results:
153 def __init__(self):
154 self.passed = []
155 self.failed = []
156 self.all_passed = None
157
158 def add_pass(self, test):
159 self.passed.append(test)
160
161 def add_fail(self, test):
162 self.failed.append(test)
163
164 def conclude(self):
165 self.all_passed = bool(self.passed) and not bool(self.failed)
166 return self
167
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200168 def __str__(self):
169 if self.failed:
170 return 'FAIL: %d of %d tests failed:\n %s' % (
171 len(self.failed),
172 len(self.failed) + len(self.passed),
173 '\n '.join([t.name() for t in self.failed]))
174 if not self.passed:
175 return 'no tests were run.'
176 return 'pass: all %d tests passed.' % len(self.passed)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200177
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178 def reserve_resources(self):
179 if self.reserved_resources:
180 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
181 self.log('reserving resources...')
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200182 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200184
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185 def run_tests(self, names=None):
186 if not self.reserved_resources:
187 self.reserve_resources()
188 results = SuiteRun.Results()
189 for test in self.definition.tests:
190 if names and not test.name() in names:
191 continue
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200192 self._run_test(test, results)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193 self.stop_processes()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200194 return results.conclude()
195
196 def _run_test(self, test, results):
197 try:
198 with self:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200199 test.run(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200200 results.add_pass(test)
201 except:
202 results.add_fail(test)
203 self.log_exn()
204
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200205 def remember_to_stop(self, process):
206 if self._processes is None:
207 self._processes = []
208 self._processes.append(process)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200209
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210 def stop_processes(self):
211 if not self._processes:
212 return
213 for process in self._processes:
214 process.terminate()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200215
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216 def nitb_iface(self):
217 return self.reserved_resources.get(resource.R_NITB_IFACE)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219 def nitb(self, nitb_iface=None):
220 if nitb_iface is None:
221 nitb_iface = self.nitb_iface()
222 return osmo_nitb.OsmoNitb(self, nitb_iface)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200223
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224 def bts(self):
225 return bts_obj(self, self.reserved_resources.get(resource.R_BTS))
226
227 def modem(self):
228 return modem_obj(self.reserved_resources.get(resource.R_MODEM))
229
230 def msisdn(self):
231 msisdn = self.resources_pool.next_msisdn(self.origin)
232 self.log('using MSISDN', msisdn)
233 return msisdn
234
235 def wait(self, condition, *condition_args, timeout=300, **condition_kwargs):
236 if not timeout or timeout < 0:
237 raise RuntimeError('wait() *must* time out at some point. timeout=%r' % timeout)
238
239 started = time.time()
240 while True:
241 self.poll()
242 if condition(*condition_args, **condition_kwargs):
243 return True
244 waited = time.time() - started
245 if waited > timeout:
246 return False
247 time.sleep(.1)
248
249 def sleep(self, seconds):
250 self.wait(lambda: False, timeout=seconds)
251
252 def poll(self):
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200253 ofono_client.poll()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254 if self._processes:
255 for process in self._processes:
256 process.poll()
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200257 if not process.is_running():
258 process.log_stdout_tail()
259 process.log_stderr_tail()
260 process.raise_exn('Process ended prematurely')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200261
262 def prompt(self, *msgs, **msg_details):
263 'ask for user interaction. Do not use in tests that should run automatically!'
264 if msg_details:
265 msgs = list(msgs)
266 msgs.append('{%s}' %
267 (', '.join(['%s=%r' % (k,v)
268 for k,v in sorted(msg_details.items())])))
269 msg = ' '.join(msgs) or 'Hit Enter to continue'
270 self.log('prompt:', msg)
271 sys.__stdout__.write(msg)
272 sys.__stdout__.write('\n> ')
273 sys.__stdout__.flush()
274 entered = util.input_polling(self.poll)
275 self.log('prompt entered:', entered)
276 return entered
277
278
279loaded_suite_definitions = {}
280
281def load(suite_name):
282 global loaded_suite_definitions
283
284 suite = loaded_suite_definitions.get(suite_name)
285 if suite is not None:
286 return suite
287
288 suites_dir = config.get_suites_dir()
289 suite_dir = suites_dir.child(suite_name)
290 if not suites_dir.exists(suite_name):
291 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
292 if not suites_dir.isdir(suite_name):
293 raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
294
295 suite_def = SuiteDefinition(suite_dir)
296 loaded_suite_definitions[suite_name] = suite_def
297 return suite_def
298
299def parse_suite_scenario_str(suite_scenario_str):
300 tokens = suite_scenario_str.split(':')
301 if len(tokens) > 2:
302 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
303
304 suite_name = tokens[0]
305 if len(tokens) <= 1:
306 scenario_names = []
307 else:
308 scenario_names = tokens[1].split('+')
309
310 return suite_name, scenario_names
311
312def load_suite_scenario_str(suite_scenario_str):
313 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
314 suite = load(suite_name)
315 scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
316 return (suite, scenarios)
317
318def bts_obj(suite_run, conf):
319 bts_type = conf.get('type')
320 log.dbg(None, None, 'create BTS object', type=bts_type)
321 bts_class = resource.KNOWN_BTS_TYPES.get(bts_type)
322 if bts_class is None:
323 raise RuntimeError('No such BTS type is defined: %r' % bts_type)
324 return bts_class(suite_run, conf)
325
326def modem_obj(conf):
327 log.dbg(None, None, 'create Modem object', conf=conf)
328 return ofono_client.Modem(conf)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200329
330# vim: expandtab tabstop=4 shiftwidth=4