Skip to content
This repository was archived by the owner on Dec 26, 2022. It is now read-only.

Commit 3d9ef90

Browse files
committed
feat(MQTT): Implement MQTT regression test
Close #421. We share the same test cases between http connection method and mqtt ones. We could add more test suites easier later in this way.
1 parent cc69983 commit 3d9ef90

13 files changed

+313
-25
lines changed

tests/regression/common.py

Lines changed: 183 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,75 @@
1+
import time
2+
import re
13
import sys
24
import json
5+
import string
36
import random
47
import logging
58
import requests
69
import statistics
710
import subprocess
811
import argparse
12+
import paho.mqtt.publish as publish
13+
import paho.mqtt.subscribe as subscribe
14+
from multiprocessing import Pool
15+
from multiprocessing.context import TimeoutError
916

1017
TIMES_TOTAL = 100
1118
TIMEOUT = 100 # [sec]
19+
MQTT_RECV_TIMEOUT = 30
1220
STATUS_CODE_500 = "500"
1321
STATUS_CODE_405 = "405"
1422
STATUS_CODE_404 = "404"
1523
STATUS_CODE_400 = "400"
1624
STATUS_CODE_200 = "200"
25+
STATUS_CODE_ERR = "-1"
1726
EMPTY_REPLY = "000"
1827
LEN_TAG = 27
1928
LEN_ADDR = 81
2029
LEN_MSG_SIGN = 2187
2130
TRYTE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ9"
2231
URL = ""
32+
DEVICE_ID = None
33+
CONNECTION_METHOD = None
2334

2435

2536
def parse_cli_arg():
2637
global URL
38+
global CONNECTION_METHOD
39+
global DEVICE_ID
40+
rand_device_id = ''.join(
41+
random.choice(string.printable[:62]) for _ in range(32))
2742
parser = argparse.ArgumentParser('Regression test runner program')
2843
parser.add_argument('-u',
2944
'--url',
3045
dest='raw_url',
3146
default="localhost:8000")
3247
parser.add_argument('-d', '--debug', dest="debug", action="store_true")
3348
parser.add_argument('--nostat', dest="no_stat", action="store_true")
49+
parser.add_argument('--mqtt', dest="enable_mqtt", action="store_true")
50+
parser.add_argument('--device_id',
51+
dest="device_id",
52+
default=rand_device_id)
3453
args = parser.parse_args()
3554

55+
# Determine whether to use full time statistic or not
3656
if args.no_stat:
3757
global TIMES_TOTAL
3858
TIMES_TOTAL = 2
59+
# Determine connection method
60+
if args.enable_mqtt:
61+
CONNECTION_METHOD = "mqtt"
62+
URL = "localhost"
63+
else:
64+
CONNECTION_METHOD = "http"
65+
URL = "http://" + args.raw_url
66+
# Run with debug mode or not
3967
if args.debug:
4068
logging.basicConfig(level=logging.DEBUG)
4169
else:
4270
logging.basicConfig(level=logging.INFO)
43-
URL = "http://" + args.raw_url
71+
# Configure connection destination
72+
DEVICE_ID = args.device_id
4473

4574

4675
def eval_stat(time_cost, func_name):
@@ -69,11 +98,15 @@ def test_logger(f):
6998
logger = logging.getLogger(f.__module__)
7099
name = f.__name__
71100

72-
def decorate(instance):
73-
logger.debug(f"Testing case = {name}")
74-
return instance
101+
def decorate(*args, **kwargs):
102+
bg_color = "\033[48;5;38m"
103+
fg_color = "\033[38;5;16m"
104+
clear_color = "\033[0m"
105+
logger.info(f"{bg_color}{fg_color}{name}{clear_color}")
106+
res = f(*args, **kwargs)
107+
return res
75108

76-
return decorate(f)
109+
return decorate
77110

78111

79112
def valid_trytes(trytes, trytes_len):
@@ -94,7 +127,98 @@ def map_field(key, value):
94127
return json.dumps(ret)
95128

96129

