Kea2-python 0.1.3__py3-none-any.whl → 0.2.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/fastbotManager.py CHANGED
@@ -1,13 +1,17 @@
1
- from dataclasses import asdict
2
1
  import subprocess
3
2
  import threading
3
+ from dataclasses import asdict
4
4
  import requests
5
5
  from time import sleep
6
- from .adbUtils import push_file
6
+
7
+
8
+ from uiautomator2.core import HTTPResponse, _http_request
9
+ from kea2.adbUtils import ADBDevice, ADBStreamShell_V2
7
10
  from pathlib import Path
8
- from .utils import getLogger
11
+ from kea2.utils import getLogger
12
+
9
13
 
10
- from typing import IO, TYPE_CHECKING
14
+ from typing import IO, TYPE_CHECKING, Dict
11
15
  if TYPE_CHECKING:
12
16
  from .keaUtils import Options
13
17
 
@@ -21,56 +25,49 @@ class FastbotManager:
21
25
  self.log_file: str = log_file
22
26
  self.port = None
23
27
  self.thread = None
28
+ self._device_output_dir = None
29
+ ADBDevice.setDevice(options.serial, options.transport_id)
30
+ self.dev = ADBDevice()
24
31
 
25
-
26
- def _activateFastbot(self) -> threading.Thread:
32
+ def _activateFastbot(self) -> ADBStreamShell_V2:
27
33
  """
28
34
  activate fastbot.
29
35
  :params: options: the running setting for fastbot
30
36
  :params: port: the listening port for script driver
31
37
  :return: the fastbot daemon thread
32
38
  """
33
- options = self.options
34
39
  cur_dir = Path(__file__).parent
