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.

@@ -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
- try:
426
- step_type = step_data["Type"]
427
- screenshot_name = step_data["Screenshot"]
428
- if not screenshot_name:
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
- except Exception as e:
444
- logger.error(f"Error when marking screenshots: {e}")
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
- try:
536
- # Format timestamp for display
537
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
538
-
539
- # Ensure coverage_trend has data
540
- if not data["coverage_trend"]:
541
- logger.warning("No coverage trend data")
542
- # Use the same field names as in coverage.log file
543
- data["coverage_trend"] = [{"stepsCount": 0, "coverage": 0, "testedActivitiesCount": 0}]
544
-
545
- # Convert coverage_trend to JSON string, ensuring all data points are included
546
- coverage_trend_json = json.dumps(data["coverage_trend"])
547
- logger.debug(f"Number of coverage trend data points: {len(data['coverage_trend'])}")
548
-
549
- # Prepare template data
550
- template_data = {
551
- 'timestamp': timestamp,
552
- 'bugs_found': data["bugs_found"],
553
- 'total_testing_time': data["total_testing_time"],
554
- 'executed_events': data["executed_events"],
555
- 'coverage_percent': round(data["coverage"], 2),
556
- 'total_activities_count': data["total_activities_count"],
557
- 'tested_activities_count': data["tested_activities_count"],
558
- 'tested_activities': data["tested_activities"],
559
- 'total_activities': data["total_activities"],
560
- 'all_properties_count': data["all_properties_count"],
561
- 'executed_properties_count': data["executed_properties_count"],
562
- 'items_per_page': 10, # Items to display per page
563
- 'screenshots': self.screenshots,
564
- 'property_violations': data["property_violations"],
565
- 'property_stats': data["property_stats"],
566
- 'property_error_details': data["property_error_details"],
567
- 'coverage_data': coverage_trend_json,
568
- 'take_screenshots': self.take_screenshots, # Pass screenshot setting to template
569
- 'property_execution_trend': data["property_execution_trend"],
570
- 'property_execution_data': json.dumps(data["property_execution_trend"]),
571
- 'activity_count_history': data["activity_count_history"],
572
- 'crash_events': data["crash_events"],
573
- 'anr_events': data["anr_events"]
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
- # Use Jinja2 to render template
582
- template = self.jinja_env.get_template("bug_report_template.html")
583
- html_content = template.render(**template_data)
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
- return html_content
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
- except Exception as e:
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
- "precondition_page": start_step,
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, TypedDict, Union
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
- try:
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
- Merged property execution results
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) -> tuple[List[Dict], List[Dict]]:
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,