130+
# Simulate random field to mqtt since we cannot put the information in the route
131+
def add_random_field(post):
132+
return data
133+
134+
135+
def route_http_to_mqtt(query, get_data, post_data):
136+
data = {}
137+
if get_data: query += get_data
138+
if post_data:
139+
data.update(json.loads(post_data))
140+
if query[-1] == "/": query = query[:-1] # Remove trailing slash
141+
142+
# api_generate_address
143+
r = re.search("/address$", query)
144+
if r is not None:
145+
return query, data
146+
147+
# api_find_transactions_by_tag
148+
r = re.search(f"/tag/(?P<tag>[\x00-\xff]*?)/hashes$", query)
149+
if r is not None:
150+
tag = r.group("tag")
151+
data.update({"tag": tag})
152+
query = "/tag/hashes"
153+
return query, data
154+
155+
# api_find_transactions_object_by_tag
156+
r = re.search(f"/tag/(?P<tag>[\x00-\xff]*?)$", query)
157+
if r is not None:
158+
tag = r.group("tag")
159+
data.update({"tag": tag})
160+
query = "/tag/object"
161+
return query, data
162+
163+
# api_find_transacion_object
164+
r = re.search(f"/transaction/object$", query)
165+
if r is not None:
166+
query = "/transaction/object"
167+
return query, data
168+
169+
r = re.search(f"/transaction/(?P<hash>[\u0000-\uffff]*?)$", query)
170+
if r is not None:
171+
hash = r.group("hash")
172+
data.update({"hash": hash})
173+
query = f"/transaction"
174+
return query, data
175+
176+
# api_send_transfer
177+
r = re.search(f"/transaction$", query)
178+
if r is not None:
179+
query = "/transaction/send"
180+
return query, data
181+
182+
# api_get_tips
183+
r = re.search(f"/tips$", query)
184+
if r is not None:
185+
query = "/tips/all"
186+
return query, data
187+
188+
# api_get_tips_pair
189+
r = re.search(f"/tips/pair$", query)
190+
if r is not None:
191+
return query, data
192+
193+
# api_send_trytes
194+
r = re.search(f"/tryte$", query)
195+
if r is not None:
196+
return query, data
197+
198+
# Error, cannot identify route (return directly from regression test)
199+
return None, None
200+
201+
97202
def API(get_query, get_data=None, post_data=None):
203+
global CONNECTION_METHOD
204+
assert CONNECTION_METHOD != None
205+
if CONNECTION_METHOD == "http":
206+
return _API_http(get_query, get_data, post_data)
207+
elif CONNECTION_METHOD == "mqtt":
208+
query, data = route_http_to_mqtt(get_query, get_data, post_data)
209+
if (query, data) == (None, None):
210+
msg = {
211+
"message":
212+
"Cannot identify route, directly return from regression test",
213+
"status_code": STATUS_CODE_400
214+
}
215+
logging.debug(msg)
216+
return msg
217+
218+
return _API_mqtt(query, data)
219+
220+
221+
def _API_http(get_query, get_data, post_data):
98222
global URL
99223
command = "curl {} -X POST -H 'Content-Type: application/json' -w \", %{{http_code}}\" -d '{}'"
100224
try:
@@ -130,3 +254,57 @@ def API(get_query, get_data=None, post_data=None):
130254
logging.debug(f"Command = {command}, response = {response}")
131255

