Skip to content

Commit 41ba216

Browse files
Fix Python3 errors for device tests (#6670)
* Fix Python3 errors for device tests The Python3 migration didn't include fixes for local scripts in the device test tree. Fatal build and run Python errors fixed. The last update to xunitmerge is ~5 years ago, so it looks to be unsupported now. Use a local copy of the two components to allow patching to work with Python3. The serial test seems to send garbage chars (non-ASCII/etc.), so use a codepage 437 which supports all 255 chars. Fixes: #6660 * Run tests at 160MHz (req'd for some SSL connections) * Fix debuglevel options for builder * Fix Python3 interpreter path in xunitmerge * Remove virtualenv on "make clean" * Add appropriate attribution, license to xunitmerge Add like to the original sources with the author's license to the copied/fixed xunitmerge files.
1 parent 1e17ddd commit 41ba216

File tree

8 files changed

+232
-27
lines changed

8 files changed

+232
-27
lines changed

tests/device/Makefile

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ UPLOAD_PORT ?= $(shell ls /dev/tty* | grep -m 1 -i USB)
1111
UPLOAD_BAUD ?= 460800
1212
UPLOAD_BOARD ?= nodemcu
1313
BS_DIR ?= libraries/BSTest
14-
DEBUG_LEVEL ?= DebugLevel=None____
14+
DEBUG_LEVEL ?= lvl=None____
1515
#FQBN ?= esp8266com:esp8266:generic:CpuFrequency=80,FlashFreq=40,FlashMode=dio,UploadSpeed=115200,FlashSize=4M1M,LwIPVariant=v2mss536,ResetMethod=none,Debug=Serial,$(DEBUG_LEVEL)
16-
FQBN ?= esp8266com:esp8266:generic:xtal=80,FlashFreq=40,FlashMode=dio,baud=115200,eesz=4M1M,ip=lm2f,ResetMethod=none,dbg=Serial,$(DEBUG_LEVEL)
16+
FQBN ?= esp8266com:esp8266:generic:xtal=160,FlashFreq=40,FlashMode=dio,baud=115200,eesz=4M1M,ip=lm2f,ResetMethod=none,dbg=Serial,$(DEBUG_LEVEL)
1717
BUILD_TOOL := $(ARDUINO_IDE_PATH)/arduino-builder
1818
TEST_CONFIG := test_env.cfg
1919
TEST_REPORT_XML := test_report.xml
@@ -104,7 +104,7 @@ ifneq ("$(NO_RUN)","1")
104104
endif
105105

106106
$(TEST_REPORT_XML): $(HARDWARE_DIR) virtualenv
107-
$(SILENT)$(BS_DIR)/virtualenv/bin/xunitmerge $(shell find $(BUILD_DIR) -name 'test_result.xml' | xargs echo) $(TEST_REPORT_XML)
107+
$(SILENT)$(BS_DIR)/xunitmerge $(shell find $(BUILD_DIR) -name 'test_result.xml' | xargs echo) $(TEST_REPORT_XML)
108108

109109
$(TEST_REPORT_HTML): $(TEST_REPORT_XML) | virtualenv
110110
$(SILENT)$(BS_DIR)/virtualenv/bin/junit2html $< $@
@@ -124,7 +124,8 @@ virtualenv:
124124

125125
clean:
126126
rm -rf $(BUILD_DIR)
127-
rm -rf $(HARDWARE_DIR)
127+
rm -rf $(HARDWARE_DIR)A
128+
rm -rf $(BS_DIR)/virtualenv
128129
rm -f $(TEST_REPORT_HTML) $(TEST_REPORT_XML)
129130

130131
distclean: clean

tests/device/libraries/BSTest/requirements.txt

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ junit-xml
33
MarkupSafe
44
pexpect
55
pyserial
6-
xunitmerge
76
junit2html
8-
poster
7+
poster3

tests/device/libraries/BSTest/runner.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,10 @@ def request_env(self, key):
236236
def spawn_port(port_name, baudrate=115200):
237237
global ser
238238
ser = serial.serial_for_url(port_name, baudrate=baudrate)
239-
return fdpexpect.fdspawn(ser, 'wb', timeout=0)
239+
return fdpexpect.fdspawn(ser, 'wb', timeout=0, encoding='cp437')
240240