35
- push_file(
40
+ self.dev.sync.push(
36
41
  Path.joinpath(cur_dir, "assets/monkeyq.jar"),
37
- "/sdcard/monkeyq.jar",
38
- device=options.serial
42
+ "/sdcard/monkeyq.jar"
39
43
  )
40
- push_file(
44
+ self.dev.sync.push(
41
45
  Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
42
46
  "/sdcard/fastbot-thirdpart.jar",
43
- device=options.serial,
44
47
  )
45
- push_file(
48
+ self.dev.sync.push(
46
49
  Path.joinpath(cur_dir, "assets/kea2-thirdpart.jar"),
47
50
  "/sdcard/kea2-thirdpart.jar",
48
- device=options.serial,
49
51
  )
50
- push_file(
51
- Path.joinpath(cur_dir, "assets/framework.jar"),
52
+ self.dev.sync.push(
53
+ Path.joinpath(cur_dir, "assets/framework.jar"),
52
54
  "/sdcard/framework.jar",
53
- device=options.serial
54
55
  )
55
- push_file(
56
- Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a"),
57
- "/data/local/tmp",
58
- device=options.serial
56
+ self.dev.sync.push(
57
+ Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a/libfastbot_native.so"),
58
+ "/data/local/tmp/arm64-v8a/libfastbot_native.so",
59
59
  )
60
- push_file(
61
- Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a"),
62
- "/data/local/tmp",
63
- device=options.serial
60
+ self.dev.sync.push(
61
+ Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a/libfastbot_native.so"),
62
+ "/data/local/tmp/armeabi-v7a/libfastbot_native.so",
64
63
  )
65
- push_file(
66
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86"),
67
- "/data/local/tmp",
68
- device=options.serial
64
+ self.dev.sync.push(
65
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86/libfastbot_native.so"),
66
+ "/data/local/tmp/x86/libfastbot_native.so",
69
67
  )
70
- push_file(
71
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64"),
72
- "/data/local/tmp",
73
- device=options.serial
68
+ self.dev.sync.push(
69
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64/libfastbot_native.so"),
70
+ "/data/local/tmp/x86_64/libfastbot_native.so",
74
71
  )
75
72
 
76
73
  t = self._startFastbotService()
@@ -79,29 +76,88 @@ class FastbotManager:
79
76
  return t
80
77
 
81
78
 
82
- def check_alive(self, port):
79
+ def check_alive(self):
83
80
  """
84
81
  check if the script driver and proxy server are alive.
85
82
  """
83
+
86
84
  for _ in range(10):
87
85
  sleep(2)
88
86
  try:
89
- requests.get(f"http://localhost:{port}/ping")
87
+ _http_request(
88
+ dev=self.dev,
89
+ device_port=8090,
90
+ method="GET",
91
+ path="/ping",
92
+ )
90
93
  return
91
94
  except requests.ConnectionError:
92
95
  logger.info("waiting for connection.")
93
96
  pass
94
97
  raise RuntimeError("Failed to connect fastbot")
95
98
 
99
+ def request(self, method: str, path: str, data: Dict=None, timeout: int=10) -> HTTPResponse:
100
+ return _http_request(self.dev, 8090, method, path, data, timeout)
101
+
102
+ def init(self, options: "Options", stamp):
103
+ post_data = {
104
+ "takeScreenshots": options.take_screenshots,
105
+ "Stamp": stamp,
106
+ "deviceOutputRoot": options.device_output_root,
107
+ }
108
+ r = _http_request(
109
+ self.dev,
110
+ device_port=8090,
111
+ method="POST",
112
+ path="/init",
113
+ data=post_data
114
+ )
115
+ print(f"[INFO] Init fastbot: {post_data}", flush=True)
116
+ import re
117
+ self._device_output_dir = re.match(r"outputDir:(.+)", r.text).group(1)
118
+ print(f"[INFO] Fastbot initiated. outputDir: {r.text}", flush=True)
119
+
120
+ def stepMonkey(self, monkeyStepInfo):
121
+ r = self.request(
122
+ method="POST",
123
+ path="/stepMonkey",
124
+ data=monkeyStepInfo
125
+ )
126
+ return r.json()["result"]
127
+
128
+ def stopMonkey(self):
129
+ """
130
+ send a stop monkey request to the server.
131
+ """
132
+ r = self.request(
133
+ method="GET",
134
+ path="/stopMonkey",
135
+ )
136
+
137
+ print(f"[Server INFO] {r.text}", flush=True)
138
+
139
+ def logScript(self, execution_info: Dict):
140
+ r = self.request(
141
+ method="POST",
142
+ path="/logScript",
143
+ data=execution_info
144
+ )
145
+ res = r.text
146
+ if res != "OK":
147
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
96
148
 
97
- def _startFastbotService(self) -> threading.Thread:
149
+ @property
150
+ def device_output_dir(self):
151
+ return self._device_output_dir
152
+
153
+ def _startFastbotService(self) -> ADBStreamShell_V2:
98
154
  shell_command = [
99
155
  "CLASSPATH="
100
156
  "/sdcard/monkeyq.jar:"
101
157
  "/sdcard/framework.jar:"
102
158
  "/sdcard/fastbot-thirdpart.jar:"
103
159
  "/sdcard/kea2-thirdpart.jar",
104
-
160
+
105
161
  "exec", "app_process",
106
162
  "/system/bin", "com.android.commands.monkey.Monkey",
107
163
  "-p", *self.options.packageNames,
@@ -119,37 +175,39 @@ class FastbotManager:
119
175
 
120
176
  full_cmd = ["adb"] + (["-s", self.options.serial] if self.options.serial else []) + ["shell"] + shell_command
121
177
 
178
+
122
179
  outfile = open(self.log_file, "w", encoding="utf-8", buffering=1)
123
180
 
124
181
  logger.info("Options info: {}".format(asdict(self.options)))
125
182
  logger.info("Launching fastbot with shell command:\n{}".format(" ".join(full_cmd)))
126
183
  logger.info("Fastbot log will be saved to {}".format(outfile.name))
127
184
 
185
+ # stream = self.dev.shell(shell_command, encoding="utf-8", stream=True, timeout=float("inf"))
128
186
  # process handler
129
- proc = subprocess.Popen(full_cmd, stdout=outfile, stderr=outfile)
130
- t = threading.Thread(target=self.close_on_exit, args=(proc, outfile), daemon=True)
131
- t.start()
132
-
187
+ t = self.dev.stream_shell(shell_command, stdout=outfile, stderr=outfile)
188
+ # proc = subprocess.Popen(full_cmd, stdout=outfile, stderr=outfile)
189
+ # t = threading.Thread(target=self.close_on_exit, args=(proc, outfile), daemon=True)
190
+ # t.start()
133
191
  return t
134
192
 
135
- def close_on_exit(self, proc: subprocess.Popen, f: IO):
193
+ def close_on_exit(self, proc: ADBStreamShell_V2, f: IO):
136
194
  self.return_code = proc.wait()
137
195
  f.close()
138
196
  if self.return_code != 0:
139
197
  raise RuntimeError(f"Fastbot Error: Terminated with [code {self.return_code}] See {self.log_file} for details.")
140
198
 
141
199
  def get_return_code(self):
142
- if self.thread:
200
+ if self.thread.is_running():
143
201
  logger.info("Waiting for Fastbot to exit.")
144
- self.thread.join()
145
- return self.return_code
202
+ return self.thread.wait()
203
+ return self.thread.poll()
146
204
 
147
205
  def start(self):
148
206
  self.thread = self._activateFastbot()
149
207
 
150
208
  def join(self):
151
- if self.thread:
152
- self.thread.join()
209
+ self.thread.join()
210
+
153
211
 
154
212
 
155
213
 
kea2/keaUtils.py CHANGED
@@ -2,6 +2,7 @@ import json
2
2
  import os
3
3
  from pathlib import Path
4
4
  import traceback
5
+ import time
5
6
  from typing import Callable, Any, Dict, List, Literal, NewType, TypedDict, Union
6
7
  from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult
7
8
  import random
@@ -110,6 +111,8 @@ class Options:
110
111
  packageNames: List[str]
111
112
  # target device
112
113
  serial: str = None
114
+ # target device with transport_id
115
+ transport_id: str = None
113
116
  # test agent. "native" for stage 1 and "u2" for stage 1~3
114
117
  agent: Literal["u2", "native"] = "u2"
115
118
  # max step in exploration (availble in stage 2~3)
@@ -139,8 +142,13 @@ class Options:
139
142
  def __post_init__(self):
140
143
  import logging
141
144
  logging.basicConfig(level=logging.DEBUG if self.debug else logging.INFO)
142
- if self.serial and self.Driver:
143
- self.Driver.setDeviceSerial(self.serial)
145
+ if self.Driver:
146
+ target_device = dict()
147
+ if self.serial:
148
+ target_device["serial"] = self.serial
149
+ if self.transport_id:
150
+ target_device["transport_id"] = self.transport_id
151
+ self.Driver.setDevice(target_device)
144
152
  global LOGFILE, RESFILE, STAMP
145
153
  if self.log_stamp:
146
154
  illegal_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\n', '\r', '\t', '\0']
@@ -313,10 +321,11 @@ class KeaTestRunner(TextTestRunner):
313
321
  result.flushResult(outfile=RESFILE)
314
322
  # setUp for the u2 driver
315
323
  self.scriptDriver = self.options.Driver.getScriptDriver()
316
- fb.check_alive(port=self.scriptDriver.lport)
317
- self._init()
324
+ fb.check_alive()
325
+
326
+ fb.init(options=self.options, stamp=STAMP)
318
327
 
319
- resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
328
+ resultSyncer = ResultSyncer(fb.device_output_dir, self.options)
320
329
  resultSyncer.run()
321
330
 
322
331
  end_by_remote = False
@@ -331,7 +340,7 @@ class KeaTestRunner(TextTestRunner):
331
340
  )
332
341
 
333
342
  try:
334
- xml_raw = self.stepMonkey()
343
+ xml_raw = fb.stepMonkey(self._monkeyStepInfo)
335
344
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
336
345
  except requests.ConnectionError:
337
346
  logger.info("Connection refused by remote.")
@@ -369,18 +378,18 @@ class KeaTestRunner(TextTestRunner):
369
378
  print("execute property %s." % execPropName, flush=True)
370
379
 
371
380
  result.addExcuted(test)
372
- self._logScript(result.lastExecutedInfo)
381
+ fb.logScript(result.lastExecutedInfo)
373
382
  try:
374
383
  test(result)
375
384
  finally:
376
385
  result.printErrors()
377
386
 
378
387
  result.updateExectedInfo()
379
- self._logScript(result.lastExecutedInfo)
388
+ fb.logScript(result.lastExecutedInfo)
380
389
  result.flushResult(outfile=RESFILE)
381
390
 
382
391
  if not end_by_remote:
383
- self.stopMonkey()
392
+ fb.stopMonkey()
384
393
  result.flushResult(outfile=RESFILE)
385
394
  resultSyncer.close()
386
395
 
@@ -424,40 +433,46 @@ class KeaTestRunner(TextTestRunner):
424
433
  self.stream.flush()
425
434
  return result
426
435
 
427
- def stepMonkey(self) -> str:
428
- """
429
- send a step monkey request to the server and get the xml string.
430
- """
436
+ # def stepMonkey(self) -> str:
437
+ # """
438
+ # send a step monkey request to the server and get the xml string.
439
+ # """
440
+ # block_dict = self._getBlockedWidgets()
441
+ # block_widgets: List[str] = block_dict['widgets']
442
+ # block_trees: List[str] = block_dict['trees']
443
+ # URL = f"http://localhost:{self.scriptDriver.lport}/stepMonkey"
444
+ # logger.debug(f"Sending request: {URL}")
445
+ # logger.debug(f"Blocking widgets: {block_widgets}")
446
+ # logger.debug(f"Blocking trees: {block_trees}")
447
+ # r = requests.post(
448
+ # url=URL,
449
+ # json={
450
+ # "steps_count": self.stepsCount,
451
+ # "block_widgets": block_widgets,
452
+ # "block_trees": block_trees
453
+ # }
454
+ # )
455
+
456
+ # res = json.loads(r.content)
457
+ # xml_raw = res["result"]
458
+ # return xml_raw
459
+
460
+ @property
461
+ def _monkeyStepInfo(self):
462
+ r = self._get_block_widgets()
463
+ r["steps_count"] = self.stepsCount
464
+ return r
465
+
466
+ def _get_block_widgets(self):
431
467
  block_dict = self._getBlockedWidgets()
432
468
  block_widgets: List[str] = block_dict['widgets']
433
469
  block_trees: List[str] = block_dict['trees']
434
- URL = f"http://localhost:{self.scriptDriver.lport}/stepMonkey"
435
- logger.debug(f"Sending request: {URL}")
436
470
  logger.debug(f"Blocking widgets: {block_widgets}")
437
471
  logger.debug(f"Blocking trees: {block_trees}")
438
- r = requests.post(
439
- url=URL,
440
- json={
441
- "steps_count": self.stepsCount,
442
- "block_widgets": block_widgets,
443
- "block_trees": block_trees
444
- }
445
- )
446
-
447
- res = json.loads(r.content)
448
- xml_raw = res["result"]
449
- return xml_raw
450
-
451
- def stopMonkey(self) -> str:
452
- """
453
- send a stop monkey request to the server and get the xml string.
454
- """
455
- URL = f"http://localhost:{self.scriptDriver.lport}/stopMonkey"
456
- logger.debug(f"Sending request: {URL}")
457
- r = requests.get(URL)
458
-
459
- res = r.content.decode(encoding="utf-8")
460
- print(f"[Server INFO] {res}", flush=True)
472
+ return {
473
+ "block_widgets": block_widgets,
474
+ "block_trees": block_trees
475
+ }
461
476
 
462
477
  def getValidProperties(self, xml_raw: str, result: JsonResult) -> PropertyStore:
463
478
 
@@ -490,40 +505,13 @@ class KeaTestRunner(TextTestRunner):
490
505
  print(f"{getFullPropName(test)} has reached its max_tries. Skip.", flush=True)
491
506
  continue
492
507
  validProps[propName] = test
493
-
508
+
494
509
  print(f"{len(validProps)} precond satisfied.", flush=True)
495
510
  if len(validProps) > 0:
496
511
  print("[INFO] Valid properties:",flush=True)
497
512
  print("\n".join([f' - {getFullPropName(p)}' for p in validProps.values()]), flush=True)
498
513
  return validProps
499
514
 
500
- def _logScript(self, execution_info:Dict):
501
- URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
502
- r = requests.post(
503
- url=URL,
504
- json=execution_info
505
- )
506
- res = r.content.decode(encoding="utf-8")
507
- if res != "OK":
508
- print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
509
-
510
- def _init(self):
511
- URL = f"http://localhost:{self.scriptDriver.lport}/init"
512
- data = {
513
- "takeScreenshots": self.options.take_screenshots,
514
- "Stamp": STAMP,
515
- "deviceOutputRoot": self.options.device_output_root,
516
- }
517
- print(f"[INFO] Init fastbot: {data}", flush=True)
518
- r = requests.post(
519
- url=URL,
520
- json=data
521
- )
522
- res = r.content.decode(encoding="utf-8")
523
- import re
524
- self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
525
- print(f"[INFO] Fastbot initiated. outputDir: {res}", flush=True)
526
-
527
515
  def collectAllProperties(self, test: TestSuite):
528
516
  """collect all the properties to prepare for PBT
529
517
  """
@@ -679,17 +667,19 @@ class KeaTestRunner(TextTestRunner):
679
667
  def __del__(self):
680
668
  """tearDown method. Cleanup the env.
681
669
  """
682
- try:
683
- self.stopMonkey()
684
- except Exception as e:
685
- pass
686
-
687
670
  if self.options.Driver:
688
671
  self.options.Driver.tearDown()
689
672
 
690
673
  try:
674
+ start_time = time.time()
691
675
  logger.info("Generating bug report")
692
676
  report_generator = BugReportGenerator(self.options.output_dir)
693
677
  report_generator.generate_report()
678
+
679
+ end_time = time.time()
680
+ generation_time = end_time - start_time
681
+
682
+ logger.info(f"Bug report generation completed in {generation_time:.2f} seconds")
683
+
694
684
  except Exception as e:
695
685
  logger.error(f"Error generating bug report: {e}", flush=True)
kea2/kea_launcher.py CHANGED
@@ -9,10 +9,22 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
9
9
  "-s",
10
10
  "--serial",
11
11
  dest="serial",
12
+ required=False,
13
+ default=None,
12
14
  type=str,
13
15
  help="The serial of your device. Can be found with `adb devices`",
14
16
  )
15
17
 
18
+ parser.add_argument(
19
+ "-t",
20
+ "--transport-id",
21
+ dest="transport_id",
22
+ required=False,
23
+ default=None,
24
+ type=str,
25
+ help="transport-id of your device, can be found with `adb devices -l`",
26
+ )
27
+
16
28
  parser.add_argument(
17
29
  "-p",
18
30
  "--packages",
@@ -128,6 +140,8 @@ def driver_info_logger(args):
128
140
  print("[INFO] Driver Settings:", flush=True)
129
141
  if args.serial:
130
142
  print(" serial:", args.serial, flush=True)
143
+ if args.transport_id:
144
+ print(" transport_id:", args.transport_id, flush=True)
131
145
  if args.package_names:
132
146
  print(" package_names:", args.package_names, flush=True)
133
147
  if args.agent:
@@ -174,6 +188,7 @@ def run(args=None):
174
188
  Driver=U2Driver,
175
189
  packageNames=args.package_names,
176
190
  serial=args.serial,
191
+ transport_id=args.transport_id,
177
192
  running_mins=args.running_minutes,
178
193
  maxStep=args.max_step,
179
194
  throttle=args.throttle_ms,
kea2/resultSyncer.py CHANGED
@@ -1,19 +1,26 @@
1
+ from pathlib import Path
1
2
  import threading
2
- from .adbUtils import adb_shell, pull_file
3
+ from .adbUtils import ADBDevice
3
4
  from .utils import getLogger
5
+ from typing import TYPE_CHECKING
6
+ if TYPE_CHECKING:
7
+ from .keaUtils import Options
4
8
 
5
9
  logger = getLogger(__name__)
6
10
 
7
11
 
8
12
  class ResultSyncer:
9
13
 
10
- def __init__(self, device_output_dir, output_dir):
14
+ def __init__(self, device_output_dir, options: "Options"):
11
15
  self.device_output_dir = device_output_dir
12
- self.output_dir = output_dir
16
+ self.output_dir = Path(options.output_dir) / Path(device_output_dir).name
13
17
  self.running = False
14
18
  self.thread = None
15
19
  self.sync_event = threading.Event()
16
20
 
21
+ ADBDevice.setDevice(serial=options.serial, transport_id=options.transport_id)
22
+ self.dev = ADBDevice()
23
+
17
24
  def run(self):
18
25
  """Start a background thread to sync device data when triggered"""
19
26
  self.running = True
@@ -37,7 +44,8 @@ class ResultSyncer:
37
44
  try:
38
45
  logger.debug(f"Removing device output directory: {self.device_output_dir}")
39
46
  remove_device_dir = ["rm", "-rf", self.device_output_dir]
40
- adb_shell(remove_device_dir)
47
+ # adb_shell(remove_device_dir)
48
+ self.dev.shell(remove_device_dir)
41
49
  except Exception as e:
42
50
  logger.error(f"Error removing device output directory: {e}", flush=True)
43
51
 
@@ -48,9 +56,11 @@ class ResultSyncer:
48
56
  try:
49
57
  logger.debug("Syncing data")
50
58
 
51
- pull_file(self.device_output_dir, str(self.output_dir))
59
+ self.dev.sync.pull_dir(self.device_output_dir, self.output_dir, exist_ok=True)
60
+ # pull_file(self.device_output_dir, str(self.output_dir))
52
61
 
53
- remove_pulled_screenshots = ["find", self.device_output_dir, "-name", "\"*.png\"", "-delete"]
54
- adb_shell(remove_pulled_screenshots)
62
+ remove_pulled_screenshots = ["find", self.device_output_dir, "-name", '"*.png"', "-delete"]
63
+ self.dev.shell(remove_pulled_screenshots)
64
+ # adb_shell(remove_pulled_screenshots)
55
65
  except Exception as e:
56
- logger.error(f"Error in data sync: {e}", flush=True)
66
+ logger.error(f"Error in data sync: {e}")
@@ -356,48 +356,7 @@
356
356
 
357
357
  <!-- Key Statistics -->
358
358
  <div class="row g-4 mb-4">
359
- <div class="col-md-6">
360
- <div class="stats-card">
361
- <div class="card-header bg-primary text-white">
362
- <i class="bi bi-stopwatch"></i> Time Statistics
363
- </div>
364
- <div class="card-body">
365
- <div class="mb-3">
366
- <h5 class="d-flex justify-content-between">
367
- <span>First Bug Discovery Time:</span>
368
- <span class="value-danger">{{ (first_bug_time / 60)|int }} min: {{ (first_bug_time % 60)|int }} sec</span>
369
- </h5>
370
- <div class="progress">
371
- <div class="progress-bar bg-danger" role="progressbar"
372
- style="width: calc({{ first_bug_time }} / {{ total_testing_time }} * 100%);"
373
- aria-valuenow="{{ first_bug_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
374
- </div>
375
- </div>
376
- <div class="mb-3">
377
- <h5 class="d-flex justify-content-between">
378
- <span>First Precondition Satisfaction Time:</span>
379
- <span class="value-success">{{ (first_precondition_time / 60)|int }} min: {{ (first_precondition_time % 60)|int }} sec</span>
380
- </h5>
381
- <div class="progress">
382
- <div class="progress-bar bg-success" role="progressbar"
383
- style="width: calc({{ first_precondition_time }} / {{ total_testing_time }} * 100%);"
384
- aria-valuenow="{{ first_precondition_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
385
- </div>
386
- </div>
387
- <div>
388
- <h5 class="d-flex justify-content-between">
389
- <span>Total Testing Time:</span>
390
- <span class="value-primary">{{ (total_testing_time / 60)|int }} min: {{ (total_testing_time % 60)|int }} sec</span>
391
- </h5>
392
- <div class="progress">
393
- <div class="progress-bar bg-primary" role="progressbar" style="width: 100%;"
394
- aria-valuenow="{{ total_testing_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
395
- </div>
396
- </div>
397
- </div>
398
- </div>
399
- </div>
400
- <div class="col-md-6">
359
+ <div class="col-12">
401
360
  <div class="stats-card">
402
361
  <div class="card-header bg-success text-white">
403
362
  <i class="bi bi-bar-chart"></i> Coverage Statistics
@@ -437,7 +396,7 @@
437
396
  <!-- Tested Activities Panel -->
438
397
  <div class="col-md-6">
439
398
  <div class="card">
440
- <div class="card-header bg-success text-white">
399
+ <div class="card-header bg-warning text-white">
441
400
  <div class="d-flex justify-content-between align-items-center">
442
401
  <span><i class="bi bi-check-circle"></i> Tested Activities ({{ tested_activities|length }})</span>
443
402
  <span class="badge bg-light text-dark">{{ tested_activities|length }} / {{ total_activities_count }}</span>
@@ -605,14 +564,14 @@
605
564
  </tr>
606
565
  </thead>
607
566
  <tbody id="property-stats-container">
608
- {% for stat in property_stats %}
567
+ {% for property_name, test_result in property_stats.items() %}
609
568
  <tr class="property-stat-row" data-page="1">
610
- <td>{{ stat.index }}</td>
611
- <td><span class="badge bg-light text-dark badge-custom">{{ stat.property_name }}</span></td>
612
- <td>{{ stat.precond_satisfied }}</td>
613
- <td>{{ stat.precond_checked }}</td>
614
- <td><span class="badge bg-danger text-white">{{ stat.postcond_violated }}</span></td>
615
- <td><span class="badge bg-warning text-dark">{{ stat.error }}</span></td>
569
+ <td>{{ loop.index }}</td>
570
+ <td><span class="badge bg-light text-dark badge-custom">{{ property_name }}</span></td>
571
+ <td>{{ test_result.precond_satisfied|default(0) }}</td>
572
+ <td>{{ test_result.executed|default(0) }}</td>
573
+ <td><span class="badge bg-danger text-white">{{ test_result.fail|default(0) }}</span></td>
574
+ <td><span class="badge bg-warning text-dark">{{ test_result.error|default(0) }}</span></td>
616
575
  </tr>
617
576
  {% endfor %}
618
577
  </tbody>