Skip to content

Commit 6e88413

Browse files
committed
Added functionality to run java E2E framework from application on current project/module
1 parent 9d2c39c commit 6e88413

8 files changed

Lines changed: 647 additions & 15 deletions

File tree

core/test_runner.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from PyQt6.QtCore import QObject, QProcess, pyqtSignal, QProcessEnvironment, QTimer
2+
from models.run_config import RunConfig
3+
import os
4+
5+
# MAVEN_EXECUTABLE_PATH = "C:\\Users\\meetd\\Downloads\\apache-maven-3.9.10-bin\\apache-maven-3.9.10\\bin\\mvn.cmd"
6+
MAVEN_EXECUTABLE_PATH = "mvn"
7+
8+
class TestRunner(QObject):
9+
process_started = pyqtSignal()
10+
process_finished = pyqtSignal(int)
11+
output_received = pyqtSignal(str)
12+
error_received = pyqtSignal(str)
13+
14+
def __init__(self, e2e_dir: str):
15+
super().__init__()
16+
self.e2e_dir = e2e_dir
17+
self.process = None
18+
self.is_running = False
19+
self._force_kill_timer = QTimer()
20+
self._force_kill_timer.setSingleShot(True)
21+
self._force_kill_timer.timeout.connect(self._force_kill_process)
22+
23+
def run_test(self, config: RunConfig):
24+
if self.is_running:
25+
self.output_received.emit("Test already running! Stop it first.")
26+
return
27+
28+
# Verify directory and pom.xml
29+
if not os.path.exists(self.e2e_dir):
30+
self.error_received.emit(f"E2E directory does not exist: {self.e2e_dir}")
31+
return
32+
33+
pom_file = os.path.join(self.e2e_dir, "pom.xml")
34+
if not os.path.exists(pom_file):
35+
self.error_received.emit(f"pom.xml not found in E2E directory: {self.e2e_dir}")
36+
return
37+
38+
args = self._build_command_args(config)
39+
40+
self.process = QProcess()
41+
self.process.setWorkingDirectory(self.e2e_dir)
42+
43+
# Use system environment as-is (your PATH already has everything needed)
44+
env = QProcessEnvironment.systemEnvironment()
45+
46+
# Just set JAVA_HOME since it's missing
47+
if not env.contains("JAVA_HOME"):
48+
env.insert("JAVA_HOME", r"C:\Program Files\Java\jdk-21")
49+
50+
self.process.setProcessEnvironment(env)
51+
52+
self.process.readyReadStandardOutput.connect(self._on_stdout)
53+
self.process.readyReadStandardError.connect(self._on_stderr)
54+
self.process.finished.connect(self._on_finished)
55+
self.process.errorOccurred.connect(self._on_error)
56+
57+
self.is_running = True
58+
self.process_started.emit()
59+
self.output_received.emit(f"Starting test: {config.project_name}/{config.module_name}")
60+
61+
# Use mvn.cmd explicitly
62+
self.process.start("mvn.cmd", args)
63+
64+
def _build_command_args(self, config: RunConfig) -> list:
65+
exec_args = f"{config.project_name} {config.base_url} {config.browser} {config.video_option} {config.module_name} {config.wait_time}"
66+
67+
return [
68+
"clean",
69+
"compile",
70+
"exec:java",
71+
f"-Dexec.mainClass=com.startAuto.StartAuto",
72+
f"-Dexec.args={exec_args}"
73+
]
74+
75+
def stop_test(self):
76+
if not self.is_running or not self.process:
77+
return
78+
79+
self.output_received.emit("Stopping test execution...")
80+
self.process.terminate()
81+
82+
self._force_kill_timer.start(3000)
83+
84+
def _force_kill_process(self):
85+
if self.is_running and self.process and self.process.state() == QProcess.ProcessState.Running:
86+
self.process.kill()
87+
self.output_received.emit("Force killed process")
88+
self.is_running = False
89+
self.output_received.emit("Test stopped")
90+
91+
def _on_stdout(self):
92+
if self.process:
93+
data = self.process.readAllStandardOutput().data().decode()
94+
self.output_received.emit(data)
95+
96+
def _on_stderr(self):
97+
if self.process:
98+
data = self.process.readAllStandardError().data().decode()
99+
self.error_received.emit(f"❌ {data}")
100+
101+
def _on_finished(self, exit_code):
102+
self.is_running = False
103+
self._force_kill_timer.stop()
104+
if exit_code == 0:
105+
self.output_received.emit("✅ Test completed successfully")
106+
else:
107+
self.output_received.emit(f"❌ Test failed with exit code: {exit_code}")
108+
self.process_finished.emit(exit_code)
109+
110+
def is_test_running(self):
111+
return self.is_running
112+
113+
def cleanup(self):
114+
if self.process:
115+
self._force_kill_timer.stop()
116+
117+
if self.process.state() == QProcess.ProcessState.Running:
118+
self.process.kill()
119+
self.process.waitForFinished(100)
120+
121+
try:
122+
self.process.readyReadStandardOutput.disconnect()
123+
self.process.readyReadStandardError.disconnect()
124+
self.process.finished.disconnect()
125+
self.process.errorOccurred.disconnect()
126+
except:
127+
pass
128+
129+
self.process = None
130+
131+
self.is_running = False
132+
133+
def _on_error(self, error: QProcess.ProcessError):
134+
error_messages = {
135+
QProcess.ProcessError.FailedToStart: "❌ Failed to start the process. Check if Maven and Java are properly configured.",
136+
QProcess.ProcessError.Crashed: "The process crashed.",
137+
QProcess.ProcessError.Timedout: "The process timed out.",
138+
QProcess.ProcessError.ReadError: "An error occurred while reading from the process.",
139+
QProcess.ProcessError.WriteError: "An error occurred while writing to the process.",
140+
QProcess.ProcessError.UnknownError: "An unknown error occurred."
141+
}
142+
self.error_received.emit(f"Process Error: {error_messages.get(error, 'An unknown error occurred.')}")
143+
144+
if error == QProcess.ProcessError.FailedToStart:
145+
self.error_received.emit("Please ensure:")
146+
self.error_received.emit("1. Java JDK is installed and JAVA_HOME is set")
147+
self.error_received.emit("2. Maven is installed and in PATH")
148+
self.error_received.emit("4. The E2E directory contains a valid Maven project")