241241
def spawn_exec(name):
242-
return pexpect.spawn(name, timeout=0)
242+
return pexpect.spawn(name, timeout=0, encoding='cp437')
243243

244244
def run_tests(spawn, name, mocks, env_vars):
245245
tw = BSTestRunner(spawn, name, mocks, env_vars)
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Cloned from https://github.com/miki725/xunitmerge
2+
# to fix a Python3 error.
3+
#
4+
# xunitmerge is MIT licensed by Miroslav Shubernetskiy https://github.com/miki725
5+
#
6+
# The MIT License (MIT)
7+
#
8+
# Permission is hereby granted, free of charge, to any person obtaining a copy
9+
# of this software and associated documentation files (the "Software"), to deal
10+
# in the Software without restriction, including without limitation the rights
11+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
# copies of the Software, and to permit persons to whom the Software is
13+
# furnished to do so, subject to the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be included in
16+
# all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
# THE SOFTWARE.
25+
26+
from contextlib import contextmanager
27+
from xml.etree import ElementTree as etree
28+
from xml.sax.saxutils import quoteattr
29+
30+
import six
31+
32+
33+
CNAME_TAGS = ('system-out', 'skipped', 'error', 'failure')
34+
CNAME_PATTERN = '<![CDATA[{}]]>'
35+
TAG_PATTERN = '<{tag}{attrs}>{text}</{tag}>'
36+
37+
38+
@contextmanager
39+
def patch_etree_cname(etree):
40+
"""
41+
Patch ElementTree's _serialize_xml function so that it will
42+
write text as CDATA tag for tags tags defined in CNAME_TAGS.
43+
44+
>>> import re
45+
>>> from xml.etree import ElementTree
46+
>>> xml_string = '''
47+
... <testsuite name="nosetests" tests="1" errors="0" failures="0" skip="0">
48+
... <testcase classname="some.class.Foo" name="test_system_out" time="0.001">
49+
... <system-out>Some output here</system-out>
50+
... </testcase>
51+
... <testcase classname="some.class.Foo" name="test_skipped" time="0.001">
52+
... <skipped type="unittest.case.SkipTest" message="Skipped">Skipped</skipped>
53+
... </testcase>
54+
... <testcase classname="some.class.Foo" name="test_error" time="0.001">
55+
... <error type="KeyError" message="Error here">Error here</error>
56+
... </testcase>
57+
... <testcase classname="some.class.Foo" name="test_failure" time="0.001">
58+
... <failure type="AssertionError" message="Failure here">Failure here</failure>
59+
... </testcase>
60+
... </testsuite>
61+
... '''
62+
>>> tree = ElementTree.fromstring(xml_string)
63+
>>> with patch_etree_cname(ElementTree):
64+
... saved = str(ElementTree.tostring(tree))
65+
>>> systemout = re.findall(r'(<system-out>.*?</system-out>)', saved)[0]
66+
>>> print(systemout)
67+
<system-out><![CDATA[Some output here]]></system-out>
68+
>>> skipped = re.findall(r'(<skipped.*?</skipped>)', saved)[0]
69+
>>> print(skipped)
70+
<skipped message="Skipped" type="unittest.case.SkipTest"><![CDATA[Skipped]]></skipped>
71+
>>> error = re.findall(r'(<error.*?</error>)', saved)[0]
72+
>>> print(error)
73+
<error message="Error here" type="KeyError"><![CDATA[Error here]]></error>
74+
>>> failure = re.findall(r'(<failure.*?</failure>)', saved)[0]
75+
>>> print(failure)
76+
<failure message="Failure here" type="AssertionError"><![CDATA[Failure here]]></failure>
77+
"""
78+
original_serialize = etree._serialize_xml
79+
80+
def _serialize_xml(write, elem, *args, **kwargs):
81+
if elem.tag in CNAME_TAGS:
82+
attrs = ' '.join(
83+
['{}={}'.format(k, quoteattr(v))
84+
for k, v in sorted(elem.attrib.items())]
85+
)
86+
attrs = ' ' + attrs if attrs else ''
87+
text = CNAME_PATTERN.format(elem.text)
88+
write(TAG_PATTERN.format(
89+
tag=elem.tag,
90+
attrs=attrs,
91+
text=text
92+
))
93+
else:
94+
original_serialize(write, elem, *args, **kwargs)
95+
96+
etree._serialize_xml = etree._serialize['xml'] = _serialize_xml
97+
98+
yield
99+
100+
etree._serialize_xml = etree._serialize['xml'] = original_serialize
101+
102+
103+
def merge_trees(*trees):
104+
"""
105+
Merge all given XUnit ElementTrees into a single ElementTree.
106+
This combines all of the children test-cases and also merges
107+
all of the metadata of how many tests were executed, etc.
108+
"""
109+
first_tree = trees[0]
110+
first_root = first_tree.getroot()
111+
112+
if len(trees) == 0:
113+
return first_tree
114+
115+
for tree in trees[1:]:
116+
root = tree.getroot()
117+
118+
# append children elements (testcases)
119+
first_root.extend(root.getchildren())
120+
121+
# combine root attributes which stores the number
122+
# of executed tests, skipped tests, etc
123+
for key, value in first_root.attrib.items():
124+
if not value.isdigit():
125+
continue
126+
combined = six.text_type(int(value) + int(root.attrib.get(key, '0')))
127+
first_root.set(key, combined)
128+
129+
return first_tree
130+
131+
132+
def merge_xunit(files, output, callback=None):
133+
"""
134+
Merge the given xunit xml files into a single output xml file.
135+
136+
If callback is not None, it will be called with the merged ElementTree
137+
before the output file is written (useful for applying other fixes to
138+
the merged file). This can either modify the element tree in place (and
139+
return None) or return a completely new ElementTree to be written.
140+
"""
141+
trees = []
142+
143+
for f in files:
144+
trees.append(etree.parse(f))
145+
146+
merged = merge_trees(*trees)
147+
148+
if callback is not None:
149+
result = callback(merged)
150+
if result is not None:
151+
merged = result
152+
153+
with patch_etree_cname(etree):
154+
merged.write(output, encoding='utf-8', xml_declaration=True)
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python3
2+
3+
# Cloned from https://github.com/miki725/xunitmerge
4+
# to fix a Python3 error.
5+
#
6+
# xunitmerge is MIT licensed by Miroslav Shubernetskiy https://github.com/miki725
7+
#
8+
# The MIT License (MIT)
9+
#
10+
# Permission is hereby granted, free of charge, to any person obtaining a copy
11+
# of this software and associated documentation files (the "Software"), to deal
12+
# in the Software without restriction, including without limitation the rights
13+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
# copies of the Software, and to permit persons to whom the Software is
15+
# furnished to do so, subject to the following conditions:
16+
#
17+
# The above copyright notice and this permission notice shall be included in
18+
# all copies or substantial portions of the Software.
19+
#
20+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26+
# THE SOFTWARE.
27+
28+
import argparse
29+
from xmerge import merge_xunit
30+
31+
32+
parser = argparse.ArgumentParser(
33+
description='Utility for merging multiple XUnit xml reports '
34+
'into a single xml report.',
35+
)
36+
parser.add_argument(
37+
'report',
38+
nargs='+',
39+
type=argparse.FileType('r'),
40+
help='Path of XUnit xml report. Multiple can be provided.',
41+
)
42+
parser.add_argument(
43+
'output',
44+
help='Path where merged of XUnit will be saved.',
45+
)
46+
47+
48+
if __name__ == '__main__':
49+
args = parser.parse_args()
50+
merge_xunit(args.report, args.output)

