-
Notifications
You must be signed in to change notification settings - Fork 258
Expand file tree
/
Copy pathrun_unit_tests.py
More file actions
908 lines (722 loc) · 33.8 KB
/
Copy pathrun_unit_tests.py
File metadata and controls
908 lines (722 loc) · 33.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
#! /usr/bin/python
# Copyright (c) 2016 Freescale Semiconductor, Inc.
# Copyright 2016-2024 NXP
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import argparse
import itertools
import os
import re
import shutil
import sys
from abc import ABC, abstractmethod
from datetime import datetime
from pathlib import Path
from subprocess import run, Popen, TimeoutExpired, PIPE
from time import sleep, time
from typing import Any, Type
from xml.etree import ElementTree
def to_pascal_case(text: str, separator: str = "_") -> str:
return "".join([i.capitalize() for i in text.split("_")])
class TestResult:
def __init__(self):
self.skipped: set[str] = set()
self.passed: set[str] = set()
self.failed: set[str] = set()
self.test_suit_name: str | None = None
@property
def all(self) -> set[str]:
return self.skipped.union(self.passed, self.failed)
@property
def return_code(self) -> int:
return len(self.failed)
def set_test_suit_name(self, test_suit_name: str):
self.test_suit_name = test_suit_name
def add_failed(self, test_case: str):
self.failed.add(test_case)
def add_passed(self, test_case: str):
self.passed.add(test_case)
def add_skipped(self, test_case: str):
self.skipped.add(test_case)
def print_result(self):
padding = 40
print(f"{'':*^{padding}}")
print(f"* {'TEST RESULTS':^{padding - 4}} *")
if self.test_suit_name:
print(f"* {self.test_suit_name:^{padding - 4}} *")
print(f"{'':*^{padding}}")
print(f"\n{len(self.passed)}/{len(self.all)} passed, "
f"{len(self.failed)} failed, "
f"{len(self.skipped)} skipped.")
if len(self.failed) != 0:
print(f"\n{bcolors.RED}Failed tests:")
print("\n".join(f"- {t}" for t in self.failed))
print(bcolors.ENDC)
if len(self.skipped) != 0:
print(f"\n{bcolors.ORANGE}Skipped tests:")
print("\n".join(f"- {t}" for t in self.skipped))
print(bcolors.ENDC)
if len(self.failed) == 0:
print(f"\n{bcolors.GREEN}All tests passed.{bcolors.ENDC}")
class AbstractTests(ABC):
"""
Base class for testing eRPC with different languages.
Define this abstract functions:
- client(): This function should start the eRPC client with test cases
- test(): This function should start the eRPC server
- test_case_server_exist(), test_case_client_exist(): Return if client/server implementation in given language exists
- init_tests(): Init anything that concreate class need to run tests (build, flash, download, ...)
How to use:
- Init client and server with all arguments
- Iter through client's test_cases
- call init_test(test_case, test_suit_name, transport) on client and server
- Check if test case exist for client/server
- call server() on given implementation
- call client() on given implementation
- call wait_client() to get results and wait for client's termination
- call wait_servet() to get results (if server exited correctly)
"""
test_dir_regex = re.compile("test_*")
def __init__(self, result_dir: str | None = None, build_dir: str | None = None, pristine: bool = False,
test_cases: set[str] | None = None, add_test_prefix: bool = False, erpcgen: str | None = None,
**kwargs: dict[str, Any]) -> None:
self.test_dir = Path(os.path.dirname(os.path.abspath(__file__)))
self.erpc_dir = self.test_dir.parent
self.result_dir = Path(result_dir).absolute() if result_dir else self.test_dir.joinpath("results")
self.build_dir = Path(build_dir).absolute() if build_dir else self.erpc_dir.joinpath("build")
if erpcgen:
self.erpcgen_executable = Path(erpcgen).absolute()
elif erpcgen := shutil.which("erpcgen"):
self.erpcgen_executable = Path(erpcgen)
elif self.executable_exists(self.build_dir.joinpath("erpcgen/").joinpath("erpcgen")):
self.erpcgen_executable = self.build_dir.joinpath("erpcgen/").joinpath("erpcgen")
else:
self.erpcgen_executable = None
self.add_test_prefix = add_test_prefix
if not self.result_dir.exists():
self.result_dir.mkdir()
self.pristine = pristine
if test_cases:
self.test_cases = set(test_cases)
else:
self.test_cases = set(self.test_dir.joinpath(p).name for p in os.listdir(self.test_dir) if
self.is_test_dir(self.test_dir.joinpath(p)))
self.server_process: Popen[bytes] | None = None
self.client_process: Popen[bytes] | None = None
self._test_case: str | None = None
self._test_suit_name: str | None = None
self._transport: str | None = None
self.client_extra_parameters: list[str] = []
self.server_extra_parameters: list[str] = []
@property
def test_case(self) -> str:
if self._test_case is None:
raise RuntimeError("Call init_test before client/server.")
return self._test_case
@test_case.setter
def test_case(self, value: str) -> None:
self._test_case = value
@property
def test_suit_name(self) -> str:
if self._test_suit_name is None:
raise RuntimeError("Call init_test before client/server.")
return self._test_suit_name
@test_suit_name.setter
def test_suit_name(self, value: str) -> None:
self._test_suit_name = value
@property
def transport(self) -> str:
if self._transport is None:
raise RuntimeError("Call init_test before client/server.")
return self._transport
@transport.setter
def transport(self, value: str) -> None:
self._transport = value
def init_test(self, test_case: str, test_suit_name: str, transport: str) -> bool:
"""
Initialise next test case values. This function have to be called before each test case.
Can be overridden, bud the function must call super().init_test(test_case, test_suit_name, transport).
@param test_case: Name of the test case
@param test_suit_name: Name of the test suit (tcp_c_c, ...)
@param transport: tcp/serial
@return: Result
"""
self.test_case = test_case
self.test_suit_name = test_suit_name
self.transport = transport
return True
def add_client_parameter(self, parameter: str):
self.client_extra_parameters.append(parameter)
def add_server_parameter(self, parameter: str):
self.server_extra_parameters.append(parameter)
def wait_server(self, timeout: int = 5) -> bool:
"""
Wait for server to termine, otherwise kill the process and return False
@param timeout: Server wait timeout
@return: True if server terminate itself and correctly, False otherwise
"""
if self.server_process is None:
return True
try:
result = self.server_process.wait(timeout) == 0
except TimeoutExpired:
print(f"{bcolors.RED}SERVER TIMEOUT{bcolors.ENDC}")
self.server_process.kill()
result = False
self.server_process = None
return result
def wait_client(self, timeout: int = 5) -> bool:
"""
Wait for client to termine, otherwise kill the process and return False
@param timeout: Client wait timeout
@return: True if client terminate itself and correctly, False otherwise
"""
if self.client_process is None:
return True
try:
result = self.client_process.wait(timeout) == 0
except TimeoutExpired:
print(f"{bcolors.RED}CLIENT TIMEOUT{bcolors.ENDC}")
self.client_process.kill()
self.create_failed_test_suite_xml(
self.test_case,
self.get_result_file()
)
result = False
result_file = self.get_result_file()
if self.add_test_prefix:
self.update_xml_testcases_name(
result_file,
self.test_suit_name
)
self.client_process = None
return result
def get_result_file(self) -> Path:
"""
Get Path to result xml file base on result_dir, test_suit_name and test_case
@return: Path
"""
return self.result_dir.joinpath(f"{self.test_suit_name}_{self.test_case}.xml")
@abstractmethod
def init_tests(self) -> bool:
pass
@abstractmethod
def generate_shim_code(self):
pass
@abstractmethod
def test_case_client_exist(self, test_case: str) -> bool:
"""
Return true if given test_case exists, false otherwise
"""
pass
@abstractmethod
def test_case_server_exist(self, test_case: str) -> bool:
"""
Return true if given test_case exists, false otherwise
"""
pass
@abstractmethod
def client(self) -> None:
"""
Client implementation of given language. It is called for each test case.
Can set self.client_process and then client_wait() to wait for process to termine.
self.client_extra_parameters can be used to modify client command from global scope.
"""
pass
@abstractmethod
def server(self) -> None:
"""
Client implementation of given language. It is called for each test case.
Can set self.server_process and then server_wait() to wait for process to terminate.
self.server_extra_parameters can be used to modify client command from global scope.
"""
pass
@staticmethod
def is_test_dir(directory: Path):
return directory.is_dir and AbstractTests.test_dir_regex.match(directory.name)
@staticmethod
def create_failed_test_suite_xml(test_case: str, result_file: Path):
"""
Create empty failed xml result if test timeout.
@param test_case: Name of the test_case
@param result_file: Output file
"""
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="0" failures="1" disabled="0" errors="0" timestamp="{datetime.now().strftime("%Y-%m-%dT%H:%M:%S")}" time="0.000" name="AllTests">
<testsuite name="{test_case}" tests="0" failures="1" disabled="0" errors="0" time="0.000">
<testcase name="{test_case}" status="run" time="0.015" classname="{test_case}" />
</testsuite>
</testsuites>
"""
with open(result_file, mode="w", encoding="utf8") as fp:
fp.write(xml)
@staticmethod
def update_xml_testcases_name(xml_file: Path, prefix: str = "", suffix: str = ""):
"""
Update xml testcase name with prefix and suffix for better organization.
(Simpler thant trying to change the name in pytest/gtest)
@param xml_file: Path to the xml report
@param prefix: Test case prefix
@param suffix: Test case suffix
"""
tree = ElementTree.parse(xml_file)
root = tree.getroot()
for testsuite in root.findall("testsuite") if root.tag != "testsuite" else [root]:
for testcase in testsuite.findall("testcase"):
testcase_name = testcase.get("name")
testcase.set(
"name",
f"{prefix}{'_' if prefix else ''}{testcase_name}{'_' if suffix else ''}{suffix}"
)
tree.write(xml_file)
@staticmethod
def executable_exists(path: Path) -> bool:
return path.exists() or Path(str(path) + ".exe").exists()
def erpcgen(self, language: str, output_dir: str | Path, idl_file: str | Path, package: str | None = None,
cwd: str | Path | None = None):
erpcgen_command = [
self.erpcgen_executable,
f"-g{language}",
f"-o{output_dir}",
idl_file
]
if package:
erpcgen_command.insert(1, f"-p{package}")
print(f"{bcolors.BLUE}Generating: '{' '.join(str(x) for x in erpcgen_command)}' in {cwd}{bcolors.ENDC}")
erpcgen_proc = Popen(erpcgen_command, cwd=cwd)
res = erpcgen_proc.wait(10)
if res != 0:
raise RuntimeError("Erpcgen execution failed.")
class PythonTests(AbstractTests):
def __init__(self, tcp_port: int, serial_port: str | None = None, **kwargs: dict[str, Any]) -> None:
super().__init__(**kwargs)
self.tcp_port = tcp_port
self.serial_port = serial_port
def init_tests(self) -> bool:
return True
def test_case_client_exist(self, test_case: str) -> bool:
return self.pytest_test_path(test_case).exists()
def test_case_server_exist(self, test_case: str) -> bool:
return self.pytest_test_path(test_case).exists()
def client(self) -> None:
result_file = self.get_result_file()
pytest_command = [
"pytest", str(self.pytest_test_path(self.test_case)),
f"--junitxml={result_file}",
"--client",
*self.client_extra_parameters
]
if self.transport == "tcp":
pytest_command.extend(["--port", str(self.tcp_port)])
elif self.transport == "serial" and self.serial_port:
pytest_command.extend(["--serial", self.serial_port])
else:
raise ValueError
self.client_process = Popen(pytest_command)
def server(self) -> None:
pytest_command = [
"pytest", str(self.pytest_test_path(self.test_case)),
"--server",
*self.server_extra_parameters
]
if self.transport == "tcp":
pytest_command.extend(["--port", str(self.tcp_port)])
elif self.transport == "serial" and self.serial_port:
pytest_command.extend(["--serial", self.serial_port])
else:
raise ValueError
self.server_process = Popen(pytest_command)
sleep(0.5)
def pytest_test_path(self, test_case: str) -> Path:
return self.test_dir.joinpath("python_impl_tests").joinpath(test_case)
def generate_shim_code(self):
for case in self.test_cases:
if self.test_case_client_exist(case) or self.test_case_server_exist(case):
self.erpcgen(
"py",
self.test_dir.joinpath(f"python_impl_tests/{case}/service/"),
self.test_dir.joinpath(f"{case}/{case}.erpc"),
cwd=self.test_dir.joinpath(f"python_impl_tests/")
)
class CTests(AbstractTests):
def __init__(self, c_compiler: str | None = None, cxx_compiler: str | None = None, target: str | None = None,
build_all: bool = False, conf_file: str | None = None, menuconfig: bool = False,
**kwargs: dict[str, Any]) -> None:
super().__init__(**kwargs)
self.menuconfig = menuconfig
self.c_compiler = c_compiler
self.cxx_compiler = cxx_compiler
self.target = target
self.build_all = build_all
self.conf_file = Path(conf_file).absolute() if conf_file else self.test_dir.joinpath("prj.conf")
def init_tests(self) -> bool:
"""
Generate cmake project and build test one by one or all together if --build/-b is selected.
If --menuconfig is enabled, show menu config before build.
@return: set of built tests
"""
if self.pristine and self.build_dir.exists():
print(f"{bcolors.ORANGE}Pristine enabled, removing {self.build_dir}.{bcolors.ENDC}")
shutil.rmtree(self.build_dir)
if not self.build_dir.exists():
self.build_dir.mkdir()
if self.cmake_generate() != 0:
print(f"{bcolors.RED}CMake project generation FAILED.{bcolors.ENDC}")
return False
if self.menuconfig:
if self.cmake_build_target("menuconfig") != 0:
print(f"{bcolors.RED}Building menuconfig for Kconfig FAILED.{bcolors.ENDC}")
return False
if self.build_all:
if self.cmake_build_target("test_all") != 0:
print(f"{bcolors.RED}Tests build FAILED.{bcolors.ENDC}")
return False
else:
for test_case in self.test_cases:
print(f"{bcolors.BLUE}Building: {test_case}{bcolors.ENDC}")
if self.cmake_build_target(test_case) != 0:
print(f"{bcolors.RED}Test'{test_case}' build FAILED.{bcolors.ENDC}")
return True
def test_case_client_exist(self, test_case: str) -> bool:
return self.executable_exists(self.get_test_executable(test_case, "client", self.transport))
def test_case_server_exist(self, test_case: str) -> bool:
return self.executable_exists(self.get_test_executable(test_case, "server", self.transport))
def client(self) -> None:
result_file = self.get_result_file()
client_executable = self.get_test_executable(self.test_case, "client", self.transport)
self.client_process = Popen(
[client_executable, f"--gtest_output=xml:{result_file}", *self.client_extra_parameters])
def server(self) -> None:
server_executable = self.get_test_executable(self.test_case, "server", self.transport)
self.server_process = Popen([server_executable, *self.server_extra_parameters])
sleep(0.5) # Wait for server to start
def get_test_executable(self, test: str, test_type: str, transport: str) -> Path:
"""
Get path to executable of given test, type and transport (work for C++ tests)
@param test: Name of the test
@param test_type: client/server
@param transport: tcp/serial
@return: Path to the executable
"""
executable = self.build_dir.joinpath("test").joinpath(test).joinpath(f"{test}_{test_type}_{transport}")
return executable
def cmake_generate(self) -> int:
"""
Generate cmake project in build dir from erpc as source. Conf file, C and C++ compiler can be defined
@return: Command return code
"""
cmake_command = ["cmake", "-S", str(self.erpc_dir), "-B", str(self.build_dir),
f"-DCMAKE_BUILD_TYPE={self.target}"]
if self.conf_file:
cmake_command.append(f"-DCONF_FILE='{self.conf_file}'")
if self.c_compiler:
cmake_command.append(f"-DCMAKE_C_COMPILER={self.c_compiler}")
if self.c_compiler:
cmake_command.append(f"-DCMAKE_CXX_COMPILER={self.cxx_compiler}")
return run(cmake_command).returncode
def cmake_build_target(self, target: str | None = None) -> int:
"""
Build given target using cmake in build dir
@param target: Target to build or build all
@return: Command return code
"""
if target:
ret = run(["cmake", "--build", str(self.build_dir), "--target", f"{target}"])
else:
ret = run(["cmake", "--build", str(self.build_dir)])
return ret.returncode
def generate_shim_code(self):
pass
class ZephyrTests(AbstractTests):
def __init__(self, zephyr_base: str, serial_port: int, board: str, **kwargs: dict[str, Any]):
super().__init__(**kwargs)
# Zephyr options
self.zephyr_base = Path(zephyr_base)
self.zephyr_erpc = self.zephyr_base.joinpath("modules/lib/erpc/")
self.zephyr_erpc_test = self.zephyr_erpc.joinpath("test/zephyr/uart")
self.serial_port = serial_port
self.board = board
def init_tests(self) -> bool:
return True
def init_test(self, test_case: str, test_suit_name: str, transport: str) -> bool:
super().init_test(test_case, test_suit_name, transport)
cwd = os.getcwd()
os.chdir(self.zephyr_base)
build_dir = Path(f"build/{test_case}").absolute()
if self.pristine and build_dir.exists():
print(f"{bcolors.ORANGE}Pristine enabled, removing Zephyr {str(build_dir)}.{bcolors.ENDC}")
shutil.rmtree(build_dir)
build_command = ["west", "build", "-b", self.board, f"./modules/lib/erpc/test/zephyr/uart/{test_case}", "-d",
f"build/{test_case}"]
flash_command = ["west", "flash", "--build-dir", str(build_dir)]
if run(build_command).returncode != 0:
print(f"{bcolors.RED}Zephyr west build for {test_case} FAILED")
os.chdir(cwd)
return False
if run(flash_command).returncode != 0:
print(f"{bcolors.RED}Zephyr west flash for {test_case} FAILED")
os.chdir(cwd)
return False
os.chdir(cwd)
return True
def test_case_client_exist(self, test_case: str) -> bool:
raise NotImplemented("Zephyr does not support client tests.")
def test_case_server_exist(self, test_case: str) -> bool:
return self.zephyr_erpc_test.joinpath(test_case).exists()
def client(self) -> None:
raise NotImplemented("Zephyr does not support client tests.")
def server(self) -> None:
pass
def generate_shim_code(self):
pass
class JavaTests(AbstractTests):
def __init__(self, maven: str, tcp_port: int, serial_port: str | None = None, **kwargs: dict[str, Any]) -> None:
super().__init__(**kwargs)
self.maven = Path(shutil.which("mvn")) if maven == "mvn" else Path(maven)
self.tcp_port = tcp_port
self.serial_port = serial_port
self.java_test_dir = self.test_dir.joinpath("java_impl_tests/src/test/java/io/github/embeddedrpc/erpc/tests/")
self.java_result_dir = self.test_dir.joinpath("java_impl_tests/target/surefire-reports")
def init_tests(self) -> bool:
return run([self.maven, "generate-test-sources"], cwd=self.test_dir.joinpath("java_impl_tests")).returncode == 0
def test_case_client_exist(self, test_case: str) -> bool:
return self.java_test_dir.joinpath("client").joinpath(f"{to_pascal_case(test_case)}.java").exists()
def test_case_server_exist(self, test_case: str) -> bool:
return self.java_test_dir.joinpath("server").joinpath(f"{to_pascal_case(test_case)}Server.java").exists()
def get_result_file(self) -> Path:
original_file = self.java_result_dir.joinpath(
f"TEST-io.github.embeddedrpc.erpc.tests.client.{to_pascal_case(self.test_case)}.xml")
new_file = self.result_dir.joinpath(f"{self.test_suit_name}_{self.test_case}.xml")
if original_file.exists():
shutil.move(original_file, new_file)
else:
self.create_failed_test_suite_xml(self.test_case, new_file)
return new_file
def client(self) -> None:
mvn_command = [
self.maven, "test",
f"-Dtest={to_pascal_case(self.test_case)}",
"-Dclient",
*self.client_extra_parameters
]
if self.transport == "tcp":
mvn_command.append(f"-Dport={str(self.tcp_port)}")
elif self.transport == "serial" and self.serial_port:
mvn_command.append(f"-Dserial={self.serial_port}")
else:
raise ValueError
self.client_process = Popen(
mvn_command,
cwd=self.test_dir.joinpath('java_impl_tests')
)
def wait_client(self, timeout: int = 5) -> bool:
return super().wait_client(10)
def server(self) -> None:
mvn_command = [
self.maven, "test",
f"-Dtest={to_pascal_case(self.test_case)}Server",
"-Dserver",
*self.server_extra_parameters
]
if self.transport == "tcp":
mvn_command.append(f"-Dport={str(self.tcp_port)}")
elif self.transport == "serial" and self.serial_port:
mvn_command.append(f"-Dserial={self.serial_port}")
else:
raise ValueError
self.server_process = Popen(
mvn_command,
cwd=self.test_dir.joinpath('java_impl_tests'),
stdout=PIPE
)
now = time()
delay = 10
while time() < now + delay:
stdout = self.server_process.stdout.readline()
if stdout.decode().strip() != "":
print(stdout.decode().strip())
if f"Running io.github.embeddedrpc.erpc.tests.server" in stdout.decode():
print("XXX", stdout.decode())
break
sleep(1) # Wait for server to start
def generate_shim_code(self):
for case in self.test_cases:
case_dir = self.test_dir.joinpath(f"java_impl_tests/src/main/java/io/github/embeddedrpc/erpc/tests/{case}/")
case_idl = self.test_dir.joinpath(f"{case}/{case}.erpc")
case_package = f"io.github.embeddedrpc.erpc.tests.{case}.erpc_outputs"
case_cwd = self.test_dir.joinpath(f"java_impl_tests/")
if self.test_case_client_exist(case) or self.test_case_server_exist(case):
if case == "test_struct":
case_idl = self.test_dir.joinpath(f"{case}/test_struct_java.erpc")
if case == "test_arrays":
case_package = f"io.github.embeddedrpc.erpc.tests.{case}"
if case == "test_annotations":
case_package = f"io.github.embeddedrpc.erpc.tests.{case}"
self.erpcgen("java", case_dir, case_idl, case_package, case_cwd)
class bcolors:
# define output text colour class
GREEN = "\033[36m"
BLUE = "\033[38;5;097m"
ORANGE = "\033[38;5;172m"
RED = "\033[31m"
ENDC = "\033[0m"
def run_test(client: AbstractTests, server: AbstractTests, test_suit_name: str, transport: str,
client_extra_parameters: list[str] | None = None,
server_extra_parameters: list[str] | None = None) -> TestResult:
"""
Function to run test combination. Uses implementation of AbstractTests class to start test's client and server
@param client: implementation of AbstractTests
@param server: implementation of AbstractTests
@param test_suit_name: Used to rename result file and test_cases
@param transport: Transport used for testing
@param client_extra_parameters: Extra parameter for executing client tests
@param server_extra_parameters: Extra parameter for executing server tests
@return: TestResult
"""
if not client_extra_parameters:
client_extra_parameters = []
if not server_extra_parameters:
server_extra_parameters = []
client.init_tests()
server.init_tests()
result = TestResult()
# Add extra parameters to client/server
[client.add_client_parameter(x) for x in client_extra_parameters]
[server.add_client_parameter(x) for x in server_extra_parameters]
for test_case in client.test_cases:
# Init each testcase
client.init_test(test_case, test_suit_name, transport)
server.init_test(test_case, test_suit_name, transport)
# Check if test case exist on both sides
if not client.test_case_client_exist(test_case) or not server.test_case_server_exist(test_case):
result.add_skipped(test_case)
continue
# Start client/server
server.server()
client.client()
# Wait for client to finish tests, and server to shut down
client_result = client.wait_client(10)
server_result = server.wait_server(10)
if client_result and server_result:
result.add_passed(test_case)
else:
result.add_failed(test_case)
return result
def main(args: argparse.Namespace) -> int:
"""
Main function that takes parsed arguments and pass them to AbstractTests implementations.
Then call requested combination.
@param args: Parsed CLI arguments
@return: Number of failed tests, 0 if all passed
"""
params = parser.parse_args().__dict__
if type(params["generate_shim"]) is list:
ctest = CTests(**params)
ctest.cmake_generate()
ctest.cmake_build_target("erpcgen")
if "python" in params["generate_shim"] or len(params["generate_shim"]) == 0:
PythonTests(**params).generate_shim_code()
if "java" in params["generate_shim"] or len(params["generate_shim"]) == 0:
JavaTests(**params).generate_shim_code()
tests: list[tuple[str, str, Type[AbstractTests], Type[AbstractTests], str, list[str], list[str]]] = [
("c", "c", CTests, CTests, "tcp", [], []),
("c", "python", CTests, PythonTests, "tcp", ["--gtest_filter=-test_struct.TestSendingByrefMembers"], []),
("c", "java", CTests, JavaTests, "tcp", ["--gtest_filter=-test_struct.TestSendingByrefMembers"], []),
("python", "c", PythonTests, CTests, "tcp", [], []),
("python", "python", PythonTests, PythonTests, "tcp", [], []),
("python", "java", PythonTests, JavaTests, "tcp", [], []),
("java", "c", JavaTests, CTests, "tcp", [], []),
("java", "python", JavaTests, PythonTests, "tcp", [], []),
("java", "java", JavaTests, JavaTests, "tcp", [], []),
("c", "zephyr", CTests, ZephyrTests, "serial", ["--gtest_filter=-test_struct.TestSendingByrefMembers"], []),
("python", "zephyr", PythonTests, ZephyrTests, "serial", [], []),
("java", "zephyr", JavaTests, ZephyrTests, "serial", [], []),
]
results: list[TestResult] = []
return_code = 0
clients = ["c", "python", "java"] if args.client == "all" else [args.client]
servers = ["c", "python", "java"] if args.server == "all" else [args.server]
tests = list(
x[2] for x in # Take given test suit
itertools.product(clients, servers, tests) # From all possible combination
if x[0] == x[2][0] and x[1] == x[2][1] # Filter only mathing
)
for (client, server, client_cls, server_cls, transport, client_args, server_args) in tests:
print(f"{bcolors.RED}RUNNING {transport}_{client}_{server} {bcolors.ENDC}")
result = run_test(
client_cls(**params),
server_cls(**params),
f"{transport}_{client}_{server}",
transport,
client_args,
server_args
)
result.set_test_suit_name(f"{transport}_{client}_{server}")
results.append(result)
return_code += result.return_code
for result in results:
result.print_result()
return return_code
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
prog=Path(__file__).name,
description="""Run eRPC unit tests.
Currently supported options (client - server):
- C - C
- C - Python
- C - Java
- C - Zephyr
- Python - C
- Python - Python
- Python - Java
- Python - Zephyr
- Java - C
- Java - Python
- Java - Java
- Java - Zephyr""")
parser.add_argument("test_cases", nargs="*",
help="List of tests to execute.")
parser.add_argument("-B", "--build-dir",
help="Cmake build dir. If not specified, 'erpc/build' is used.")
parser.add_argument("-b", "--build", action="store_true",
help="Build all before running tests.")
parser.add_argument("-p", "--pristine", action="store_true",
help="Clear the CMake build dir.")
parser.add_argument("--c-compiler",
help="Set CMake default compiler for C (-DCMAKE_C_COMPILER=<compiler>).")
parser.add_argument("--cxx-compiler",
help="Set CMake default compiler for C++ (-DCMAKE_CXX_COMPILER=<compiler>).")
parser.add_argument("-T", "--target", choices=["Debug", "Release"], default="Debug",
help="Set CMake build type (-DCMAKE_BUILD_TYPE=<target>).")
parser.add_argument("--transport", choices=["tcp", "serial"], default="tcp",
help="Set transport layer if there is more options, "
"for example Python-Python only support TCP now.")
parser.add_argument("--conf-file",
help="Set Kconfig config file. Default test/prj.conf")
parser.add_argument("--zephyr-base",
help="Path to Zephyr base directory")
parser.add_argument("-C", "--client", choices=["python", "c", "java", "all"],
help="Set what client should be used. Default C.")
parser.add_argument("-S", "--server", choices=["zephyr", "python", "c", "java", "all"],
help="Set what server should be used. Default C.")
parser.add_argument("--result-dir",
help="Path where xml results from gtest and pytest should be stored. "
"Result files have format '[pytest|gtest]_[test_name]_[transport]_[client]_[server].xml'")
parser.add_argument("--tcp-port", default=12345,
help="TCP port used for pytest. TCP port for C test should be specified in prj.conf")
parser.add_argument("--board", default="mimxrt1060_evkb",
help="Board that would be used for zephyr build.")
parser.add_argument("--serial-port", default="COM4",
help="Serial port that would be used for zephyr build.")
parser.add_argument("--menuconfig", action="store_true",
help="If selected, show menuconfig before building. "
"This allows user to edit tests parameters and choose what to build.")
parser.add_argument("--add-test-prefix", action="store_true",
help="Add prefix to testcase name corresponding to current test setting. "
"TCP Pytohn - C test -> tcp_python_c_test_testName")
parser.add_argument("--maven", default="mvn", help="Path to maven for Java tests. Default 'mvn'.")
parser.add_argument("--erpcgen", help="Path to erpcgen. Default 'erpcgen'.")
parser.add_argument("--generate-shim", nargs="*", help="Generate shim code for Python and Java tests.")
sys.exit(main(parser.parse_args()))