models/run_config.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from dataclasses import dataclass
2+
from PyQt6.QtCore import QSettings
3+
4+
@dataclass
5+
class RunConfig:
6+
project_name: str = ""
7+
module_name: str = ""
8+
base_url: str = ""
9+
browser: str = "ChromeHL"
10+
video_option: str = "NONE"
11+
wait_time: int = 30
12+
13+
def save_to_settings(self):
14+
settings = QSettings("TestEditor", "RunConfig")
15+
settings.setValue("browser", self.browser)
16+
settings.setValue("video_option", self.video_option)
17+
settings.setValue("wait_time", self.wait_time)
18+
19+
def load_from_settings(self):
20+
settings = QSettings("TestEditor", "RunConfig")
21+
self.browser = settings.value("browser", "ChromeHL")
22+
self.video_option = settings.value("video_option", "NONE")
23+
self.wait_time = settings.value("wait_time", 30, type=int)
24+
25+
default_config = RunConfig()
26+
default_config.load_from_settings()
27+
28+
29+
def get_base_url(project_name: str) -> str:
30+
return PROJECT_URLS.get(project_name, "")
31+
32+
PROJECT_URLS = {
33+
"qaoptimus": "https://qaoptimus.synoption.com/#/auth/login",
34+
"uatoptimus": "https://uatoptimus.synoption.com/#/auth/login",
35+
"qatitan": "https://qatitan.synoption.com/#/auth/login",
36+
"uattitan": "https://uattitan.synoption.com/#/auth/login",
37+
"uatmaqa": "https://uatmaqa.synoption.com/#/auth/login",
38+
"qaocbctitan": "https://qaocbctitan.synoption.com/#/auth/login",
39+
"uatocbctitan": "https://uatocbctitan.synoption.com/#/auth/login",
40+
}