tests/device/test_ClientContext/test_ClientContext.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/usr/bin/env python3
2+
13
from mock_decorators import setup, teardown
24
from flask import Flask, request
35
from threading import Thread
@@ -21,7 +23,7 @@ def run():
2123
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2224
for port in range(8266, 8285 + 1):
2325
try:
24-
print >>sys.stderr, 'trying port', port
26+
print ('trying port %d' %port, file=sys.stderr)
2527
server_address = ("0.0.0.0", port)
2628
sock.bind(server_address)
2729
sock.listen(1)
@@ -31,17 +33,17 @@ def run():
3133
print >>sys.stderr, 'busy'
3234
if not running:
3335
return
34-
print >>sys.stderr, 'starting up on %s port %s' % server_address
35-
print >>sys.stderr, 'waiting for connections'
36+
print ('starting up on %s port %s' % server_address, file=sys.stderr)
37+
print ( 'waiting for connections', file=sys.stderr)
3638
while running:
37-
print >>sys.stderr, 'loop'
39+
print ('loop', file=sys.stderr)
3840
readable, writable, errored = select.select([sock], [], [], 1.0)
3941
if readable:
4042
connection, client_address = sock.accept()
4143
try:
42-
print >>sys.stderr, 'client connected:', client_address
44+
print('client connected: %s' % str(client_address), file=sys.stderr)
4345
finally:
44-
print >>sys.stderr, 'close'
46+
print ('close', file=sys.stderr)
4547
connection.shutdown(socket.SHUT_RDWR)
4648
connection.close()
4749