132256
return response
257+
258+
259+
def _subscribe(get_query):
260+
add_slash = ""
261+
if get_query[-1] != "/": add_slash = "/"
262+
topic = f"root/topics{get_query}{add_slash}{DEVICE_ID}"
263+
logging.debug(f"Subscribe topic: {topic}")
264+
265+
return subscribe.simple(topics=topic, hostname=URL, qos=1).payload
266+
267+
268+
def _API_mqtt(get_query, data):
269+
global URL, DEVICE_ID
270+
data.update({"device_id": DEVICE_ID})
271+
272+
# Put subscriber in a thread since it is a blocking function
273+
with Pool() as p:
274+
payload = p.apply_async(_subscribe, [get_query])
275+
topic = f"root/topics{get_query}"
276+
logging.debug(f"Publish topic: {topic}, data: {data}")
277+
278+
# Prevents publish execute earlier than subscribe
279+
time.sleep(0.1)
280+
281+
# Publish requests
282+
publish.single(topic, json.dumps(data), hostname=URL, qos=1)
283+
msg = {}
284+
try:
285+
res = payload.get(MQTT_RECV_TIMEOUT)
286+
msg = json.loads(res)
287+
288+
if type(msg) is dict and "message" in msg.keys():
289+
content = msg["message"]
290+
if content == "Internal service error":
291+
msg.update({"status_code": STATUS_CODE_500})
292+
elif content == "Request not found":
293+
msg.update({"status_code": STATUS_CODE_404})
294+
elif content == "Invalid path" or content == "Invalid request header":
295+
msg.update({"status_code": STATUS_CODE_400})
296+
else:
297+
msg.update({"status_code": STATUS_CODE_200})
298+
else:
299+
msg = {
300+
"content": json.dumps(msg),
301+
"status_code": STATUS_CODE_200
302+
}
303+
except TimeoutError:
304+
msg = {
305+
"content": "Time limit exceed",
306+
"status_code": STATUS_CODE_ERR
307+
}
308+
309+
logging.debug(f"Modified response: {msg}")
310+
return msg

tests/regression/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
certifi==2019.11.28
22
chardet==3.0.4
33
idna==2.7
4+
paho-mqtt==1.5.0
45
requests==2.20.0
56
urllib3==1.24.3

