812aecee117daa8c7d0a8f6d19d152dfc91b14ea
[nfvbench.git] / test / test_chains.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16 """Test Chaining functions."""
17
18 from mock_trex import no_op
19
20 from mock import MagicMock
21 from mock import patch
22 import pytest
23
24 from nfvbench.chain_runner import ChainRunner
25 from nfvbench.chaining import ChainException
26 from nfvbench.chaining import ChainVnfPort
27 from nfvbench.chaining import InstancePlacer
28 from nfvbench.compute import Compute
29 import nfvbench.credentials
30 from nfvbench.factory import BasicFactory
31 import nfvbench.log
32 from nfvbench.nfvbench import load_default_config
33 from nfvbench.nfvbench import NFVBench
34 from nfvbench.packet_stats import InterfaceStats
35 from nfvbench.specs import ChainType
36 from nfvbench.specs import OpenStackSpec
37 from nfvbench.specs import Specs
38 from nfvbench.summarizer import _annotate_chain_stats
39 from nfvbench.traffic_client import TrafficClient
40 from nfvbench.traffic_gen.traffic_base import Latency
41 from nfvbench.traffic_gen.trex import TRex
42
43
44 # just to get rid of the unused function warning
45 no_op()
46
47
48 def setup_module(module):
49     """Enable log."""
50     nfvbench.log.setup(mute_stdout=False)
51     nfvbench.log.set_level(debug=True)
52
53 def _get_chain_config(sc=ChainType.PVP, scc=1, shared_net=True, rate='1Mpps'):
54     config, _ = load_default_config()
55     config.vm_image_file = 'nfvbenchvm-0.0.qcow2'
56     config.service_chain_count = scc
57     config.service_chain = sc
58     config.service_chain_shared_net = shared_net
59     config.rate = rate
60     config['traffic_generator']['generator_profile'] = [{'name': 'dummy',
61                                                          'tool': 'dummy',
62                                                          'ip': '127.0.0.1',
63                                                          'intf_speed': '10Gbps',
64                                                          'interfaces': [{'port': 0, 'pci': '0.0'},
65                                                                         {'port': 1, 'pci': '0.0'}]}]
66     config.ndr_run = False
67     config.pdr_run = False
68     config.single_run = True
69     config.generator_profile = 'dummy'
70     config.duration_sec = 2
71     config.interval_sec = 1
72     config.openrc_file = "dummy.rc"
73     return config
74
75 def test_chain_runner_ext_no_openstack():
76     """Test ChainRunner EXT no openstack."""
77     config = _get_chain_config(sc=ChainType.EXT)
78     specs = Specs()
79     config.vlans = [100, 200]
80     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
81     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
82
83     for shared_net in [True, False]:
84         for no_arp in [False, True]:
85             for vlan_tag in [False, True]:
86                 for scc in [1, 2]:
87                     config = _get_chain_config(ChainType.EXT, scc, shared_net)
88                     config.no_arp = no_arp
89                     if no_arp:
90                         # If EXT and no arp, the config must provide mac (1 pair per chain)
91                         config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00'] * scc
92                         config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00'] * scc
93                     config['vlan_tagging'] = vlan_tag
94                     if vlan_tag:
95                         # these are the 2 valid forms of vlan ranges
96                         if scc == 1:
97                             config.vlans = [100, 200]
98                         else:
99                             config.vlans = [[port * 100 + index for index in range(scc)]
100                                             for port in range(2)]
101                     runner = ChainRunner(config, None, specs, BasicFactory())
102                     runner.close()
103
104
105 def _mock_find_image(self, image_name):
106     return True
107
108 @patch.object(Compute, 'find_image', _mock_find_image)
109 @patch('nfvbench.chaining.Client')
110 @patch('nfvbench.chaining.neutronclient')
111 @patch('nfvbench.chaining.glanceclient')
112 def _test_pvp_chain(config, cred, mock_glance, mock_neutron, mock_client):
113     # instance = self.novaclient.servers.create(name=vmname,...)
114     # instance.status == 'ACTIVE'
115     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
116     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
117     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
118     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
119     specs = Specs()
120     openstack_spec = OpenStackSpec()
121     specs.set_openstack_spec(openstack_spec)
122     cred = MagicMock(spec=nfvbench.credentials.Credentials)
123     cred.is_admin = True
124     runner = ChainRunner(config, cred, specs, BasicFactory())
125     runner.close()
126
127 def test_pvp_chain_runner():
128     """Test PVP chain runner."""
129     cred = MagicMock(spec=nfvbench.credentials.Credentials)
130     cred.is_admin = True
131     for shared_net in [True, False]:
132         for sc in [ChainType.PVP]:
133             for scc in [1, 2]:
134                 config = _get_chain_config(sc, scc, shared_net)
135                 _test_pvp_chain(config, cred)
136
137
138 # Test not admin exception with empty value is raised
139 @patch.object(Compute, 'find_image', _mock_find_image)
140 @patch('nfvbench.chaining.Client')
141 @patch('nfvbench.chaining.neutronclient')
142 @patch('nfvbench.chaining.glanceclient')
143 def _test_pvp_chain_no_admin_no_config_values(config, cred, mock_glance, mock_neutron, mock_client):
144     # instance = self.novaclient.servers.create(name=vmname,...)
145     # instance.status == 'ACTIVE'
146     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
147     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
148     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
149     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
150     specs = Specs()
151     openstack_spec = OpenStackSpec()
152     specs.set_openstack_spec(openstack_spec)
153     runner = ChainRunner(config, cred, specs, BasicFactory())
154     runner.close()
155
156 def test_pvp_chain_runner_no_admin_no_config_values():
157     """Test PVP chain runner."""
158     cred = MagicMock(spec=nfvbench.credentials.Credentials)
159     cred.is_admin = False
160     for shared_net in [True, False]:
161         for sc in [ChainType.PVP]:
162             for scc in [1, 2]:
163                 config = _get_chain_config(sc, scc, shared_net)
164                 with pytest.raises(ChainException):
165                     _test_pvp_chain_no_admin_no_config_values(config, cred)
166
167 # Test not admin with mandatory parameters values in config file
168 @patch.object(Compute, 'find_image', _mock_find_image)
169 @patch('nfvbench.chaining.Client')
170 @patch('nfvbench.chaining.neutronclient')
171 @patch('nfvbench.chaining.glanceclient')
172 def _test_pvp_chain_no_admin_config_values(config, cred, mock_glance, mock_neutron, mock_client):
173     # instance = self.novaclient.servers.create(name=vmname,...)
174     # instance.status == 'ACTIVE'
175     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
176     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
177     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
178     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
179     specs = Specs()
180     openstack_spec = OpenStackSpec()
181     specs.set_openstack_spec(openstack_spec)
182     runner = ChainRunner(config, cred, specs, BasicFactory())
183     runner.close()
184
185 def test_pvp_chain_runner_no_admin_config_values():
186     """Test PVP chain runner."""
187     cred = MagicMock(spec=nfvbench.credentials.Credentials)
188     cred.is_admin = False
189     for shared_net in [True, False]:
190         for sc in [ChainType.PVP]:
191             for scc in [1, 2]:
192                 config = _get_chain_config(sc, scc, shared_net)
193                 config.availability_zone = "az"
194                 config.hypervisor_hostname = "server"
195                 # these are the 2 valid forms of vlan ranges
196                 if scc == 1:
197                     config.vlans = [100, 200]
198                 else:
199                     config.vlans = [[port * 100 + index for index in range(scc)]
200                                     for port in range(2)]
201                 _test_pvp_chain_no_admin_config_values(config, cred)
202
203
204 @patch.object(Compute, 'find_image', _mock_find_image)
205 @patch('nfvbench.chaining.Client')
206 @patch('nfvbench.chaining.neutronclient')
207 @patch('nfvbench.chaining.glanceclient')
208 def _test_ext_chain(config, cred, mock_glance, mock_neutron, mock_client):
209     # instance = self.novaclient.servers.create(name=vmname,...)
210     # instance.status == 'ACTIVE'
211     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
212     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
213     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
214     specs = Specs()
215     openstack_spec = OpenStackSpec()
216     specs.set_openstack_spec(openstack_spec)
217     cred = MagicMock(spec=nfvbench.credentials.Credentials)
218     cred.is_admin = True
219     runner = ChainRunner(config, cred, specs, BasicFactory())
220     runner.close()
221
222 def test_ext_chain_runner():
223     """Test openstack+EXT chain runner.
224
225     Test 8 combinations of configs:
226     shared/not shared net x arp/no_arp x scc 1 or 2
227     """
228     cred = MagicMock(spec=nfvbench.credentials.Credentials)
229     cred.is_admin = True
230     for shared_net in [True, False]:
231         for no_arp in [False, True]:
232             for scc in [1, 2]:
233                 config = _get_chain_config(ChainType.EXT, scc, shared_net)
234                 config.no_arp = no_arp
235                 if no_arp:
236                     # If EXT and no arp, the config must provide mac addresses (1 pair per chain)
237                     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00'] * scc
238                     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00'] * scc
239                 _test_ext_chain(config, cred)
240
241 def _check_nfvbench_openstack(sc=ChainType.PVP, l2_loopback=False):
242     for scc in range(1, 3):
243         config = _get_chain_config(sc, scc=scc, shared_net=True)
244         if l2_loopback:
245             config.l2_loopback = True
246             config.vlans = [[100], [200]]
247         factory = BasicFactory()
248         config_plugin = factory.get_config_plugin_class()(config)
249         config = config_plugin.get_config()
250         openstack_spec = config_plugin.get_openstack_spec()
251         nfvb = NFVBench(config, openstack_spec, config_plugin, factory)
252         res = nfvb.run({}, 'pytest')
253         if res['status'] != 'OK':
254             print res
255         assert res['status'] == 'OK'
256
257
258 mac_seq = 0
259
260 def _mock_get_mac(dummy):
261     global mac_seq
262     mac_seq += 1
263     return '01:00:00:00:00:%02x' % mac_seq
264
265 @patch.object(Compute, 'find_image', _mock_find_image)
266 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
267 @patch.object(ChainVnfPort, 'get_mac', _mock_get_mac)
268 @patch('nfvbench.chaining.Client')
269 @patch('nfvbench.chaining.neutronclient')
270 @patch('nfvbench.chaining.glanceclient')
271 @patch('nfvbench.nfvbench.credentials')
272 def test_nfvbench_run(mock_cred, mock_glance, mock_neutron, mock_client):
273     """Test NFVbench class with openstack+PVP."""
274     # instance = self.novaclient.servers.create(name=vmname,...)
275     # instance.status == 'ACTIVE'
276     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
277     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
278     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
279     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
280     _check_nfvbench_openstack()
281
282 @patch.object(Compute, 'find_image', _mock_find_image)
283 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
284 @patch('nfvbench.chaining.Client')
285 @patch('nfvbench.chaining.neutronclient')
286 @patch('nfvbench.chaining.glanceclient')
287 @patch('nfvbench.nfvbench.credentials')
288 def test_nfvbench_ext_arp(mock_cred, mock_glance, mock_neutron, mock_client):
289     """Test NFVbench class with openstack+EXT+ARP."""
290     # instance = self.novaclient.servers.create(name=vmname,...)
291     # instance.status == 'ACTIVE'
292     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
293     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
294     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
295     _check_nfvbench_openstack(sc=ChainType.EXT)
296
297 @patch.object(Compute, 'find_image', _mock_find_image)
298 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
299 @patch('nfvbench.chaining.Client')
300 @patch('nfvbench.chaining.neutronclient')
301 @patch('nfvbench.chaining.glanceclient')
302 @patch('nfvbench.nfvbench.credentials')
303 def test_nfvbench_l2_loopback(mock_cred, mock_glance, mock_neutron, mock_client):
304     """Test NFVbench class with l2-loopback."""
305     # instance = self.novaclient.servers.create(name=vmname,...)
306     # instance.status == 'ACTIVE'
307     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
308     _check_nfvbench_openstack(l2_loopback=True)
309
310
311 # This is a reduced version of flow stats coming from Trex
312 # with 2 chains and latency for a total of 8 packet groups
313 # Random numbers with random losses
314 CH0_P0_TX = 1234
315 CH0_P1_RX = 1200
316 CH0_P1_TX = 28900
317 CH0_P0_RX = 28000
318 LCH0_P0_TX = 167
319 LCH0_P1_RX = 130
320 LCH0_P1_TX = 523
321 LCH0_P0_RX = 490
322 CH1_P0_TX = 132344
323 CH1_P1_RX = 132004
324 CH1_P1_TX = 1289300
325 CH1_P0_RX = 1280400
326 LCH1_P0_TX = 51367
327 LCH1_P1_RX = 5730
328 LCH1_P1_TX = 35623
329 LCH1_P0_RX = 67
330
331 TREX_STATS = {
332     'flow_stats': {
333         # chain 0 port 0 normal stream
334         0: {'rx_pkts': {0: 0, 1: CH0_P1_RX, 'total': CH0_P1_RX},
335             'tx_pkts': {0: CH0_P0_TX, 1: 0, 'total': CH0_P0_TX}},
336         # chain 1 port 0 normal stream
337         1: {'rx_pkts': {0: 0, 1: CH1_P1_RX, 'total': CH1_P1_RX},
338             'tx_pkts': {0: CH1_P0_TX, 1: 0, 'total': CH1_P0_TX}},
339         # chain 0 port 1 normal stream
340         128: {'rx_pkts': {0: CH0_P0_RX, 1: 0, 'total': CH0_P0_RX},
341               'tx_pkts': {0: 0, 1: CH0_P1_TX, 'total': CH0_P1_TX}},
342         # chain 1 port 1 normal stream
343         129: {'rx_pkts': {0: CH1_P0_RX, 1: 0, 'total': CH1_P0_RX},
344               'tx_pkts': {0: 0, 1: CH1_P1_TX, 'total': CH1_P1_TX}},
345         # chain 0 port 0 latency stream
346         256: {'rx_pkts': {0: 0, 1: LCH0_P1_RX, 'total': LCH0_P1_RX},
347               'tx_pkts': {0: LCH0_P0_TX, 1: 0, 'total': LCH0_P0_TX}},
348         # chain 1 port 0 latency stream
349         257: {'rx_pkts': {0: 0, 1: LCH1_P1_RX, 'total': LCH1_P1_RX},
350               'tx_pkts': {0: LCH1_P0_TX, 1: 0, 'total': LCH1_P0_TX}},
351         # chain 0 port 1 latency stream
352         384: {'rx_pkts': {0: LCH0_P0_RX, 1: 0, 'total': LCH0_P0_RX},
353               'tx_pkts': {0: 0, 1: LCH0_P1_TX, 'total': LCH0_P1_TX}},
354         # chain 1 port 1 latency stream
355         385: {'rx_pkts': {0: LCH1_P0_RX, 1: 0, 'total': LCH1_P0_RX},
356               'tx_pkts': {0: 0, 1: LCH1_P1_TX, 'total': LCH1_P1_TX}}}}
357
358 def test_trex_streams_stats():
359     """Test TRex stats for chains 0 and 1."""
360     traffic_client = MagicMock()
361     trex = TRex(traffic_client)
362     if_stats = [InterfaceStats("p0", "dev0"), InterfaceStats("p1", "dev1")]
363     latencies = [Latency()] * 2
364     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 0)
365     assert if_stats[0].tx == CH0_P0_TX + LCH0_P0_TX
366     assert if_stats[0].rx == CH0_P0_RX + LCH0_P0_RX
367     assert if_stats[1].tx == CH0_P1_TX + LCH0_P1_TX
368     assert if_stats[1].rx == CH0_P1_RX + LCH0_P1_RX
369
370     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 1)
371     assert if_stats[0].tx == CH1_P0_TX + LCH1_P0_TX
372     assert if_stats[0].rx == CH1_P0_RX + LCH1_P0_RX
373     assert if_stats[1].tx == CH1_P1_TX + LCH1_P1_TX
374     assert if_stats[1].rx == CH1_P1_RX + LCH1_P1_RX
375
376 def check_placer(az, hyp, req_az, resolved=False):
377     """Combine multiple combinatoons of placer tests."""
378     placer = InstancePlacer(az, hyp)
379     assert placer.is_resolved() == resolved
380     assert placer.get_required_az() == req_az
381     assert placer.register_full_name('nova:comp1')
382     assert placer.is_resolved()
383     assert placer.get_required_az() == 'nova:comp1'
384
385 def test_placer_no_user_pref():
386     """Test placement when user does not provide any preference."""
387     check_placer(None, None, '')
388
389 def test_placer_user_az():
390     """Test placement when user only provides an az."""
391     check_placer('nova', None, 'nova:')
392     check_placer(None, 'nova:', 'nova:')
393     check_placer('nebula', 'nova:', 'nova:')
394
395 def test_placer_user_hyp():
396     """Test placement when user provides a hypervisor."""
397     check_placer(None, 'comp1', ':comp1')
398     check_placer('nova', 'comp1', 'nova:comp1', resolved=True)
399     check_placer(None, 'nova:comp1', 'nova:comp1', resolved=True)
400     # hyp overrides az
401     check_placer('nebula', 'nova:comp1', 'nova:comp1', resolved=True)
402     # also check for cases of extra parts (more than 1 ':')
403     check_placer('nova:nebula', 'comp1', 'nova:comp1', resolved=True)
404
405
406 def test_placer_negative():
407     """Run negative tests on placer."""
408     # AZ mismatch
409     with pytest.raises(Exception):
410         placer = InstancePlacer('nova', None)
411         placer.register('nebula:comp1')
412     # comp mismatch
413     with pytest.raises(Exception):
414         placer = InstancePlacer(None, 'comp1')
415         placer.register('nebula:comp2')
416
417
418 # without total, with total and only 2 col
419 CHAIN_STATS = [{0: {'packets': [2000054, 1999996, 1999996]}},
420                {0: {'packets': [2000054, 1999996, 1999996]},
421                 1: {'packets': [2000054, 2000054, 2000054]},
422                 'total': {'packets': [4000108, 4000050, 4000050]}},
423                {0: {'packets': [2000054, 2000054]}},
424                {0: {'packets': [2000054, 1999996]}},
425                # shared networks no drops, shared nets will have empty strings
426                {0: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
427                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
428                 'total': {'packets': [30000004, 30000004, 30000004, 30000004, 30000004, 30000004]}},
429                {0: {'packets': [15000002, '', 14000002, 14000002, '', 13000002]},
430                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
431                 'total': {'packets': [30000004, 29000004, 29000004, 29000004, 29000004, 28000004]}},
432                # example with non-available rx count in last position
433                {0: {'packets': [2000054, 1999996, None]},
434                 1: {'packets': [2000054, 2000054, None]},
435                 'total': {'packets': [4000108, 4000050, 4000050]}}]
436 XP_CHAIN_STATS = [{0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]}},
437                   {0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]},
438                    1: {'packets': [2000054, '=>', 2000054]},
439                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}},
440                   {0: {'packets': [2000054, 2000054]}},
441                   {0: {'packets': [2000054, '-58 (-0.0029%)']}},
442                   # shared net, leave spaces alone
443                   {0: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
444                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
445                    'total': {'packets': [30000004, '=>', '=>', '=>', '=>', 30000004]}},
446                   {0: {'packets': [15000002, '', '-1,000,000 (-6.6667%)', '=>', '',
447                                    '-1,000,000 (-7.1429%)']},
448                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
449                    'total': {'packets': [30000004, '-1,000,000 (-3.3333%)', '=>', '=>', '=>',
450                                          '-1,000,000 (-3.4483%)']}},
451                   {0: {'packets': [2000054, '-58 (-0.0029%)', 'n/a']},
452                    1: {'packets': [2000054, '=>', 'n/a']},
453                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}}]
454
455
456 def test_summarizer():
457     """Test Summarizer class."""
458     for stats, exp_stats in zip(CHAIN_STATS, XP_CHAIN_STATS):
459         _annotate_chain_stats(stats)
460         assert stats == exp_stats
461
462 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
463 def test_fixed_rate_no_openstack():
464     """Test FIxed Rate run - no openstack."""
465     config = _get_chain_config(ChainType.EXT, 1, True, rate='100%')
466     specs = Specs()
467     config.vlans = [100, 200]
468     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
469     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
470     config.no_arp = True
471     config['vlan_tagging'] = True
472     config['traffic'] = {'profile': 'profile_64',
473                          'bidirectional': True}
474     config['traffic_profile'] = [{'name': 'profile_64', 'l2frame_size': ['64']}]
475
476     runner = ChainRunner(config, None, specs, BasicFactory())
477     tg = runner.traffic_client.gen
478
479     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=50, max_11_tx=50)
480     # tx packets should be 50% at requested 50% line rate or higher for 64B and no drops...
481     results = runner.run()
482     assert results
483     # pprint.pprint(results['EXT']['result']['result']['64'])
484     runner.close()