@@ -54,7 +56,7 @@ def teardown_tcpsrv(e):
5456
global thread
5557
global running
5658

57-
print >>sys.stderr, 'closing'
59+
print ('closing', file=sys.stderr)
5860
running = False
5961
thread.join()
6062
return 0

tests/device/test_http_client/test_http_client.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from mock_decorators import setup, teardown
22
from flask import Flask, request, redirect
33
from threading import Thread
4-
import urllib2
4+
import urllib
55
import os
66
import ssl
77
import time
@@ -20,7 +20,7 @@ def shutdown():
2020
return 'Server shutting down...'
2121
@app.route("/", methods = ['GET', 'POST'])
2222
def root():
23-
print('Got data: ' + request.data);
23+
print('Got data: ' + request.data.decode());
2424
return 'hello!!!'
2525
@app.route("/data")
2626
def get_data():
@@ -48,7 +48,7 @@ def flaskThread():
4848

4949
@teardown('HTTP GET & POST requests')
5050
def teardown_http_get(e):
51-
response = urllib2.urlopen('http://localhost:8088/shutdown')
51+
response = urllib.request.urlopen('http://localhost:8088/shutdown')
5252
html = response.read()
5353
time.sleep(1) # avoid address in use error on macOS
5454

@@ -86,6 +86,6 @@ def teardown_http_get(e):
8686
ctx.check_hostname = False
8787
ctx.verify_mode = ssl.CERT_NONE
8888
p = os.path.dirname(os.path.abspath(__file__))
89-
response = urllib2.urlopen('https://localhost:8088/shutdown', context=ctx)
89+
response = urllib.request.urlopen('https://localhost:8088/shutdown', context=ctx)
9090
html = response.read()
9191

tests/device/test_http_server/test_http_server.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from collections import OrderedDict
22
from mock_decorators import setup, teardown
33
from threading import Thread
4-
from poster.encode import MultipartParam
5-
from poster.encode import multipart_encode
6-
from poster.streaminghttp import register_openers
7-
import urllib2
4+
from poster3.encode import MultipartParam
5+
from poster3.encode import multipart_encode
6+
from poster3.streaminghttp import register_openers
87
import urllib
98

109
def http_test(res, url, get=None, post=None):
@@ -13,8 +12,8 @@ def http_test(res, url, get=None, post=None):
1312
if get:
1413
url += '?' + urllib.urlencode(get)
1514
if post:
16-
post = urllib.urlencode(post)
17-
request = urllib2.urlopen(url, post, 2)
15+
post = urllib.parse.quote(post)
16+
request = urllib.request.urlopen(url, post, 2)
1817
response = request.read()
1918
except:
2019
return 1
@@ -60,8 +59,8 @@ def testRun():
6059
register_openers()
6160
p = MultipartParam("file", "0123456789abcdef", "test.txt", "text/plain; charset=utf8")
6261
datagen, headers = multipart_encode( [("var4", "val with spaces"), p] )
63-
request = urllib2.Request('http://etd.local/upload', datagen, headers)
64-
response = urllib2.urlopen(request, None, 2).read()
62+
request = urllib.request('http://etd.local/upload', datagen, headers)
63+
response = urllib.request.urlopen(request, None, 2).read()
6564
except:
6665
return 1
6766
if response != 'test.txt:16\nvar4 = val with spaces':

0 commit comments

Comments
 (0)