nextog-cli 1.0.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.
- nextog/__init__.py +4 -0
- nextog/cli.py +545 -0
- nextog/config/__init__.py +1 -0
- nextog/config/settings.py +132 -0
- nextog/core/__init__.py +1 -0
- nextog/core/engine.py +193 -0
- nextog/core/permissions.py +129 -0
- nextog/core/privacy.py +130 -0
- nextog/core/reporter.py +204 -0
- nextog/core/runner.py +236 -0
- nextog/data/__init__.py +1 -0
- nextog/data/local_db.py +367 -0
- nextog/data/models.py +72 -0
- nextog/data/sync.py +65 -0
- nextog/engines/__init__.py +1 -0
- nextog/engines/api/__init__.py +1 -0
- nextog/engines/api/graphql.py +54 -0
- nextog/engines/api/rest.py +346 -0
- nextog/engines/api/websocket.py +59 -0
- nextog/engines/embedded/__init__.py +1 -0
- nextog/engines/embedded/firmware.py +53 -0
- nextog/engines/embedded/hardware.py +330 -0
- nextog/engines/mobile/__init__.py +1 -0
- nextog/engines/mobile/android.py +333 -0
- nextog/engines/mobile/cross.py +48 -0
- nextog/engines/mobile/ios.py +46 -0
- nextog/engines/system/__init__.py +1 -0
- nextog/engines/system/load.py +121 -0
- nextog/engines/system/performance.py +128 -0
- nextog/engines/system/security.py +170 -0
- nextog/engines/web/__init__.py +1 -0
- nextog/engines/web/accessibility.py +191 -0
- nextog/engines/web/browser.py +387 -0
- nextog/engines/web/elements.py +285 -0
- nextog/engines/web/responsive.py +79 -0
- nextog/live/__init__.py +1 -0
- nextog/live/dashboard.py +30 -0
- nextog/live/panel.py +325 -0
- nextog/reports/__init__.py +1359 -0
- nextog/training/__init__.py +1 -0
- nextog/training/learner.py +269 -0
- nextog/training/patterns.py +102 -0
- nextog/utils/__init__.py +1 -0
- nextog/utils/helpers.py +91 -0
- nextog/utils/logger.py +37 -0
- nextog/utils/validators.py +98 -0
- nextog_cli-1.0.0.dist-info/METADATA +344 -0
- nextog_cli-1.0.0.dist-info/RECORD +51 -0
- nextog_cli-1.0.0.dist-info/WHEEL +5 -0
- nextog_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nextog_cli-1.0.0.dist-info/top_level.txt +1 -0
nextog/core/runner.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Runner - Orchestrates test execution across all engines
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestRunner:
|
|
16
|
+
"""Orchestrates running tests across multiple engines"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings, db, privacy, permissions):
|
|
19
|
+
self.settings = settings
|
|
20
|
+
self.db = db
|
|
21
|
+
self.privacy = privacy
|
|
22
|
+
self.permissions = permissions
|
|
23
|
+
|
|
24
|
+
def run_all(self, config_path: str, coverage_target: int = 90, parallel: bool = True) -> Dict:
|
|
25
|
+
"""Run all tests from configuration"""
|
|
26
|
+
config = self._load_config(config_path)
|
|
27
|
+
results = {
|
|
28
|
+
"categories": {},
|
|
29
|
+
"total_coverage": 0,
|
|
30
|
+
"timestamp": datetime.now().isoformat(),
|
|
31
|
+
"duration": 0,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
engines_config = config.get("engines", {})
|
|
35
|
+
total_phases = len(engines_config)
|
|
36
|
+
|
|
37
|
+
with Progress(
|
|
38
|
+
SpinnerColumn(),
|
|
39
|
+
TextColumn("[progress.description]{task.description}"),
|
|
40
|
+
BarColumn(),
|
|
41
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
42
|
+
console=console,
|
|
43
|
+
) as progress:
|
|
44
|
+
overall = progress.add_task("Running all tests...", total=total_phases)
|
|
45
|
+
|
|
46
|
+
for engine_name, engine_config in engines_config.items():
|
|
47
|
+
progress.update(overall, description=f"Running {engine_name} tests...")
|
|
48
|
+
|
|
49
|
+
engine = self._get_engine(engine_name)
|
|
50
|
+
if engine:
|
|
51
|
+
engine_results = engine.run_tests(**engine_config)
|
|
52
|
+
results["categories"][engine_name] = engine_results
|
|
53
|
+
results["total_coverage"] = self._calc_coverage(results)
|
|
54
|
+
|
|
55
|
+
progress.advance(overall)
|
|
56
|
+
|
|
57
|
+
results["duration"] = self._calculate_duration(results)
|
|
58
|
+
self.db.save_test_results(results)
|
|
59
|
+
self.privacy.record_activity("full_test_run", results)
|
|
60
|
+
|
|
61
|
+
return results
|
|
62
|
+
|
|
63
|
+
def _load_config(self, config_path: str) -> Dict:
|
|
64
|
+
"""Load test configuration from YAML"""
|
|
65
|
+
import yaml
|
|
66
|
+
path = Path(config_path)
|
|
67
|
+
if path.exists():
|
|
68
|
+
with open(path) as f:
|
|
69
|
+
return yaml.safe_load(f) or {}
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
def _get_engine(self, name: str):
|
|
73
|
+
"""Get engine instance by name"""
|
|
74
|
+
engine_map = {
|
|
75
|
+
"web": ("nextog.engines.web.browser", "WebTestEngine"),
|
|
76
|
+
"mobile": ("nextog.engines.mobile.android", "MobileTestEngine"),
|
|
77
|
+
"api": ("nextog.engines.api.rest", "APITestEngine"),
|
|
78
|
+
"embedded": ("nextog.engines.embedded.hardware", "EmbeddedTestEngine"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if name not in engine_map:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
module_path, class_name = engine_map[name]
|
|
85
|
+
import importlib
|
|
86
|
+
module = importlib.import_module(module_path)
|
|
87
|
+
engine_class = getattr(module, class_name)
|
|
88
|
+
return engine_class(self.settings, self.db, self.privacy)
|
|
89
|
+
|
|
90
|
+
def _calc_coverage(self, results: Dict) -> float:
|
|
91
|
+
"""Calculate total coverage from all results"""
|
|
92
|
+
total = 0
|
|
93
|
+
count = 0
|
|
94
|
+
for cat_data in results.get("categories", {}).values():
|
|
95
|
+
if isinstance(cat_data, dict) and "coverage" in cat_data:
|
|
96
|
+
total += cat_data["coverage"]
|
|
97
|
+
count += 1
|
|
98
|
+
return round(total / count, 2) if count > 0 else 0
|
|
99
|
+
|
|
100
|
+
def _calculate_duration(self, results: Dict) -> float:
|
|
101
|
+
"""Calculate total test duration"""
|
|
102
|
+
return sum(
|
|
103
|
+
cat.get("duration", 0)
|
|
104
|
+
for cat in results.get("categories", {}).values()
|
|
105
|
+
if isinstance(cat, dict)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProjectInitializer:
|
|
110
|
+
"""Initialize new nextOG projects"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, settings, db):
|
|
113
|
+
self.settings = settings
|
|
114
|
+
self.db = db
|
|
115
|
+
|
|
116
|
+
def create_project(self, name: str, template: str = "full"):
|
|
117
|
+
"""Create a new project structure"""
|
|
118
|
+
project_dir = Path(name)
|
|
119
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
# Create .nextog directory
|
|
122
|
+
nextog_dir = project_dir / ".nextog"
|
|
123
|
+
nextog_dir.mkdir(exist_ok=True)
|
|
124
|
+
(nextog_dir / "tests").mkdir(exist_ok=True)
|
|
125
|
+
(nextog_dir / "reports").mkdir(exist_ok=True)
|
|
126
|
+
(nextog_dir / "data").mkdir(exist_ok=True)
|
|
127
|
+
|
|
128
|
+
# Generate config based on template
|
|
129
|
+
config = self._get_template(template, name)
|
|
130
|
+
|
|
131
|
+
import yaml
|
|
132
|
+
with open(nextog_dir / "config.yaml", "w") as f:
|
|
133
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
134
|
+
|
|
135
|
+
# Create initial test files based on template
|
|
136
|
+
self._create_test_files(nextog_dir / "tests", template)
|
|
137
|
+
|
|
138
|
+
def _get_template(self, template: str, name: str) -> Dict:
|
|
139
|
+
"""Get configuration template"""
|
|
140
|
+
templates = {
|
|
141
|
+
"full": {
|
|
142
|
+
"project": name,
|
|
143
|
+
"version": "1.0",
|
|
144
|
+
"engines": {
|
|
145
|
+
"web": {"url": "http://localhost:3000", "browser": "chromium", "responsive": True, "accessibility": True},
|
|
146
|
+
"api": {"base_url": "http://localhost:3000/api", "spec": "openapi.yaml"},
|
|
147
|
+
"mobile": {"platform": "android", "app": "./app.apk"},
|
|
148
|
+
"embedded": {"target": "localhost", "protocol": "mqtt"},
|
|
149
|
+
},
|
|
150
|
+
"coverage_target": 90,
|
|
151
|
+
"parallel": True,
|
|
152
|
+
},
|
|
153
|
+
"web": {
|
|
154
|
+
"project": name,
|
|
155
|
+
"version": "1.0",
|
|
156
|
+
"engines": {
|
|
157
|
+
"web": {"url": "http://localhost:3000", "browser": "chromium", "responsive": True, "accessibility": True},
|
|
158
|
+
},
|
|
159
|
+
"coverage_target": 90,
|
|
160
|
+
},
|
|
161
|
+
"mobile": {
|
|
162
|
+
"project": name,
|
|
163
|
+
"version": "1.0",
|
|
164
|
+
"engines": {
|
|
165
|
+
"mobile": {"platform": "android", "app": "./app.apk"},
|
|
166
|
+
},
|
|
167
|
+
"coverage_target": 90,
|
|
168
|
+
},
|
|
169
|
+
"api": {
|
|
170
|
+
"project": name,
|
|
171
|
+
"version": "1.0",
|
|
172
|
+
"engines": {
|
|
173
|
+
"api": {"base_url": "http://localhost:3000/api", "spec": "openapi.yaml"},
|
|
174
|
+
},
|
|
175
|
+
"coverage_target": 90,
|
|
176
|
+
},
|
|
177
|
+
"minimal": {
|
|
178
|
+
"project": name,
|
|
179
|
+
"version": "1.0",
|
|
180
|
+
"engines": {},
|
|
181
|
+
"coverage_target": 60,
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
return templates.get(template, templates["full"])
|
|
185
|
+
|
|
186
|
+
def _create_test_files(self, test_dir: Path, template: str):
|
|
187
|
+
"""Create initial test files"""
|
|
188
|
+
import yaml
|
|
189
|
+
|
|
190
|
+
# Create sample web test
|
|
191
|
+
if template in ("full", "web"):
|
|
192
|
+
web_test = {
|
|
193
|
+
"name": "Sample Web Tests",
|
|
194
|
+
"url": "http://localhost:3000",
|
|
195
|
+
"tests": [
|
|
196
|
+
{
|
|
197
|
+
"name": "Homepage loads",
|
|
198
|
+
"type": "smoke",
|
|
199
|
+
"steps": [{"action": "navigate", "target": "/"}],
|
|
200
|
+
"assertions": [{"type": "status", "expected": 200}],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "Login form works",
|
|
204
|
+
"type": "functional",
|
|
205
|
+
"steps": [
|
|
206
|
+
{"action": "navigate", "target": "/login"},
|
|
207
|
+
{"action": "fill", "target": "#email", "value": "test@test.com"},
|
|
208
|
+
{"action": "fill", "target": "#password", "value": "password"},
|
|
209
|
+
{"action": "click", "target": "#submit"},
|
|
210
|
+
],
|
|
211
|
+
"assertions": [{"type": "url_contains", "expected": "/dashboard"}],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}
|
|
215
|
+
with open(test_dir / "web_tests.yaml", "w") as f:
|
|
216
|
+
yaml.dump(web_test, f, default_flow_style=False)
|
|
217
|
+
|
|
218
|
+
# Create sample API test
|
|
219
|
+
if template in ("full", "api"):
|
|
220
|
+
api_test = {
|
|
221
|
+
"name": "Sample API Tests",
|
|
222
|
+
"base_url": "http://localhost:3000/api",
|
|
223
|
+
"tests": [
|
|
224
|
+
{
|
|
225
|
+
"name": "Health check",
|
|
226
|
+
"method": "GET",
|
|
227
|
+
"endpoint": "/health",
|
|
228
|
+
"assertions": [
|
|
229
|
+
{"type": "status_code", "expected": 200},
|
|
230
|
+
{"type": "response_time", "max_ms": 1000},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
with open(test_dir / "api_tests.yaml", "w") as f:
|
|
236
|
+
yaml.dump(api_test, f, default_flow_style=False)
|
nextog/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data management modules"""
|
nextog/data/local_db.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local Database - SQLite-based local storage for all nextOG data
|
|
3
|
+
Privacy-first: All data stays on the user's machine
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LocalDatabase:
|
|
17
|
+
"""Local SQLite database for nextOG CLI"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, db_path: str = None):
|
|
20
|
+
if db_path is None:
|
|
21
|
+
db_dir = Path.home() / ".nextog" / "data"
|
|
22
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
db_path = str(db_dir / "nextog.db")
|
|
24
|
+
|
|
25
|
+
self.db_path = db_path
|
|
26
|
+
self._init_db()
|
|
27
|
+
|
|
28
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
29
|
+
"""Get database connection"""
|
|
30
|
+
conn = sqlite3.connect(self.db_path)
|
|
31
|
+
conn.row_factory = sqlite3.Row
|
|
32
|
+
return conn
|
|
33
|
+
|
|
34
|
+
def _init_db(self):
|
|
35
|
+
"""Initialize database tables"""
|
|
36
|
+
conn = self._get_conn()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
conn.executescript("""
|
|
40
|
+
-- Test results storage
|
|
41
|
+
CREATE TABLE IF NOT EXISTS test_results (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
engine TEXT NOT NULL,
|
|
44
|
+
project TEXT,
|
|
45
|
+
results_json TEXT NOT NULL,
|
|
46
|
+
coverage REAL DEFAULT 0,
|
|
47
|
+
timestamp TEXT NOT NULL,
|
|
48
|
+
duration REAL DEFAULT 0
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- User accounts
|
|
52
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
username TEXT UNIQUE NOT NULL,
|
|
55
|
+
role TEXT NOT NULL DEFAULT 'tester',
|
|
56
|
+
email TEXT,
|
|
57
|
+
status TEXT DEFAULT 'active',
|
|
58
|
+
created_at TEXT NOT NULL,
|
|
59
|
+
last_login TEXT
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Learned patterns
|
|
63
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
pattern_type TEXT NOT NULL,
|
|
66
|
+
frequency INTEGER DEFAULT 0,
|
|
67
|
+
context TEXT,
|
|
68
|
+
related_elements TEXT,
|
|
69
|
+
timestamp TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Detected elements
|
|
73
|
+
CREATE TABLE IF NOT EXISTS elements (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
target TEXT NOT NULL,
|
|
76
|
+
element_json TEXT NOT NULL,
|
|
77
|
+
timestamp TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- Metadata storage
|
|
81
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
82
|
+
key TEXT PRIMARY KEY,
|
|
83
|
+
value TEXT NOT NULL,
|
|
84
|
+
updated_at TEXT NOT NULL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- Optimized test order
|
|
88
|
+
CREATE TABLE IF NOT EXISTS optimized_tests (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
test_name TEXT NOT NULL,
|
|
91
|
+
priority INTEGER DEFAULT 0,
|
|
92
|
+
engine TEXT,
|
|
93
|
+
timestamp TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
-- Activity log
|
|
97
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
98
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
|
+
activity_type TEXT NOT NULL,
|
|
100
|
+
data_json TEXT,
|
|
101
|
+
timestamp TEXT NOT NULL
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
-- Coverage tracking
|
|
105
|
+
CREATE TABLE IF NOT EXISTS coverage_history (
|
|
106
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
107
|
+
project TEXT,
|
|
108
|
+
coverage REAL NOT NULL,
|
|
109
|
+
phase TEXT,
|
|
110
|
+
timestamp TEXT NOT NULL
|
|
111
|
+
);
|
|
112
|
+
""")
|
|
113
|
+
|
|
114
|
+
conn.commit()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
console.print(f"[red]Database init error: {e}[/red]")
|
|
117
|
+
finally:
|
|
118
|
+
conn.close()
|
|
119
|
+
|
|
120
|
+
# ─── Test Results ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
def save_test_results(self, results: Dict):
|
|
123
|
+
"""Save test results"""
|
|
124
|
+
conn = self._get_conn()
|
|
125
|
+
try:
|
|
126
|
+
conn.execute(
|
|
127
|
+
"INSERT INTO test_results (engine, project, results_json, coverage, timestamp, duration) VALUES (?, ?, ?, ?, ?, ?)",
|
|
128
|
+
(
|
|
129
|
+
results.get("engine", "unknown"),
|
|
130
|
+
results.get("project", ""),
|
|
131
|
+
json.dumps(results.get("results", results), default=str),
|
|
132
|
+
results.get("results", {}).get("coverage", 0) if isinstance(results.get("results"), dict) else 0,
|
|
133
|
+
results.get("timestamp", datetime.now().isoformat()),
|
|
134
|
+
results.get("duration", 0),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
conn.commit()
|
|
138
|
+
finally:
|
|
139
|
+
conn.close()
|
|
140
|
+
|
|
141
|
+
def get_latest_results(self, project: str = None) -> Optional[Dict]:
|
|
142
|
+
"""Get latest test results"""
|
|
143
|
+
conn = self._get_conn()
|
|
144
|
+
try:
|
|
145
|
+
if project:
|
|
146
|
+
row = conn.execute(
|
|
147
|
+
"SELECT * FROM test_results WHERE project = ? ORDER BY timestamp DESC LIMIT 1",
|
|
148
|
+
(project,)
|
|
149
|
+
).fetchone()
|
|
150
|
+
else:
|
|
151
|
+
row = conn.execute(
|
|
152
|
+
"SELECT * FROM test_results ORDER BY timestamp DESC LIMIT 1"
|
|
153
|
+
).fetchone()
|
|
154
|
+
|
|
155
|
+
if row:
|
|
156
|
+
return {
|
|
157
|
+
"id": row["id"],
|
|
158
|
+
"engine": row["engine"],
|
|
159
|
+
"project": row["project"],
|
|
160
|
+
"results": json.loads(row["results_json"]),
|
|
161
|
+
"coverage": row["coverage"],
|
|
162
|
+
"timestamp": row["timestamp"],
|
|
163
|
+
}
|
|
164
|
+
finally:
|
|
165
|
+
conn.close()
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def get_all_test_results(self) -> List[Dict]:
|
|
169
|
+
"""Get all test results"""
|
|
170
|
+
conn = self._get_conn()
|
|
171
|
+
try:
|
|
172
|
+
rows = conn.execute("SELECT * FROM test_results ORDER BY timestamp DESC").fetchall()
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
"id": row["id"],
|
|
176
|
+
"engine": row["engine"],
|
|
177
|
+
"project": row["project"],
|
|
178
|
+
"results": json.loads(row["results_json"]),
|
|
179
|
+
"coverage": row["coverage"],
|
|
180
|
+
"timestamp": row["timestamp"],
|
|
181
|
+
}
|
|
182
|
+
for row in rows
|
|
183
|
+
]
|
|
184
|
+
finally:
|
|
185
|
+
conn.close()
|
|
186
|
+
|
|
187
|
+
def count_test_results(self) -> int:
|
|
188
|
+
"""Count test results"""
|
|
189
|
+
conn = self._get_conn()
|
|
190
|
+
try:
|
|
191
|
+
row = conn.execute("SELECT COUNT(*) FROM test_results").fetchone()
|
|
192
|
+
return row[0]
|
|
193
|
+
finally:
|
|
194
|
+
conn.close()
|
|
195
|
+
|
|
196
|
+
# ─── Users ──────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def save_user(self, user: Dict):
|
|
199
|
+
"""Save a user"""
|
|
200
|
+
conn = self._get_conn()
|
|
201
|
+
try:
|
|
202
|
+
conn.execute(
|
|
203
|
+
"INSERT OR REPLACE INTO users (username, role, email, status, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
204
|
+
(user["username"], user["role"], user.get("email"), user["status"], user["created_at"])
|
|
205
|
+
)
|
|
206
|
+
conn.commit()
|
|
207
|
+
finally:
|
|
208
|
+
conn.close()
|
|
209
|
+
|
|
210
|
+
def get_user(self, username: str) -> Optional[Dict]:
|
|
211
|
+
"""Get user by username"""
|
|
212
|
+
conn = self._get_conn()
|
|
213
|
+
try:
|
|
214
|
+
row = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
|
|
215
|
+
if row:
|
|
216
|
+
return dict(row)
|
|
217
|
+
finally:
|
|
218
|
+
conn.close()
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def get_all_users(self) -> List[Dict]:
|
|
222
|
+
"""Get all users"""
|
|
223
|
+
conn = self._get_conn()
|
|
224
|
+
try:
|
|
225
|
+
rows = conn.execute("SELECT * FROM users").fetchall()
|
|
226
|
+
return [dict(row) for row in rows]
|
|
227
|
+
finally:
|
|
228
|
+
conn.close()
|
|
229
|
+
|
|
230
|
+
def update_last_login(self, username: str):
|
|
231
|
+
"""Update user's last login"""
|
|
232
|
+
conn = self._get_conn()
|
|
233
|
+
try:
|
|
234
|
+
conn.execute(
|
|
235
|
+
"UPDATE users SET last_login = ? WHERE username = ?",
|
|
236
|
+
(datetime.now().isoformat(), username)
|
|
237
|
+
)
|
|
238
|
+
conn.commit()
|
|
239
|
+
finally:
|
|
240
|
+
conn.close()
|
|
241
|
+
|
|
242
|
+
def update_user_role(self, username: str, role: str):
|
|
243
|
+
"""Update user's role"""
|
|
244
|
+
conn = self._get_conn()
|
|
245
|
+
try:
|
|
246
|
+
conn.execute("UPDATE users SET role = ? WHERE username = ?", (role, username))
|
|
247
|
+
conn.commit()
|
|
248
|
+
finally:
|
|
249
|
+
conn.close()
|
|
250
|
+
|
|
251
|
+
def delete_user(self, username: str):
|
|
252
|
+
"""Delete a user"""
|
|
253
|
+
conn = self._get_conn()
|
|
254
|
+
try:
|
|
255
|
+
conn.execute("DELETE FROM users WHERE username = ?", (username,))
|
|
256
|
+
conn.commit()
|
|
257
|
+
finally:
|
|
258
|
+
conn.close()
|
|
259
|
+
|
|
260
|
+
# ─── Patterns ──────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def save_pattern(self, pattern: Dict):
|
|
263
|
+
"""Save a learned pattern"""
|
|
264
|
+
conn = self._get_conn()
|
|
265
|
+
try:
|
|
266
|
+
conn.execute(
|
|
267
|
+
"INSERT INTO patterns (pattern_type, frequency, context, related_elements, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
268
|
+
(
|
|
269
|
+
pattern["type"],
|
|
270
|
+
pattern["frequency"],
|
|
271
|
+
pattern.get("context", ""),
|
|
272
|
+
json.dumps(pattern.get("related_elements", [])),
|
|
273
|
+
pattern.get("timestamp", datetime.now().isoformat()),
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
conn.commit()
|
|
277
|
+
finally:
|
|
278
|
+
conn.close()
|
|
279
|
+
|
|
280
|
+
def count_patterns(self) -> int:
|
|
281
|
+
"""Count stored patterns"""
|
|
282
|
+
conn = self._get_conn()
|
|
283
|
+
try:
|
|
284
|
+
row = conn.execute("SELECT COUNT(*) FROM patterns").fetchone()
|
|
285
|
+
return row[0]
|
|
286
|
+
finally:
|
|
287
|
+
conn.close()
|
|
288
|
+
|
|
289
|
+
# ─── Elements ──────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def save_elements(self, target: str, elements: List[Dict]):
|
|
292
|
+
"""Save detected elements"""
|
|
293
|
+
conn = self._get_conn()
|
|
294
|
+
try:
|
|
295
|
+
for elem in elements:
|
|
296
|
+
conn.execute(
|
|
297
|
+
"INSERT INTO elements (target, element_json, timestamp) VALUES (?, ?, ?)",
|
|
298
|
+
(target, json.dumps(elem, default=str), datetime.now().isoformat())
|
|
299
|
+
)
|
|
300
|
+
conn.commit()
|
|
301
|
+
finally:
|
|
302
|
+
conn.close()
|
|
303
|
+
|
|
304
|
+
# ─── Metadata ──────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
def save_metadata(self, key: str, value: Any):
|
|
307
|
+
"""Save metadata"""
|
|
308
|
+
conn = self._get_conn()
|
|
309
|
+
try:
|
|
310
|
+
conn.execute(
|
|
311
|
+
"INSERT OR REPLACE INTO metadata (key, value, updated_at) VALUES (?, ?, ?)",
|
|
312
|
+
(key, json.dumps(value, default=str), datetime.now().isoformat())
|
|
313
|
+
)
|
|
314
|
+
conn.commit()
|
|
315
|
+
finally:
|
|
316
|
+
conn.close()
|
|
317
|
+
|
|
318
|
+
def get_metadata(self, key: str) -> Any:
|
|
319
|
+
"""Get metadata value"""
|
|
320
|
+
conn = self._get_conn()
|
|
321
|
+
try:
|
|
322
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone()
|
|
323
|
+
if row:
|
|
324
|
+
return json.loads(row[0])
|
|
325
|
+
finally:
|
|
326
|
+
conn.close()
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# ─── Optimized Tests ───────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
def count_optimized_tests(self) -> int:
|
|
332
|
+
"""Count optimized tests"""
|
|
333
|
+
conn = self._get_conn()
|
|
334
|
+
try:
|
|
335
|
+
row = conn.execute("SELECT COUNT(*) FROM optimized_tests").fetchone()
|
|
336
|
+
return row[0]
|
|
337
|
+
finally:
|
|
338
|
+
conn.close()
|
|
339
|
+
|
|
340
|
+
# ─── Export/Purge ──────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def export_all(self) -> Dict:
|
|
343
|
+
"""Export all data"""
|
|
344
|
+
conn = self._get_conn()
|
|
345
|
+
try:
|
|
346
|
+
data = {
|
|
347
|
+
"test_results": [dict(r) for r in conn.execute("SELECT * FROM test_results").fetchall()],
|
|
348
|
+
"users": [dict(r) for r in conn.execute("SELECT * FROM users").fetchall()],
|
|
349
|
+
"patterns": [dict(r) for r in conn.execute("SELECT * FROM patterns").fetchall()],
|
|
350
|
+
"elements": [dict(r) for r in conn.execute("SELECT * FROM elements").fetchall()],
|
|
351
|
+
"metadata": {r["key"]: json.loads(r["value"]) for r in conn.execute("SELECT * FROM metadata").fetchall()},
|
|
352
|
+
"exported_at": datetime.now().isoformat(),
|
|
353
|
+
}
|
|
354
|
+
return data
|
|
355
|
+
finally:
|
|
356
|
+
conn.close()
|
|
357
|
+
|
|
358
|
+
def purge_all(self):
|
|
359
|
+
"""Delete all data"""
|
|
360
|
+
conn = self._get_conn()
|
|
361
|
+
try:
|
|
362
|
+
tables = ["test_results", "users", "patterns", "elements", "metadata", "optimized_tests", "activity_log", "coverage_history"]
|
|
363
|
+
for table in tables:
|
|
364
|
+
conn.execute(f"DELETE FROM {table}")
|
|
365
|
+
conn.commit()
|
|
366
|
+
finally:
|
|
367
|
+
conn.close()
|
nextog/data/models.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Data models using Pydantic for validation"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestResult(BaseModel):
|
|
9
|
+
engine: str
|
|
10
|
+
total: int = 0
|
|
11
|
+
passed: int = 0
|
|
12
|
+
failed: int = 0
|
|
13
|
+
skipped: int = 0
|
|
14
|
+
coverage: float = 0.0
|
|
15
|
+
duration: float = 0.0
|
|
16
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
17
|
+
details: List[Dict[str, Any]] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestCase(BaseModel):
|
|
21
|
+
name: str
|
|
22
|
+
test_type: str
|
|
23
|
+
element: str = ""
|
|
24
|
+
description: str = ""
|
|
25
|
+
priority: str = "medium"
|
|
26
|
+
steps: List[Dict[str, Any]] = []
|
|
27
|
+
expected_result: str = ""
|
|
28
|
+
actual_result: str = ""
|
|
29
|
+
status: str = "pending"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class User(BaseModel):
|
|
33
|
+
username: str
|
|
34
|
+
role: str = "tester"
|
|
35
|
+
email: Optional[str] = None
|
|
36
|
+
status: str = "active"
|
|
37
|
+
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Pattern(BaseModel):
|
|
41
|
+
type: str
|
|
42
|
+
frequency: int = 0
|
|
43
|
+
context: str = ""
|
|
44
|
+
related_elements: List[str] = []
|
|
45
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Element(BaseModel):
|
|
49
|
+
name: str
|
|
50
|
+
type: str
|
|
51
|
+
selector: str = ""
|
|
52
|
+
description: str = ""
|
|
53
|
+
priority: str = "medium"
|
|
54
|
+
visible: bool = True
|
|
55
|
+
attributes: Dict[str, str] = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ProjectConfig(BaseModel):
|
|
59
|
+
project: str
|
|
60
|
+
version: str = "1.0"
|
|
61
|
+
engines: Dict[str, Any] = {}
|
|
62
|
+
coverage_target: int = 90
|
|
63
|
+
parallel: bool = True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CoverageData(BaseModel):
|
|
67
|
+
total_coverage: float = 0.0
|
|
68
|
+
phases: Dict[str, Any] = {}
|
|
69
|
+
elements_covered: int = 0
|
|
70
|
+
elements_total: int = 0
|
|
71
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
72
|
+
project: Optional[str] = None
|