ui/file_ui.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import re
3+
from pathlib import Path
34
from PyQt6.QtWidgets import (
45
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QLabel,
56
QPushButton, QListWidgetItem, QFileDialog, QMessageBox,
@@ -12,6 +13,7 @@ def __init__(self,main = None, mode="open"):
1213
super().__init__(main)
1314
self.mode = mode
1415
self.main = main
16+
self.e2e_dir = None
1517
self.data_directory = None
1618
self.selected_project = None
1719
self.selected_scenario_path = None
@@ -36,13 +38,13 @@ def _setup_ui(self):
3638

3739
def _create_directory_layout(self):
3840
dir_layout = QHBoxLayout()
39-
dir_layout.addWidget(QLabel("Data Directory: "))
41+
dir_layout.addWidget(QLabel("E2E Directory: "))
4042

4143
self.dir_label = QLabel("Not Selected")
4244
dir_layout.addWidget(self.dir_label)
4345

4446
self.select_dir = QPushButton("Select Dir")
45-
self.select_dir.clicked.connect(self._select_data_directory)
47+
self.select_dir.clicked.connect(self._select_e2e_directory)
4648
dir_layout.addWidget(self.select_dir)
4749

4850
return dir_layout
@@ -108,27 +110,49 @@ def _create_buttons_section(self):
108110

109111
def _load_last_directory(self):
110112
settings = QSettings("TestEditor", "Settings")
111-
last_dir = settings.value("last_directory", "")
113+
e2e_dir = settings.value("e2e_dir", "")
112114

113-
if last_dir and os.path.exists(last_dir):
114-
self.data_directory = last_dir
115-
self.dir_label.setText(last_dir)
115+
if e2e_dir and os.path.exists(e2e_dir):
116+
self.e2e_dir = e2e_dir
117+
self.data_directory = os.path.join(e2e_dir, "data")
118+
self.dir_label.setText(e2e_dir)
116119
self._populate_projects()
117120
self._load_last_project()
118121

119122
def _save_last_directory(self):
120123
if self.data_directory:
121124
settings = QSettings("TestEditor", "Settings")
122-
settings.setValue("last_directory", self.data_directory)
125+
settings.setValue("e2e_dir", self.e2e_dir)
123126

124-
def _select_data_directory(self):
127+
def _select_e2e_directory(self):
125128
directory = QFileDialog.getExistingDirectory(self, "Select Directory")
126129
if directory:
127-
self.data_directory = directory
128-
self.dir_label.setText(directory)
129-
self._save_last_directory()
130-
self._populate_projects()
131-
self._load_last_project()
130+
e2e_dir = directory
131+
data_dir = os.path.join(directory, "data")
132+
133+
if self._validate_directory(e2e_dir, data_dir):
134+
self.e2e_dir = directory
135+
self.data_directory = data_dir
136+
137+
self.dir_label.setText(self.data_directory)
138+
self._save_last_directory()
139+
self._populate_projects()
140+
self._load_last_project()
141+
else:
142+
QMessageBox.warning(self, "Invalid Directory",
143+
"Selected directory should be the E2E application root with 'data/' subdirectory")
144+
145+
def _validate_directory(self, e2e_dir, data_dir):
146+
app_path = Path(e2e_dir)
147+
data_path = Path(data_dir)
148+
149+
has_pom = (app_path / "pom.xml").exists()
150+
has_src = (app_path / "src").exists()
151+
152+
has_test_suites = (data_path / "testSuites").exists()
153+
has_obj_repos = (data_path / "objectRepositories").exists()
154+
155+
return has_pom and has_src and has_test_suites and has_obj_repos
132156

133157
def _load_last_project(self):
134158
if self.mode != "open":

ui/menu.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from PyQt6.QtGui import QAction, QKeySequence
2-
from PyQt6.QtWidgets import QMenuBar, QMessageBox
2+
from PyQt6.QtWidgets import QMenuBar, QMessageBox, QToolBar, QToolButton
3+
from PyQt6.QtCore import Qt
4+
from PyQt6.QtWidgets import QStyle
5+
from PyQt6.QtCore import QSize
36
from core.commands import COMMANDS
47
from models.test_scenario import TestScenarioModel
58
from typing import TYPE_CHECKING
@@ -14,6 +17,7 @@ def __init__(self, main: "MainWindow") -> None:
1417
self.main = main
1518
self.file_ops = main.file_ops
1619
self.create_menu_bar()
20+
self.create_corner_run_toolbar()
1721

1822
def create_menu_bar(self) -> None:
1923

@@ -165,3 +169,45 @@ def apply_fixed_widths(self) -> None:
165169
message_type="warning",
166170
timeout=5000
167171
)
172+
173+
def create_corner_run_toolbar(self):
174+
"""Super compact icons-only in corner"""
175+
run_toolbar = QToolBar("Run", self)
176+
run_toolbar.setAccessibleName("corner-toolbar")
177+
run_toolbar.setMovable(False)
178+
run_toolbar.setFloatable(False)
179+
run_toolbar.setIconSize(QSize(14, 14))
180+
181+
# Add buttons (same as above)
182+
self.run_button = QToolButton()
183+
self.run_button.setText("▶")
184+
self.run_button.setToolTip("Run Current Module (Ctrl+R)")
185+
self.run_button.clicked.connect(self.run_current_module)
186+
self.run_button.setObjectName("RunButton")
187+
run_toolbar.addWidget(self.run_button)
188+
189+
self.stop_button = QToolButton()
190+
self.stop_button.setText("■")
191+
self.stop_button.setToolTip("Stop Execution (Ctrl+Shift+R)")
192+
self.stop_button.clicked.connect(self.stop_test)
193+
self.stop_button.setObjectName("StopButton")
194+
self.stop_button.setEnabled(False)
195+
run_toolbar.addWidget(self.stop_button)
196+
197+
self.config_button = QToolButton()
198+
self.config_button.setText("⚙")
199+
self.config_button.setToolTip("Run Configuration")
200+
self.config_button.clicked.connect(self.show_run_config)
201+
self.config_button.setObjectName("ConfigButton")
202+
run_toolbar.addWidget(self.config_button)
203+
204+
self.main.menuBar().setCornerWidget(run_toolbar, Qt.Corner.TopRightCorner)
205+
206+
def run_current_module(self):
207+
self.main.run_current_module()
208+
209+
def stop_test(self):
210+
self.main.stop_test()
211+
212+
def show_run_config(self):
213+
self.main.show_run_config()

