Kea2-python 0.2.4__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Kea2-python might be problematic. Click here for more details.
- kea2/assets/monkeyq.jar +0 -0
- kea2/bug_report_generator.py +267 -7
- kea2/cli.py +71 -2
- kea2/report_merger.py +651 -0
- kea2/templates/bug_report_template.html +1583 -68
- kea2/templates/merged_bug_report_template.html +2547 -0
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/METADATA +10 -3
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/RECORD +12 -10
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/WHEEL +0 -0
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.2.4.dist-info → kea2_python-0.3.0.dist-info}/top_level.txt +0 -0
kea2/assets/monkeyq.jar
CHANGED
|
Binary file
|
kea2/bug_report_generator.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, TypedDict, List, Deque, NewType, Union, Optional
|
|
6
|
+
from typing import Dict, Tuple, TypedDict, List, Deque, NewType, Union, Optional
|
|
6
7
|
from collections import deque
|
|
7
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
9
|
|
|
@@ -56,6 +57,8 @@ class ReportData(TypedDict):
|
|
|
56
57
|
coverage_trend: List
|
|
57
58
|
property_execution_trend: List # Track executed properties count over steps
|
|
58
59
|
activity_count_history: Dict[str, int] # Activity traversal count from final coverage data
|
|
60
|
+
crash_events: List[Dict] # Crash events from crash-dump.log
|
|
61
|
+
anr_events: List[Dict] # ANR events from crash-dump.log
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
class PropertyExecResult(TypedDict):
|
|
@@ -119,6 +122,7 @@ class DataPath:
|
|
|
119
122
|
coverage_log: Path
|
|
120
123
|
screenshots_dir: Path
|
|
121
124
|
property_exec_info: Path
|
|
125
|
+
crash_dump_log: Path
|
|
122
126
|
|
|
123
127
|
|
|
124
128
|
class BugReportGenerator:
|
|
@@ -223,7 +227,8 @@ class BugReportGenerator:
|
|
|
223
227
|
result_json=self.result_dir / f"result_{self.log_timestamp}.json",
|
|
224
228
|
coverage_log=self.result_dir / f"output_{self.log_timestamp}" / "coverage.log",
|
|
225
229
|
screenshots_dir=self.result_dir / f"output_{self.log_timestamp}" / "screenshots",
|
|
226
|
-
property_exec_info=self.result_dir / f"property_exec_info_{self.log_timestamp}.json"
|
|
230
|
+
property_exec_info=self.result_dir / f"property_exec_info_{self.log_timestamp}.json",
|
|
231
|
+
crash_dump_log=self.result_dir / f"output_{self.log_timestamp}" / "crash-dump.log"
|
|
227
232
|
)
|
|
228
233
|
|
|
229
234
|
self.screenshots = deque()
|
|
@@ -287,7 +292,9 @@ class BugReportGenerator:
|
|
|
287
292
|
"screenshot_info": {},
|
|
288
293
|
"coverage_trend": [],
|
|
289
294
|
"property_execution_trend": [],
|
|
290
|
-
"activity_count_history": {}
|
|
295
|
+
"activity_count_history": {},
|
|
296
|
+
"crash_events": [],
|
|
297
|
+
"anr_events": []
|
|
291
298
|
}
|
|
292
299
|
|
|
293
300
|
# Parse steps.log file to get test step numbers and screenshot mappings
|
|
@@ -402,6 +409,11 @@ class BugReportGenerator:
|
|
|
402
409
|
# Load error details for properties with fail/error state
|
|
403
410
|
data["property_error_details"] = self._load_property_error_details()
|
|
404
411
|
|
|
412
|
+
# Load crash and ANR events from crash-dump.log
|
|
413
|
+
crash_events, anr_events = self._load_crash_dump_data()
|
|
414
|
+
data["crash_events"] = crash_events
|
|
415
|
+
data["anr_events"] = anr_events
|
|
416
|
+
|
|
405
417
|
return data
|
|
406
418
|
|
|
407
419
|
def _parse_step_data(self, raw_step_info: str) -> StepData:
|
|
@@ -432,7 +444,7 @@ class BugReportGenerator:
|
|
|
432
444
|
logger.error(f"Error when marking screenshots: {e}")
|
|
433
445
|
|
|
434
446
|
|
|
435
|
-
def _mark_screenshot_interaction(self, step_type: str, screenshot_name: str, action_type: str, position: Union[List,
|
|
447
|
+
def _mark_screenshot_interaction(self, step_type: str, screenshot_name: str, action_type: str, position: Union[List, Tuple]) -> bool:
|
|
436
448
|
"""
|
|
437
449
|
Mark interaction on screenshot with colored rectangle
|
|
438
450
|
|
|
@@ -556,7 +568,9 @@ class BugReportGenerator:
|
|
|
556
568
|
'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
|
|
557
569
|
'property_execution_trend': data["property_execution_trend"],
|
|
558
570
|
'property_execution_data': json.dumps(data["property_execution_trend"]),
|
|
559
|
-
'activity_count_history': data["activity_count_history"]
|
|
571
|
+
'activity_count_history': data["activity_count_history"],
|
|
572
|
+
'crash_events': data["crash_events"],
|
|
573
|
+
'anr_events': data["anr_events"]
|
|
560
574
|
}
|
|
561
575
|
|
|
562
576
|
# Check if template exists, if not create it
|
|
@@ -616,7 +630,7 @@ class BugReportGenerator:
|
|
|
616
630
|
})
|
|
617
631
|
|
|
618
632
|
def _process_script_info(self, property_name: str, state: str, step_index: int, screenshot: str,
|
|
619
|
-
current_property: str, current_test: Dict, property_violations: Dict) ->
|
|
633
|
+
current_property: str, current_test: Dict, property_violations: Dict) -> Tuple:
|
|
620
634
|
"""
|
|
621
635
|
Process ScriptInfo step for property violations tracking
|
|
622
636
|
|
|
@@ -817,11 +831,257 @@ class BugReportGenerator:
|
|
|
817
831
|
|
|
818
832
|
return property_execution_trend
|
|
819
833
|
|
|
834
|
+
def _load_crash_dump_data(self) -> Tuple[List[Dict], List[Dict]]:
|
|
835
|
+
"""
|
|
836
|
+
Load crash and ANR events from crash-dump.log file
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
tuple: (crash_events, anr_events) - Lists of crash and ANR event dictionaries
|
|
840
|
+
"""
|
|
841
|
+
crash_events = []
|
|
842
|
+
anr_events = []
|
|
843
|
+
|
|
844
|
+
if not self.data_path.crash_dump_log.exists():
|
|
845
|
+
logger.info(f"No crash was found in this run.")
|
|
846
|
+
return crash_events, anr_events
|
|
847
|
+
|
|
848
|
+
try:
|
|
849
|
+
with open(self.data_path.crash_dump_log, "r", encoding="utf-8") as f:
|
|
850
|
+
content = f.read()
|
|
851
|
+
|
|
852
|
+
# Parse crash events
|
|
853
|
+
crash_events = self._parse_crash_events(content)
|
|
854
|
+
|
|
855
|
+
# Parse ANR events
|
|
856
|
+
anr_events = self._parse_anr_events(content)
|
|
857
|
+
|
|
858
|
+
logger.debug(f"Found {len(crash_events)} crash events and {len(anr_events)} ANR events")
|
|
859
|
+
|
|
860
|
+
return crash_events, anr_events
|
|
861
|
+
|
|
862
|
+
except Exception as e:
|
|
863
|
+
logger.error(f"Error reading crash dump file: {e}")
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _parse_crash_events(self, content: str) -> List[Dict]:
|
|
867
|
+
"""
|
|
868
|
+
Parse crash events from crash-dump.log content
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
content: Content of crash-dump.log file
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
List[Dict]: List of crash event dictionaries
|
|
875
|
+
"""
|
|
876
|
+
crash_events = []
|
|
877
|
+
|
|
878
|
+
# Pattern to match crash blocks
|
|
879
|
+
crash_pattern = r'(\d{14})\ncrash:\n(.*?)\n// crash end'
|
|
880
|
+
|
|
881
|
+
for match in re.finditer(crash_pattern, content, re.DOTALL):
|
|
882
|
+
timestamp_str = match.group(1)
|
|
883
|
+
crash_content = match.group(2)
|
|
884
|
+
|
|
885
|
+
# Parse timestamp (format: YYYYMMDDHHMMSS)
|
|
886
|
+
try:
|
|
887
|
+
timestamp = datetime.strptime(timestamp_str, "%Y%m%d%H%M%S")
|
|
888
|
+
formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
889
|
+
except ValueError:
|
|
890
|
+
formatted_time = timestamp_str
|
|
891
|
+
|
|
892
|
+
# Extract crash information
|
|
893
|
+
crash_info = self._extract_crash_info(crash_content)
|
|
894
|
+
|
|
895
|
+
crash_event = {
|
|
896
|
+
"time": formatted_time,
|
|
897
|
+
"exception_type": crash_info.get("exception_type", "Unknown"),
|
|
898
|
+
"process": crash_info.get("process", "Unknown"),
|
|
899
|
+
"stack_trace": crash_info.get("stack_trace", "")
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
crash_events.append(crash_event)
|
|
903
|
+
|
|
904
|
+
return crash_events
|
|
905
|
+
|
|
906
|
+
def _parse_anr_events(self, content: str) -> List[Dict]:
|
|
907
|
+
"""
|
|
908
|
+
Parse ANR events from crash-dump.log content
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
content: Content of crash-dump.log file
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
List[Dict]: List of ANR event dictionaries
|
|
915
|
+
"""
|
|
916
|
+
anr_events = []
|
|
917
|
+
|
|
918
|
+
# Pattern to match ANR blocks
|
|
919
|
+
anr_pattern = r'(\d{14})\nanr:\n(.*?)\nanr end'
|
|
920
|
+
|
|
921
|
+
for match in re.finditer(anr_pattern, content, re.DOTALL):
|
|
922
|
+
timestamp_str = match.group(1)
|
|
923
|
+
anr_content = match.group(2)
|
|
924
|
+
|
|
925
|
+
# Parse timestamp (format: YYYYMMDDHHMMSS)
|
|
926
|
+
try:
|
|
927
|
+
timestamp = datetime.strptime(timestamp_str, "%Y%m%d%H%M%S")
|
|
928
|
+
formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
929
|
+
except ValueError:
|
|
930
|
+
formatted_time = timestamp_str
|
|
931
|
+
|
|
932
|
+
# Extract ANR information
|
|
933
|
+
anr_info = self._extract_anr_info(anr_content)
|
|
934
|
+
|
|
935
|
+
anr_event = {
|
|
936
|
+
"time": formatted_time,
|
|
937
|
+
"reason": anr_info.get("reason", "Unknown"),
|
|
938
|
+
"process": anr_info.get("process", "Unknown"),
|
|
939
|
+
"trace": anr_info.get("trace", "")
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
anr_events.append(anr_event)
|
|
943
|
+
|
|
944
|
+
return anr_events
|
|
945
|
+
|
|
946
|
+
def _extract_crash_info(self, crash_content: str) -> Dict:
|
|
947
|
+
"""
|
|
948
|
+
Extract crash information from crash content
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
crash_content: Content of a single crash block
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
Dict: Extracted crash information
|
|
955
|
+
"""
|
|
956
|
+
crash_info = {
|
|
957
|
+
"exception_type": "Unknown",
|
|
958
|
+
"process": "Unknown",
|
|
959
|
+
"stack_trace": ""
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
lines = crash_content.strip().split('\n')
|
|
963
|
+
|
|
964
|
+
for line in lines:
|
|
965
|
+
line = line.strip()
|
|
966
|
+
|
|
967
|
+
# Extract PID from CRASH line
|
|
968
|
+
if line.startswith("// CRASH:"):
|
|
969
|
+
# Pattern: // CRASH: process_name (pid xxxx) (dump time: ...)
|
|
970
|
+
pid_match = re.search(r'\(pid\s+(\d+)\)', line)
|
|
971
|
+
if pid_match:
|
|
972
|
+
crash_info["process"] = pid_match.group(1)
|
|
973
|
+
|
|
974
|
+
# Extract exception type from Long Msg line
|
|
975
|
+
elif line.startswith("// Long Msg:"):
|
|
976
|
+
# Pattern: // Long Msg: ExceptionType: message
|
|
977
|
+
exception_match = re.search(r'// Long Msg:\s+([^:]+)', line)
|
|
978
|
+
if exception_match:
|
|
979
|
+
crash_info["exception_type"] = exception_match.group(1).strip()
|
|
980
|
+
|
|
981
|
+
# Extract full stack trace (all lines starting with //)
|
|
982
|
+
stack_lines = []
|
|
983
|
+
for line in lines:
|
|
984
|
+
if line.startswith("//"):
|
|
985
|
+
# Remove the "// " prefix for cleaner display
|
|
986
|
+
clean_line = line[3:] if line.startswith("// ") else line[2:]
|
|
987
|
+
stack_lines.append(clean_line)
|
|
988
|
+
|
|
989
|
+
crash_info["stack_trace"] = '\n'.join(stack_lines)
|
|
990
|
+
|
|
991
|
+
return crash_info
|
|
992
|
+
|
|
993
|
+
def _extract_anr_info(self, anr_content: str) -> Dict:
|
|
994
|
+
"""
|
|
995
|
+
Extract ANR information from ANR content
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
anr_content: Content of a single ANR block
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
Dict: Extracted ANR information
|
|
1002
|
+
"""
|
|
1003
|
+
anr_info = {
|
|
1004
|
+
"reason": "Unknown",
|
|
1005
|
+
"process": "Unknown",
|
|
1006
|
+
"trace": ""
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
lines = anr_content.strip().split('\n')
|
|
1010
|
+
|
|
1011
|
+
for line in lines:
|
|
1012
|
+
line = line.strip()
|
|
1013
|
+
|
|
1014
|
+
# Extract PID from ANR line
|
|
1015
|
+
if line.startswith("// ANR:"):
|
|
1016
|
+
# Pattern: // ANR: process_name (pid xxxx) (dump time: ...)
|
|
1017
|
+
pid_match = re.search(r'\(pid\s+(\d+)\)', line)
|
|
1018
|
+
if pid_match:
|
|
1019
|
+
anr_info["process"] = pid_match.group(1)
|
|
1020
|
+
|
|
1021
|
+
# Extract reason from Reason line
|
|
1022
|
+
elif line.startswith("Reason:"):
|
|
1023
|
+
# Pattern: Reason: Input dispatching timed out (...)
|
|
1024
|
+
reason_match = re.search(r'Reason:\s+(.+)', line)
|
|
1025
|
+
if reason_match:
|
|
1026
|
+
full_reason = reason_match.group(1).strip()
|
|
1027
|
+
# Simplify the reason by extracting the main part before parentheses
|
|
1028
|
+
simplified_reason = self._simplify_anr_reason(full_reason)
|
|
1029
|
+
anr_info["reason"] = simplified_reason
|
|
1030
|
+
|
|
1031
|
+
# Store the full ANR trace content
|
|
1032
|
+
anr_info["trace"] = anr_content
|
|
1033
|
+
|
|
1034
|
+
return anr_info
|
|
1035
|
+
|
|
1036
|
+
def _simplify_anr_reason(self, full_reason: str) -> str:
|
|
1037
|
+
"""
|
|
1038
|
+
Simplify ANR reason by extracting the main part
|
|
1039
|
+
|
|
1040
|
+
Args:
|
|
1041
|
+
full_reason: Full ANR reason string
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
str: Simplified ANR reason
|
|
1045
|
+
"""
|
|
1046
|
+
# Common ANR reason patterns to simplify
|
|
1047
|
+
simplification_patterns = [
|
|
1048
|
+
# Input dispatching timed out (details...) -> Input dispatching timed out
|
|
1049
|
+
(r'^(Input dispatching timed out)\s*\(.*\).*$', r'\1'),
|
|
1050
|
+
# Broadcast of Intent (details...) -> Broadcast timeout
|
|
1051
|
+
(r'^Broadcast of Intent.*$', 'Broadcast timeout'),
|
|
1052
|
+
# Service timeout -> Service timeout
|
|
1053
|
+
(r'^Service.*timeout.*$', 'Service timeout'),
|
|
1054
|
+
# ContentProvider timeout -> ContentProvider timeout
|
|
1055
|
+
(r'^ContentProvider.*timeout.*$', 'ContentProvider timeout'),
|
|
1056
|
+
]
|
|
1057
|
+
|
|
1058
|
+
# Apply simplification patterns
|
|
1059
|
+
for pattern, replacement in simplification_patterns:
|
|
1060
|
+
match = re.match(pattern, full_reason, re.IGNORECASE)
|
|
1061
|
+
if match:
|
|
1062
|
+
if callable(replacement):
|
|
1063
|
+
return replacement(match)
|
|
1064
|
+
elif '\\1' in replacement:
|
|
1065
|
+
return re.sub(pattern, replacement, full_reason, flags=re.IGNORECASE)
|
|
1066
|
+
else:
|
|
1067
|
+
return replacement
|
|
1068
|
+
|
|
1069
|
+
# If no pattern matches, try to extract the part before the first parenthesis
|
|
1070
|
+
paren_match = re.match(r'^([^(]+)', full_reason)
|
|
1071
|
+
if paren_match:
|
|
1072
|
+
simplified = paren_match.group(1).strip()
|
|
1073
|
+
# Remove trailing punctuation
|
|
1074
|
+
simplified = re.sub(r'[.,;:]+$', '', simplified)
|
|
1075
|
+
return simplified
|
|
1076
|
+
|
|
1077
|
+
# If all else fails, return the original but truncated
|
|
1078
|
+
return full_reason[:50] + "..." if len(full_reason) > 50 else full_reason
|
|
1079
|
+
|
|
820
1080
|
|
|
821
1081
|
if __name__ == "__main__":
|
|
822
1082
|
print("Generating bug report")
|
|
823
1083
|
# OUTPUT_PATH = "<Your output path>"
|
|
824
|
-
OUTPUT_PATH = "P:/Python/Kea2/output/
|
|
1084
|
+
OUTPUT_PATH = "P:/Python/Kea2/output/res_2025072011_5048015228"
|
|
825
1085
|
|
|
826
1086
|
report_generator = BugReportGenerator()
|
|
827
1087
|
report_path = report_generator.generate_report(OUTPUT_PATH)
|
kea2/cli.py
CHANGED
|
@@ -54,9 +54,15 @@ def cmd_report(args):
|
|
|
54
54
|
logger.error("Report directory path is required. Use -p to specify the path.")
|
|
55
55
|
return
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
if Path(report_dir).is_absolute():
|
|
58
|
+
report_path = Path(report_dir)
|
|
59
|
+
else:
|
|
60
|
+
report_path = Path.cwd() / report_dir
|
|
61
|
+
|
|
62
|
+
report_path = report_path.resolve()
|
|
63
|
+
|
|
58
64
|
if not report_path.exists():
|
|
59
|
-
logger.error(f"Report directory does not exist: {
|
|
65
|
+
logger.error(f"Report directory does not exist: {report_path}")
|
|
60
66
|
return
|
|
61
67
|
|
|
62
68
|
logger.debug(f"Generating test report from directory: {report_dir}")
|
|
@@ -74,6 +80,47 @@ def cmd_report(args):
|
|
|
74
80
|
logger.error(f"Error generating test report: {e}")
|
|
75
81
|
|
|
76
82
|
|
|
83
|
+
def cmd_merge(args):
|
|
84
|
+
"""Merge multiple test report directories and generate a combined report"""
|
|
85
|
+
from .report_merger import TestReportMerger
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Validate input paths
|
|
89
|
+
if not args.paths or len(args.paths) < 2:
|
|
90
|
+
logger.error("At least 2 test report paths are required for merging. Use -p to specify paths.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Validate that all paths exist
|
|
94
|
+
for path in args.paths:
|
|
95
|
+
path_obj = Path(path)
|
|
96
|
+
if not path_obj.exists():
|
|
97
|
+
logger.error(f"Test report path does not exist: {path}")
|
|
98
|
+
return
|
|
99
|
+
if not path_obj.is_dir():
|
|
100
|
+
logger.error(f"Path is not a directory: {path}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
logger.debug(f"Merging {len(args.paths)} test report directories...")
|
|
104
|
+
|
|
105
|
+
# Initialize merger
|
|
106
|
+
merger = TestReportMerger()
|
|
107
|
+
|
|
108
|
+
# Merge test reports
|
|
109
|
+
merged_dir = merger.merge_reports(args.paths, args.output)
|
|
110
|
+
|
|
111
|
+
# Print results
|
|
112
|
+
print(f"✅ Test reports merged successfully!", flush=True)
|
|
113
|
+
print(f"📁 Merged report directory: {merged_dir}", flush=True)
|
|
114
|
+
print(f"📊 Merged report: {merged_dir}/merged_report.html", flush=True)
|
|
115
|
+
|
|
116
|
+
# Get merge summary
|
|
117
|
+
merge_summary = merger.get_merge_summary()
|
|
118
|
+
print(f"📈 Merged {merge_summary.get('merged_directories', 0)} directories", flush=True)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error during merge operation: {e}")
|
|
122
|
+
|
|
123
|
+
|
|
77
124
|
def cmd_run(args):
|
|
78
125
|
base_dir = getProjectRoot()
|
|
79
126
|
if base_dir is None:
|
|
@@ -102,6 +149,28 @@ _commands = [
|
|
|
102
149
|
help="Path to the directory containing test results"
|
|
103
150
|
)
|
|
104
151
|
]
|
|
152
|
+
),
|
|
153
|
+
dict(
|
|
154
|
+
action=cmd_merge,
|
|
155
|
+
command="merge",
|
|
156
|
+
help="merge multiple test report directories and generate a combined report",
|
|
157
|
+
flags=[
|
|
158
|
+
dict(
|
|
159
|
+
name=["paths"],
|
|
160
|
+
args=["-p", "--paths"],
|
|
161
|
+
type=str,
|
|
162
|
+
nargs='+',
|
|
163
|
+
required=True,
|
|
164
|
+
help="Paths to test report directories (res_* directories) to merge"
|
|
165
|
+
),
|
|
166
|
+
dict(
|
|
167
|
+
name=["output"],
|
|
168
|
+
args=["-o", "--output"],
|
|
169
|
+
type=str,
|
|
170
|
+
required=False,
|
|
171
|
+
help="Output directory for merged report (optional)"
|
|
172
|
+
)
|
|
173
|
+
]
|
|
105
174
|
)
|
|
106
175
|
]
|
|
107
176
|
|