tests/regression/run-api-with-mqtt.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ for (( i = 0; i < ${#OPTIONS[@]}; i++ )); do
2020
cli_arg=${option} | cut -d '|' -f 1
2121
build_arg=${option} | cut -d '|' -f 2
2222

23-
bazel run accelerator ${build_arg} -- --ta_port=${TA_PORT} ${cli_arg} &
23+
bazel run accelerator --define mqtt=enable ${build_arg} -- --quiet --ta_port=${TA_PORT} ${cli_arg} &
2424
TA=$!
2525
sleep ${sleep_time} # TA takes time to be built
2626
trap "kill -9 ${TA};" INT # Trap SIGINT from Ctrl-C to stop TA
2727

28-
python3 tests/regression/runner.py ${remaining_args} --url localhost:${TA_PORT}
28+
python3 tests/regression/runner.py ${remaining_args} --url "localhost" --mqtt
2929
rc=$?
3030

3131
if [ $rc -ne 0 ]

tests/regression/runner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323

2424
suite_path = os.path.join(os.path.dirname(__file__), "test_suite")
2525
sys.path.append(suite_path)
26+
unsuccessful_module = []
2627
for module in os.listdir(suite_path):
2728
if module[-3:] == ".py":
2829
mod = __import__(module[:-3], locals(), globals())
2930
suite = unittest.TestLoader().loadTestsFromModule(mod)
30-
result = unittest.TextTestRunner().run(suite)
31+
result = unittest.TextTestRunner(verbosity=0).run(suite)
3132
if not result.wasSuccessful():
32-
exit(1)
33+
unsuccessful_module.append(module)
34+
35+
if len(unsuccessful_module):
36+
print(f"Error module: {unsuccessful_module}")
37+
exit(1)

tests/regression/test_suite/find_transactions_hash_by_tag.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,16 @@ def test_time_statistics(self):
6767
eval_stat(time_cost, "find transactions by tag")
6868

6969
@classmethod
70+
@test_logger
7071
def setUpClass(cls):
7172
rand_trytes_26 = gen_rand_trytes(26)
7273
rand_tag = gen_rand_trytes(LEN_TAG)
7374
rand_addr = gen_rand_trytes(LEN_ADDR)
7475
rand_msg = gen_rand_trytes(30)
7576
rand_len = random.randrange(28, 50)
7677
rand_len_trytes = gen_rand_trytes(rand_len)
77-
FindTransactionsHashByTag()._send_transaction(
78-
rand_msg, rand_tag, rand_addr)
78+
FindTransactionsHashByTag()._send_transaction(rand_msg, rand_tag,
79+
rand_addr)
7980
cls.query_string = [
8081
rand_tag, "", f"{rand_trytes_26}@", f"{rand_tag}\x00",
8182
f"{rand_tag}\x00{rand_tag}", "一二三四五", rand_len_trytes

tests/regression/test_suite/find_transactions_object_by_tag.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def test_time_statistics(self):
6767
eval_stat(time_cost, "find transactions objects by tag")
6868

6969
@classmethod
70+
@test_logger
7071
def setUpClass(cls):
7172
rand_trytes_26 = gen_rand_trytes(26)
7273
rand_tag = gen_rand_trytes(LEN_TAG)

tests/regression/test_suite/generate_address.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_time_statistics(self):
4040
eval_stat(time_cost, "generate_address")
4141

4242
@classmethod
43+
@test_logger
4344
def setUpClass(cls):
4445
rand_tag_27 = gen_rand_trytes(27)
4546
cls.query_string = ["", rand_tag_27, "飛天義大利麵神教"]

tests/regression/test_suite/get_tips.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_time_statistics(self):
4141
eval_stat(time_cost, "get_tips")
4242

4343
@classmethod
44+
@test_logger
4445
def setUpClass(cls):
4546
rand_tag_27 = gen_rand_trytes(27)
4647
cls.query_string = ["", rand_tag_27, "飛天義大利麵神教"]

tests/regression/test_suite/get_tips_pair.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_time_statistics(self):
4141
eval_stat(time_cost, "get tips pair")
4242

4343
@classmethod
44+
@test_logger
4445
def setUpClass(cls):
4546
rand_tag_27 = gen_rand_trytes(27)
4647
cls.query_string = ["", rand_tag_27, "飛天義大利麵神教"]

tests/regression/test_suite/get_transactions_object.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ class GetTransactionsObject(unittest.TestCase):
1212
def test_81_trytes_hash(self):
1313
res = API("/transaction/object",
1414
post_data=map_field(self.post_field, [self.query_string[0]]))
15-
self._verify_pass(res, 0)
15+
self._verify_pass(res, idx=0)
1616

1717
# Multiple 81 trytes transaction hash (pass)
1818
@test_logger
1919
def test_mult_81_trytes_hash(self):
2020
res = API("/transaction/object",
2121
post_data=map_field(self.post_field, [self.query_string[1]]))
22-
self._verify_pass(res, 1)
22+
self._verify_pass(res, idx=1)
2323

2424
# 20 trytes transaction hash (fail)
2525
@test_logger
@@ -33,7 +33,7 @@ def test_20_trytes_hash(self):
3333
def test_100_trytes_hash(self):
3434
res = API("/transaction/object",
3535
post_data=map_field(self.post_field, [self.query_string[3]]))
36-
self.assertEqual(STATUS_CODE_500, res["status_code"])
36+
self.assertEqual(STATUS_CODE_404, res["status_code"])
3737

3838
# Unicode transaction hash (fail)
3939
@test_logger
@@ -62,6 +62,7 @@ def test_time_statistics(self):
6262
eval_stat(time_cost, "find transaction objects")
6363

6464
@classmethod
65+
@test_logger
6566
def setUpClass(cls):
6667
sent_txn_tmp = []
6768
for i in range(3):
@@ -85,8 +86,8 @@ def setUpClass(cls):
8586
cls.response_field = []
8687
cls.query_string = [[sent_txn_tmp[0]["hash"]],
8788
[sent_txn_tmp[1]["hash"], sent_txn_tmp[2]["hash"]],
88-
gen_rand_trytes(19),
89-
gen_rand_trytes(100), "工程師批哩趴啦的生活", ""]
89+
[gen_rand_trytes(20)], [gen_rand_trytes(100)],
90+
["工程師批哩趴啦的生活"], [""]]
9091

9192
def _verify_pass(self, res, idx):
9293
expected_txns = self.sent_txn[idx]

0 commit comments

Comments
 (0)