ui/output_dock.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from PyQt6.QtWidgets import (QDockWidget, QTextEdit, QVBoxLayout, QWidget,
2+
QHBoxLayout, QPushButton, QLabel)
3+
from PyQt6.QtCore import Qt, pyqtSignal
4+
5+
class ConsoleDock(QDockWidget):
6+
clear_requested = pyqtSignal()
7+
8+
def __init__(self, parent=None):
9+
super().__init__("Console", parent)
10+
self.setup_ui()
11+
12+
def setup_ui(self):
13+
widget = QWidget()
14+
layout = QVBoxLayout(widget)
15+
16+
header_layout = QHBoxLayout()
17+
18+
self.status_label = QLabel("Ready")
19+
header_layout.addWidget(self.status_label)
20+
21+
header_layout.addStretch()
22+
23+
self.clear_btn = QPushButton("Clear")
24+
self.clear_btn.clicked.connect(self.clear_requested.emit)
25+
self.clear_btn.setMaximumWidth(60)
26+
header_layout.addWidget(self.clear_btn)
27+
28+
layout.addLayout(header_layout)
29+
30+
self.output_text = QTextEdit()
31+
self.output_text.setReadOnly(True)
32+
self.output_text.setPlaceholderText("Test output will appear here...")
33+
34+
layout.addWidget(self.output_text)
35+
self.setWidget(widget)
36+
37+
def append_output(self, text):
38+
self.output_text.append(text)
39+
cursor = self.output_text.textCursor()
40+
cursor.movePosition(cursor.MoveOperation.End)
41+
self.output_text.setTextCursor(cursor)
42+
43+
def clear_output(self):
44+
self.output_text.clear()
45+
self.status_label.setText("Ready")
46+
47+
def set_status(self, status, is_error=False):
48+
color = "red" if is_error else "green"
49+
self.status_label.setText(f"<span style='color: {color};'>{status}</span>")

0 commit comments

Comments
 (0)