Kea2-python 0.3.0__py3-none-any.whl → 0.3.2__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/bug_report_generator.py +66 -75
- kea2/fastbotManager.py +3 -2
- kea2/keaUtils.py +11 -20
- kea2/report_merger.py +29 -16
- kea2/templates/bug_report_template.html +606 -285
- kea2/templates/merged_bug_report_template.html +1057 -355
- kea2/u2Driver.py +128 -5
- kea2/utils.py +50 -9
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/METADATA +71 -8
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/RECORD +14 -14
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/WHEEL +0 -0
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.3.0.dist-info → kea2_python-0.3.2.dist-info}/top_level.txt +0 -0
kea2/bug_report_generator.py
CHANGED
|
@@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
9
9
|
|
|
10
10
|
from PIL import Image, ImageDraw, ImageFont
|
|
11
11
|
from jinja2 import Environment, FileSystemLoader, select_autoescape, PackageLoader
|
|
12
|
-
from kea2.utils import getLogger
|
|
12
|
+
from kea2.utils import getLogger, catchException
|
|
13
13
|
|
|
14
14
|
logger = getLogger(__name__)
|
|
15
15
|
|
|
@@ -421,27 +421,24 @@ class BugReportGenerator:
|
|
|
421
421
|
step_data["Info"] = json.loads(step_data["Info"])
|
|
422
422
|
return step_data
|
|
423
423
|
|
|
424
|
+
@catchException("Error when marking screenshot")
|
|
424
425
|
def _mark_screenshot(self, step_data: StepData):
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
430
|
-
|
|
431
|
-
if step_type == "Monkey":
|
|
432
|
-
act = step_data["Info"].get("act")
|
|
433
|
-
pos = step_data["Info"].get("pos")
|
|
434
|
-
if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
|
|
435
|
-
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
436
|
-
|
|
437
|
-
elif step_type == "Script":
|
|
438
|
-
act = step_data["Info"].get("method")
|
|
439
|
-
pos = step_data["Info"].get("params")
|
|
440
|
-
if act in ["click", "setText", "swipe"]:
|
|
441
|
-
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
426
|
+
step_type = step_data["Type"]
|
|
427
|
+
screenshot_name = step_data["Screenshot"]
|
|
428
|
+
if not screenshot_name:
|
|
429
|
+
return
|
|
442
430
|
|
|
443
|
-
|
|
444
|
-
|
|
431
|
+
if step_type == "Monkey":
|
|
432
|
+
act = step_data["Info"].get("act")
|
|
433
|
+
pos = step_data["Info"].get("pos")
|
|
434
|
+
if act in ["CLICK", "LONG_CLICK"] or act.startswith("SCROLL"):
|
|
435
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
436
|
+
|
|
437
|
+
elif step_type == "Script":
|
|
438
|
+
act = step_data["Info"].get("method")
|
|
439
|
+
pos = step_data["Info"].get("params")
|
|
440
|
+
if act in ["click", "setText", "swipe"]:
|
|
441
|
+
self._mark_screenshot_interaction(step_type, screenshot_name, act, pos)
|
|
445
442
|
|
|
446
443
|
|
|
447
444
|
def _mark_screenshot_interaction(self, step_type: str, screenshot_name: str, action_type: str, position: Union[List, Tuple]) -> bool:
|
|
@@ -528,65 +525,61 @@ class BugReportGenerator:
|
|
|
528
525
|
img.save(screenshot_path)
|
|
529
526
|
return True
|
|
530
527
|
|
|
528
|
+
@catchException("Error rendering template")
|
|
531
529
|
def _generate_html_report(self, data: ReportData):
|
|
532
530
|
"""
|
|
533
531
|
Generate HTML format bug report
|
|
534
532
|
"""
|
|
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
|
-
# Check if template exists, if not create it
|
|
577
|
-
template_path = Path(__file__).parent / "templates" / "bug_report_template.html"
|
|
578
|
-
if not template_path.exists():
|
|
579
|
-
logger.warning("Template file does not exist, creating default template...")
|
|
533
|
+
# Format timestamp for display
|
|
534
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
535
|
+
|
|
536
|
+
# Ensure coverage_trend has data
|
|
537
|
+
if not data["coverage_trend"]:
|
|
538
|
+
logger.warning("No coverage trend data")
|
|
539
|
+
# Use the same field names as in coverage.log file
|
|
540
|
+
data["coverage_trend"] = [{"stepsCount": 0, "coverage": 0, "testedActivitiesCount": 0}]
|
|
541
|
+
|
|
542
|
+
# Convert coverage_trend to JSON string, ensuring all data points are included
|
|
543
|
+
coverage_trend_json = json.dumps(data["coverage_trend"])
|
|
544
|
+
logger.debug(f"Number of coverage trend data points: {len(data['coverage_trend'])}")
|
|
545
|
+
|
|
546
|
+
# Prepare template data
|
|
547
|
+
template_data = {
|
|
548
|
+
'timestamp': timestamp,
|
|
549
|
+
'bugs_found': data["bugs_found"],
|
|
550
|
+
'total_testing_time': data["total_testing_time"],
|
|
551
|
+
'executed_events': data["executed_events"],
|
|
552
|
+
'coverage_percent': round(data["coverage"], 2),
|
|
553
|
+
'total_activities_count': data["total_activities_count"],
|
|
554
|
+
'tested_activities_count': data["tested_activities_count"],
|
|
555
|
+
'tested_activities': data["tested_activities"],
|
|
556
|
+
'total_activities': data["total_activities"],
|
|
557
|
+
'all_properties_count': data["all_properties_count"],
|
|
558
|
+
'executed_properties_count': data["executed_properties_count"],
|
|
559
|
+
'items_per_page': 10, # Items to display per page
|
|
560
|
+
'screenshots': self.screenshots,
|
|
561
|
+
'property_violations': data["property_violations"],
|
|
562
|
+
'property_stats': data["property_stats"],
|
|
563
|
+
'property_error_details': data["property_error_details"],
|
|
564
|
+
'coverage_data': coverage_trend_json,
|
|
565
|
+
'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
|
|
566
|
+
'property_execution_trend': data["property_execution_trend"],
|
|
567
|
+
'property_execution_data': json.dumps(data["property_execution_trend"]),
|
|
568
|
+
'activity_count_history': data["activity_count_history"],
|
|
569
|
+
'crash_events': data["crash_events"],
|
|
570
|
+
'anr_events': data["anr_events"]
|
|
571
|
+
}
|
|
580
572
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
573
|
+
# Check if template exists, if not create it
|
|
574
|
+
template_path = Path(__file__).parent / "templates" / "bug_report_template.html"
|
|
575
|
+
if not template_path.exists():
|
|
576
|
+
logger.warning("Template file does not exist, creating default template...")
|
|
584
577
|
|
|
585
|
-
|
|
578
|
+
# Use Jinja2 to render template
|
|
579
|
+
template = self.jinja_env.get_template("bug_report_template.html")
|
|
580
|
+
html_content = template.render(**template_data)
|
|
586
581
|
|
|
587
|
-
|
|
588
|
-
logger.error(f"Error rendering template: {e}")
|
|
589
|
-
raise
|
|
582
|
+
return html_content
|
|
590
583
|
|
|
591
584
|
def _add_screenshot_info(self, step_data: StepData, step_index: int, data: Dict):
|
|
592
585
|
"""
|
|
@@ -697,9 +690,7 @@ class BugReportGenerator:
|
|
|
697
690
|
data["property_violations"].append({
|
|
698
691
|
"index": index,
|
|
699
692
|
"property_name": property_name,
|
|
700
|
-
"
|
|
701
|
-
"interaction_pages": [start_step, end_step],
|
|
702
|
-
"postcondition_page": end_step
|
|
693
|
+
"interaction_pages": [start_step, end_step]
|
|
703
694
|
})
|
|
704
695
|
index += 1
|
|
705
696
|
|
kea2/fastbotManager.py
CHANGED
|
@@ -3,7 +3,7 @@ from retry.api import retry_call
|
|
|
3
3
|
from dataclasses import asdict
|
|
4
4
|
import requests
|
|
5
5
|
from time import sleep
|
|
6
|
-
|
|
6
|
+
from pkg_resources import parse_version
|
|
7
7
|
|
|
8
8
|
from uiautomator2.core import HTTPResponse, _http_request
|
|
9
9
|
from kea2.adbUtils import ADBDevice, ADBStreamShell_V2
|
|
@@ -28,6 +28,7 @@ class FastbotManager:
|
|
|
28
28
|
self._device_output_dir = None
|
|
29
29
|
ADBDevice.setDevice(options.serial, options.transport_id)
|
|
30
30
|
self.dev = ADBDevice()
|
|
31
|
+
self.android_release = parse_version(self.dev.getprop("ro.build.version.release"))
|
|
31
32
|
|
|
32
33
|
def _activateFastbot(self) -> ADBStreamShell_V2:
|
|
33
34
|
"""
|
|
@@ -219,7 +220,7 @@ class FastbotManager:
|
|
|
219
220
|
if self.thread.is_running():
|
|
220
221
|
logger.info("Waiting for Fastbot to exit.")
|
|
221
222
|
return self.thread.wait()
|
|
222
|
-
return self.thread.poll()
|
|
223
|
+
return self.thread.poll() if self.android_release >= parse_version("7.0") else 0
|
|
223
224
|
|
|
224
225
|
def start(self):
|
|
225
226
|
# kill the fastbot process if runing.
|
kea2/keaUtils.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import traceback
|
|
6
6
|
import time
|
|
7
|
-
from typing import Callable, Any, Deque, Dict, List, Literal, NewType,
|
|
7
|
+
from typing import Callable, Any, Deque, Dict, List, Literal, NewType, Union
|
|
8
8
|
from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult
|
|
9
9
|
import random
|
|
10
10
|
import warnings
|
|
@@ -14,8 +14,8 @@ from functools import wraps
|
|
|
14
14
|
from kea2.bug_report_generator import BugReportGenerator
|
|
15
15
|
from kea2.resultSyncer import ResultSyncer
|
|
16
16
|
from kea2.logWatcher import LogWatcher
|
|
17
|
-
from kea2.utils import TimeStamp, getProjectRoot, getLogger
|
|
18
|
-
from kea2.u2Driver import StaticU2UiObject
|
|
17
|
+
from kea2.utils import TimeStamp, catchException, getProjectRoot, getLogger, timer
|
|
18
|
+
from kea2.u2Driver import StaticU2UiObject, StaticXpathUiObject
|
|
19
19
|
from kea2.fastbotManager import FastbotManager
|
|
20
20
|
from kea2.adbUtils import ADBDevice
|
|
21
21
|
import uiautomator2 as u2
|
|
@@ -640,13 +640,10 @@ class KeaTestRunner(TextTestRunner):
|
|
|
640
640
|
_widgets = func(self.options.Driver.getStaticChecker())
|
|
641
641
|
_widgets = _widgets if isinstance(_widgets, list) else [_widgets]
|
|
642
642
|
for w in _widgets:
|
|
643
|
-
if isinstance(w, StaticU2UiObject):
|
|
643
|
+
if isinstance(w, (StaticU2UiObject, StaticXpathUiObject)):
|
|
644
644
|
xpath = w.selector_to_xpath(w.selector)
|
|
645
645
|
if xpath != '//error':
|
|
646
646
|
blocked_set.add(xpath)
|
|
647
|
-
elif isinstance(w, u2.xpath.XPathSelector):
|
|
648
|
-
xpath = w._parent.xpath
|
|
649
|
-
blocked_set.add(xpath)
|
|
650
647
|
else:
|
|
651
648
|
logger.error(f"block widget defined in {func.__name__} Not supported.")
|
|
652
649
|
except Exception as e:
|
|
@@ -673,6 +670,12 @@ class KeaTestRunner(TextTestRunner):
|
|
|
673
670
|
|
|
674
671
|
return result
|
|
675
672
|
|
|
673
|
+
@timer(r"Generating bug report cost %cost_time seconds.")
|
|
674
|
+
@catchException("Error when generating bug report")
|
|
675
|
+
def _generate_bug_report(self):
|
|
676
|
+
logger.info("Generating bug report")
|
|
677
|
+
report_generator = BugReportGenerator(self.options.output_dir)
|
|
678
|
+
report_generator.generate_report()
|
|
676
679
|
|
|
677
680
|
def __del__(self):
|
|
678
681
|
"""tearDown method. Cleanup the env.
|
|
@@ -680,16 +683,4 @@ class KeaTestRunner(TextTestRunner):
|
|
|
680
683
|
if self.options.Driver:
|
|
681
684
|
self.options.Driver.tearDown()
|
|
682
685
|
|
|
683
|
-
|
|
684
|
-
start_time = time.time()
|
|
685
|
-
logger.info("Generating bug report")
|
|
686
|
-
report_generator = BugReportGenerator(self.options.output_dir)
|
|
687
|
-
report_generator.generate_report()
|
|
688
|
-
|
|
689
|
-
end_time = time.time()
|
|
690
|
-
generation_time = end_time - start_time
|
|
691
|
-
|
|
692
|
-
logger.info(f"Bug report generation completed in {generation_time:.2f} seconds")
|
|
693
|
-
|
|
694
|
-
except Exception as e:
|
|
695
|
-
logger.error(f"Error generating bug report: {e}", flush=True)
|
|
686
|
+
self._generate_bug_report()
|
kea2/report_merger.py
CHANGED
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Optional, Union
|
|
6
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
|
|
9
9
|
from kea2.utils import getLogger
|
|
@@ -49,12 +49,12 @@ class TestReportMerger:
|
|
|
49
49
|
logger.debug(f"Merging {len(self.result_dirs)} test result directories...")
|
|
50
50
|
|
|
51
51
|
# Merge different types of data
|
|
52
|
-
merged_property_stats = self._merge_property_results()
|
|
52
|
+
merged_property_stats, property_source_mapping = self._merge_property_results()
|
|
53
53
|
merged_coverage_data = self._merge_coverage_data()
|
|
54
54
|
merged_crash_anr_data = self._merge_crash_dump_data()
|
|
55
55
|
|
|
56
56
|
# Calculate final statistics
|
|
57
|
-
final_data = self._calculate_final_statistics(merged_property_stats, merged_coverage_data, merged_crash_anr_data)
|
|
57
|
+
final_data = self._calculate_final_statistics(merged_property_stats, merged_coverage_data, merged_crash_anr_data, property_source_mapping)
|
|
58
58
|
|
|
59
59
|
# Add merge information to final data
|
|
60
60
|
final_data['merge_info'] = {
|
|
@@ -86,12 +86,14 @@ class TestReportMerger:
|
|
|
86
86
|
|
|
87
87
|
logger.debug(f"Validated result directory: {result_dir}")
|
|
88
88
|
|
|
89
|
-
def _merge_property_results(self) -> Dict[str, Dict]:
|
|
89
|
+
def _merge_property_results(self) -> Tuple[Dict[str, Dict], Dict[str, List[str]]]:
|
|
90
90
|
"""
|
|
91
91
|
Merge property test results from all directories
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
Returns:
|
|
94
|
-
|
|
94
|
+
Tuple of (merged_property_results, property_source_mapping)
|
|
95
|
+
- merged_property_results: Merged property execution results
|
|
96
|
+
- property_source_mapping: Maps property names to list of source directories with fail/error
|
|
95
97
|
"""
|
|
96
98
|
merged_results = defaultdict(lambda: {
|
|
97
99
|
"precond_satisfied": 0,
|
|
@@ -99,31 +101,40 @@ class TestReportMerger:
|
|
|
99
101
|
"fail": 0,
|
|
100
102
|
"error": 0
|
|
101
103
|
})
|
|
102
|
-
|
|
104
|
+
|
|
105
|
+
# Track which directories have fail/error for each property
|
|
106
|
+
property_source_mapping = defaultdict(list)
|
|
107
|
+
|
|
103
108
|
for result_dir in self.result_dirs:
|
|
104
109
|
result_files = list(result_dir.glob("result_*.json"))
|
|
105
110
|
if not result_files:
|
|
106
111
|
logger.warning(f"No result file found in {result_dir}")
|
|
107
112
|
continue
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
result_file = result_files[0] # Take the first (should be only one)
|
|
110
|
-
|
|
115
|
+
dir_name = result_dir.name # Get the directory name (e.g., res_2025072011_5048015228)
|
|
116
|
+
|
|
111
117
|
try:
|
|
112
118
|
with open(result_file, 'r', encoding='utf-8') as f:
|
|
113
119
|
test_results = json.load(f)
|
|
114
|
-
|
|
120
|
+
|
|
115
121
|
# Merge results for each property
|
|
116
122
|
for prop_name, prop_result in test_results.items():
|
|
117
123
|
for key in ["precond_satisfied", "executed", "fail", "error"]:
|
|
118
124
|
merged_results[prop_name][key] += prop_result.get(key, 0)
|
|
119
|
-
|
|
125
|
+
|
|
126
|
+
# Track source directories for properties with fail/error
|
|
127
|
+
if prop_result.get('fail', 0) > 0 or prop_result.get('error', 0) > 0:
|
|
128
|
+
if dir_name not in property_source_mapping[prop_name]:
|
|
129
|
+
property_source_mapping[prop_name].append(dir_name)
|
|
130
|
+
|
|
120
131
|
logger.debug(f"Merged results from: {result_file}")
|
|
121
|
-
|
|
132
|
+
|
|
122
133
|
except Exception as e:
|
|
123
134
|
logger.error(f"Error reading result file {result_file}: {e}")
|
|
124
135
|
continue
|
|
125
|
-
|
|
126
|
-
return dict(merged_results)
|
|
136
|
+
|
|
137
|
+
return dict(merged_results), dict(property_source_mapping)
|
|
127
138
|
|
|
128
139
|
def _merge_coverage_data(self) -> Dict:
|
|
129
140
|
"""
|
|
@@ -234,7 +245,7 @@ class TestReportMerger:
|
|
|
234
245
|
"total_anr_count": len(unique_anr_events)
|
|
235
246
|
}
|
|
236
247
|
|
|
237
|
-
def _parse_crash_dump_file(self, crash_dump_file: Path) ->
|
|
248
|
+
def _parse_crash_dump_file(self, crash_dump_file: Path) -> Tuple[List[Dict], List[Dict]]:
|
|
238
249
|
"""
|
|
239
250
|
Parse crash and ANR events from crash-dump.log file
|
|
240
251
|
|
|
@@ -529,7 +540,7 @@ class TestReportMerger:
|
|
|
529
540
|
|
|
530
541
|
return unique_anrs
|
|
531
542
|
|
|
532
|
-
def _calculate_final_statistics(self, property_stats: Dict, coverage_data: Dict, crash_anr_data: Dict = None) -> Dict:
|
|
543
|
+
def _calculate_final_statistics(self, property_stats: Dict, coverage_data: Dict, crash_anr_data: Dict = None, property_source_mapping: Dict = None) -> Dict:
|
|
533
544
|
"""
|
|
534
545
|
Calculate final statistics for template rendering
|
|
535
546
|
|
|
@@ -540,6 +551,7 @@ class TestReportMerger:
|
|
|
540
551
|
property_stats: Merged property statistics
|
|
541
552
|
coverage_data: Merged coverage data
|
|
542
553
|
crash_anr_data: Merged crash and ANR data (optional)
|
|
554
|
+
property_source_mapping: Maps property names to source directories with fail/error (optional)
|
|
543
555
|
|
|
544
556
|
Returns:
|
|
545
557
|
Complete data for template rendering
|
|
@@ -576,6 +588,7 @@ class TestReportMerger:
|
|
|
576
588
|
'all_properties_count': all_properties_count,
|
|
577
589
|
'executed_properties_count': executed_properties_count,
|
|
578
590
|
'property_stats': property_stats,
|
|
591
|
+
'property_source_mapping': property_source_mapping or {},
|
|
579
592
|
'crash_events': crash_events,
|
|
580
593
|
'anr_events': anr_events,
|
|
581
594
|
'total_crash_count': total_crash_count,
|