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/engine.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Test Engine - Orchestrates all testing operations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, List, Optional, Any
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestEngine:
|
|
16
|
+
"""Main testing engine that orchestrates all test types"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings, db, privacy):
|
|
19
|
+
self.settings = settings
|
|
20
|
+
self.db = db
|
|
21
|
+
self.privacy = privacy
|
|
22
|
+
self.results: Dict[str, Any] = {}
|
|
23
|
+
self.coverage = 0.0
|
|
24
|
+
self.start_time = None
|
|
25
|
+
|
|
26
|
+
def run_all_tests(self, config: Dict, coverage_target: int = 90) -> Dict:
|
|
27
|
+
"""Run all configured tests with coverage tracking"""
|
|
28
|
+
self.start_time = time.time()
|
|
29
|
+
self.results = {
|
|
30
|
+
"categories": {},
|
|
31
|
+
"total_coverage": 0,
|
|
32
|
+
"timestamp": datetime.now().isoformat(),
|
|
33
|
+
"duration": 0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Phase 1: Smoke Tests (0% - 20%)
|
|
37
|
+
self._run_phase("smoke", config, target=20)
|
|
38
|
+
|
|
39
|
+
# Phase 2: Functional Tests (20% - 40%)
|
|
40
|
+
self._run_phase("functional", config, target=40)
|
|
41
|
+
|
|
42
|
+
# Phase 3: Integration Tests (40% - 60%)
|
|
43
|
+
self._run_phase("integration", config, target=60)
|
|
44
|
+
|
|
45
|
+
# Phase 4: Performance & Security (60% - 80%)
|
|
46
|
+
self._run_phase("performance", config, target=80)
|
|
47
|
+
|
|
48
|
+
# Phase 5: Advanced & Regression (80% - 90%)
|
|
49
|
+
if coverage_target >= 80:
|
|
50
|
+
self._run_phase("advanced", config, target=coverage_target)
|
|
51
|
+
|
|
52
|
+
self.results["duration"] = time.time() - self.start_time
|
|
53
|
+
self._save_results()
|
|
54
|
+
|
|
55
|
+
return self.results
|
|
56
|
+
|
|
57
|
+
def _run_phase(self, phase: str, config: Dict, target: int):
|
|
58
|
+
"""Run a specific testing phase"""
|
|
59
|
+
phase_results = {
|
|
60
|
+
"total": 0,
|
|
61
|
+
"passed": 0,
|
|
62
|
+
"failed": 0,
|
|
63
|
+
"skipped": 0,
|
|
64
|
+
"coverage": 0,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Execute tests based on configured engines
|
|
68
|
+
engines = config.get("engines", ["web"])
|
|
69
|
+
|
|
70
|
+
for engine_name in engines:
|
|
71
|
+
engine = self._get_engine(engine_name)
|
|
72
|
+
if engine:
|
|
73
|
+
result = engine.execute_phase(phase)
|
|
74
|
+
phase_results["total"] += result["total"]
|
|
75
|
+
phase_results["passed"] += result["passed"]
|
|
76
|
+
phase_results["failed"] += result["failed"]
|
|
77
|
+
phase_results["skipped"] += result["skipped"]
|
|
78
|
+
|
|
79
|
+
# Calculate coverage
|
|
80
|
+
if phase_results["total"] > 0:
|
|
81
|
+
phase_results["coverage"] = round(
|
|
82
|
+
(phase_results["passed"] / phase_results["total"]) * 100, 2
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.results["categories"][phase] = phase_results
|
|
86
|
+
self.coverage = self._calculate_total_coverage()
|
|
87
|
+
self.results["total_coverage"] = self.coverage
|
|
88
|
+
|
|
89
|
+
# Check if target reached
|
|
90
|
+
if self.coverage >= target:
|
|
91
|
+
console.print(f"[green]✓ Phase '{phase}' complete - Coverage: {self.coverage}%[/green]")
|
|
92
|
+
|
|
93
|
+
def _get_engine(self, engine_name: str):
|
|
94
|
+
"""Get the appropriate test engine"""
|
|
95
|
+
from nextog.engines.web.browser import WebTestEngine
|
|
96
|
+
from nextog.engines.api.rest import APITestEngine
|
|
97
|
+
from nextog.engines.mobile.android import MobileTestEngine
|
|
98
|
+
from nextog.engines.embedded.hardware import EmbeddedTestEngine
|
|
99
|
+
|
|
100
|
+
engines = {
|
|
101
|
+
"web": WebTestEngine,
|
|
102
|
+
"api": APITestEngine,
|
|
103
|
+
"mobile": MobileTestEngine,
|
|
104
|
+
"embedded": EmbeddedTestEngine,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
engine_class = engines.get(engine_name)
|
|
108
|
+
if engine_class:
|
|
109
|
+
return engine_class(self.settings, self.db, self.privacy)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _calculate_total_coverage(self) -> float:
|
|
113
|
+
"""Calculate weighted total coverage"""
|
|
114
|
+
weights = {
|
|
115
|
+
"smoke": 0.10,
|
|
116
|
+
"functional": 0.25,
|
|
117
|
+
"integration": 0.25,
|
|
118
|
+
"performance": 0.20,
|
|
119
|
+
"advanced": 0.20,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
total = 0.0
|
|
123
|
+
for phase, data in self.results["categories"].items():
|
|
124
|
+
weight = weights.get(phase, 0.1)
|
|
125
|
+
total += data["coverage"] * weight
|
|
126
|
+
|
|
127
|
+
return round(min(total, 90.0), 2) # Cap at 90%
|
|
128
|
+
|
|
129
|
+
def _save_results(self):
|
|
130
|
+
"""Save results to local database (privacy-first)"""
|
|
131
|
+
self.db.save_test_results(self.results)
|
|
132
|
+
self.privacy.record_activity("test_run", self.results)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestSuite:
|
|
136
|
+
"""Represents a collection of test cases"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, name: str, suite_type: str):
|
|
139
|
+
self.name = name
|
|
140
|
+
self.suite_type = suite_type
|
|
141
|
+
self.test_cases: List[TestCase] = []
|
|
142
|
+
self.metadata: Dict[str, Any] = {}
|
|
143
|
+
|
|
144
|
+
def add_test(self, test_case: 'TestCase'):
|
|
145
|
+
self.test_cases.append(test_case)
|
|
146
|
+
|
|
147
|
+
def get_tests_by_priority(self, priority: str) -> List['TestCase']:
|
|
148
|
+
return [t for t in self.test_cases if t.priority == priority]
|
|
149
|
+
|
|
150
|
+
def get_coverage(self) -> float:
|
|
151
|
+
if not self.test_cases:
|
|
152
|
+
return 0.0
|
|
153
|
+
executed = sum(1 for t in self.test_cases if t.executed)
|
|
154
|
+
return round((executed / len(self.test_cases)) * 100, 2)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestCase:
|
|
158
|
+
"""Individual test case"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, name: str, test_type: str, element: str = ""):
|
|
161
|
+
self.name = name
|
|
162
|
+
self.test_type = test_type
|
|
163
|
+
self.element = element # The element being tested
|
|
164
|
+
self.description = ""
|
|
165
|
+
self.priority = "medium" # low, medium, high, critical
|
|
166
|
+
self.steps: List[Dict] = []
|
|
167
|
+
self.expected_result = ""
|
|
168
|
+
self.actual_result = ""
|
|
169
|
+
self.status = "pending" # pending, passed, failed, skipped
|
|
170
|
+
self.executed = False
|
|
171
|
+
self.duration = 0.0
|
|
172
|
+
self.screenshots: List[str] = []
|
|
173
|
+
self.metadata: Dict[str, Any] = {}
|
|
174
|
+
|
|
175
|
+
def execute(self) -> Dict:
|
|
176
|
+
"""Execute this test case"""
|
|
177
|
+
self.executed = True
|
|
178
|
+
start = time.time()
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Steps execution would be handled by specific engine
|
|
182
|
+
self.status = "passed"
|
|
183
|
+
self.actual_result = "All assertions passed"
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.status = "failed"
|
|
186
|
+
self.actual_result = str(e)
|
|
187
|
+
|
|
188
|
+
self.duration = time.time() - start
|
|
189
|
+
return {
|
|
190
|
+
"status": self.status,
|
|
191
|
+
"duration": self.duration,
|
|
192
|
+
"result": self.actual_result,
|
|
193
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permission Manager - Role-based access control for nextOG CLI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Role(Enum):
|
|
11
|
+
ADMIN = "admin"
|
|
12
|
+
TESTER = "tester"
|
|
13
|
+
VIEWER = "viewer"
|
|
14
|
+
CI = "ci"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Permission matrix: role -> list of allowed actions
|
|
18
|
+
PERMISSION_MATRIX = {
|
|
19
|
+
Role.ADMIN: [
|
|
20
|
+
"test.run", "test.create", "test.delete", "test.view",
|
|
21
|
+
"user.create", "user.delete", "user.edit", "user.view",
|
|
22
|
+
"config.read", "config.write",
|
|
23
|
+
"data.export", "data.purge", "data.view",
|
|
24
|
+
"live.start", "live.manage",
|
|
25
|
+
"train.start", "train.stop", "train.view",
|
|
26
|
+
"report.generate", "report.view",
|
|
27
|
+
],
|
|
28
|
+
Role.TESTER: [
|
|
29
|
+
"test.run", "test.create", "test.view",
|
|
30
|
+
"data.export", "data.view",
|
|
31
|
+
"live.start",
|
|
32
|
+
"train.view",
|
|
33
|
+
"report.generate", "report.view",
|
|
34
|
+
],
|
|
35
|
+
Role.VIEWER: [
|
|
36
|
+
"test.view",
|
|
37
|
+
"data.view",
|
|
38
|
+
"report.view",
|
|
39
|
+
],
|
|
40
|
+
Role.CI: [
|
|
41
|
+
"test.run", "test.view",
|
|
42
|
+
"report.generate", "report.view",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PermissionManager:
|
|
48
|
+
"""Manages users, roles, and permissions"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, db):
|
|
51
|
+
self.db = db
|
|
52
|
+
self._current_user = None
|
|
53
|
+
|
|
54
|
+
def create_user(self, username: str, role: str, email: Optional[str] = None) -> Dict:
|
|
55
|
+
"""Create a new user with specified role"""
|
|
56
|
+
# Validate role
|
|
57
|
+
try:
|
|
58
|
+
role_enum = Role(role.lower())
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise ValueError(f"Invalid role: {role}. Must be one of: {[r.value for r in Role]}")
|
|
61
|
+
|
|
62
|
+
user = {
|
|
63
|
+
"username": username,
|
|
64
|
+
"role": role_enum.value,
|
|
65
|
+
"email": email,
|
|
66
|
+
"status": "active",
|
|
67
|
+
"created_at": datetime.now().isoformat(),
|
|
68
|
+
"last_login": None,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
self.db.save_user(user)
|
|
72
|
+
return user
|
|
73
|
+
|
|
74
|
+
def authenticate(self, username: str) -> bool:
|
|
75
|
+
"""Authenticate user and set as current"""
|
|
76
|
+
user = self.db.get_user(username)
|
|
77
|
+
if user and user["status"] == "active":
|
|
78
|
+
self._current_user = user
|
|
79
|
+
self.db.update_last_login(username)
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def check_permission(self, action: str) -> bool:
|
|
84
|
+
"""Check if current user has permission for action"""
|
|
85
|
+
if not self._current_user:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
role = Role(self._current_user["role"])
|
|
89
|
+
allowed = PERMISSION_MATRIX.get(role, [])
|
|
90
|
+
return action in allowed
|
|
91
|
+
|
|
92
|
+
def require_permission(self, action: str):
|
|
93
|
+
"""Raise error if current user lacks permission"""
|
|
94
|
+
if not self.check_permission(action):
|
|
95
|
+
raise PermissionError(
|
|
96
|
+
f"User '{self._current_user.get('username', 'unknown')}' "
|
|
97
|
+
f"lacks permission for '{action}'"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def list_users(self) -> List[Dict]:
|
|
101
|
+
"""List all users"""
|
|
102
|
+
return self.db.get_all_users()
|
|
103
|
+
|
|
104
|
+
def update_role(self, username: str, new_role: str):
|
|
105
|
+
"""Update user's role"""
|
|
106
|
+
try:
|
|
107
|
+
Role(new_role.lower())
|
|
108
|
+
except ValueError:
|
|
109
|
+
raise ValueError(f"Invalid role: {new_role}")
|
|
110
|
+
|
|
111
|
+
self.db.update_user_role(username, new_role)
|
|
112
|
+
|
|
113
|
+
def delete_user(self, username: str):
|
|
114
|
+
"""Delete a user"""
|
|
115
|
+
self.require_permission("user.delete")
|
|
116
|
+
self.db.delete_user(username)
|
|
117
|
+
|
|
118
|
+
def get_user_permissions(self, username: str) -> List[str]:
|
|
119
|
+
"""Get all permissions for a user"""
|
|
120
|
+
user = self.db.get_user(username)
|
|
121
|
+
if not user:
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
role = Role(user["role"])
|
|
125
|
+
return PERMISSION_MATRIX.get(role, [])
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def current_user(self) -> Optional[Dict]:
|
|
129
|
+
return self._current_user
|
nextog/core/privacy.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Privacy Engine - Ensures all data stays local with encryption
|
|
3
|
+
Privacy-first architecture: NO external data transfer
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import hashlib
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from cryptography.fernet import Fernet
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PrivacyEngine:
|
|
16
|
+
"""Privacy-first data management - all data stays local"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, db):
|
|
19
|
+
self.db = db
|
|
20
|
+
self.data_dir = Path.home() / ".nextog" / "data"
|
|
21
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
self._encryption_key = self._load_or_create_key()
|
|
23
|
+
self._cipher = Fernet(self._encryption_key)
|
|
24
|
+
self._external_sync_enabled = False # NEVER enable external sync
|
|
25
|
+
|
|
26
|
+
def _load_or_create_key(self) -> bytes:
|
|
27
|
+
"""Load encryption key or create new one"""
|
|
28
|
+
key_path = self.data_dir / ".encryption_key"
|
|
29
|
+
|
|
30
|
+
if key_path.exists():
|
|
31
|
+
return key_path.read_bytes()
|
|
32
|
+
|
|
33
|
+
key = Fernet.generate_key()
|
|
34
|
+
key_path.write_bytes(key)
|
|
35
|
+
# Set restrictive permissions
|
|
36
|
+
os.chmod(key_path, 0o600)
|
|
37
|
+
return key
|
|
38
|
+
|
|
39
|
+
def encrypt_data(self, data: Any) -> bytes:
|
|
40
|
+
"""Encrypt data using Fernet (AES-128-CBC)"""
|
|
41
|
+
json_str = json.dumps(data, default=str)
|
|
42
|
+
return self._cipher.encrypt(json_str.encode())
|
|
43
|
+
|
|
44
|
+
def decrypt_data(self, encrypted: bytes) -> Any:
|
|
45
|
+
"""Decrypt data"""
|
|
46
|
+
decrypted = self._cipher.decrypt(encrypted)
|
|
47
|
+
return json.loads(decrypted.decode())
|
|
48
|
+
|
|
49
|
+
def record_activity(self, activity_type: str, data: Dict):
|
|
50
|
+
"""Record user activity locally (encrypted)"""
|
|
51
|
+
record = {
|
|
52
|
+
"type": activity_type,
|
|
53
|
+
"data": data,
|
|
54
|
+
"timestamp": datetime.now().isoformat(),
|
|
55
|
+
"hash": hashlib.sha256(json.dumps(data, default=str).encode()).hexdigest(),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
encrypted = self.encrypt_data(record)
|
|
59
|
+
activity_file = self.data_dir / "activity.log.enc"
|
|
60
|
+
|
|
61
|
+
with open(activity_file, "ab") as f:
|
|
62
|
+
# Write length-prefixed encrypted record
|
|
63
|
+
f.write(len(encrypted).to_bytes(4, "big"))
|
|
64
|
+
f.write(encrypted)
|
|
65
|
+
|
|
66
|
+
def export_data(self, output_path: str, encrypt: bool = True):
|
|
67
|
+
"""Export all local data for user backup"""
|
|
68
|
+
all_data = self.db.export_all()
|
|
69
|
+
|
|
70
|
+
if encrypt:
|
|
71
|
+
encrypted = self.encrypt_data(all_data)
|
|
72
|
+
Path(output_path).write_bytes(encrypted)
|
|
73
|
+
else:
|
|
74
|
+
Path(output_path).write_text(json.dumps(all_data, indent=2, default=str))
|
|
75
|
+
|
|
76
|
+
def purge_all_data(self):
|
|
77
|
+
"""Delete ALL local data permanently"""
|
|
78
|
+
# Purge database
|
|
79
|
+
self.db.purge_all()
|
|
80
|
+
|
|
81
|
+
# Purge data directory
|
|
82
|
+
import shutil
|
|
83
|
+
if self.data_dir.exists():
|
|
84
|
+
for item in self.data_dir.iterdir():
|
|
85
|
+
if item.name != ".encryption_key":
|
|
86
|
+
if item.is_file():
|
|
87
|
+
item.unlink()
|
|
88
|
+
else:
|
|
89
|
+
shutil.rmtree(item)
|
|
90
|
+
|
|
91
|
+
def get_storage_stats(self) -> Dict:
|
|
92
|
+
"""Get storage statistics"""
|
|
93
|
+
db_size = 0
|
|
94
|
+
db_path = self.data_dir / "nextog.db"
|
|
95
|
+
if db_path.exists():
|
|
96
|
+
db_size = db_path.stat().st_size
|
|
97
|
+
|
|
98
|
+
test_count = self.db.count_test_results()
|
|
99
|
+
pattern_count = self.db.count_patterns()
|
|
100
|
+
|
|
101
|
+
# Calculate directory size
|
|
102
|
+
total_size = sum(f.stat().st_size for f in self.data_dir.rglob("*") if f.is_file())
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"db_size": self._format_size(db_size),
|
|
106
|
+
"total_size": self._format_size(total_size),
|
|
107
|
+
"test_count": test_count,
|
|
108
|
+
"pattern_count": pattern_count,
|
|
109
|
+
"encrypted": True,
|
|
110
|
+
"external_sync": self._external_sync_enabled, # Always False
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def _format_size(self, size_bytes: int) -> str:
|
|
114
|
+
"""Format file size"""
|
|
115
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
116
|
+
if size_bytes < 1024:
|
|
117
|
+
return f"{size_bytes:.1f} {unit}"
|
|
118
|
+
size_bytes /= 1024
|
|
119
|
+
return f"{size_bytes:.1f} TB"
|
|
120
|
+
|
|
121
|
+
def get_data_hash(self) -> str:
|
|
122
|
+
"""Get hash of all data for integrity verification"""
|
|
123
|
+
all_data = self.db.export_all()
|
|
124
|
+
return hashlib.sha256(json.dumps(all_data, sort_keys=True, default=str).encode()).hexdigest()
|
|
125
|
+
|
|
126
|
+
def verify_integrity(self) -> bool:
|
|
127
|
+
"""Verify data integrity hasn't been tampered with"""
|
|
128
|
+
stored_hash = self.db.get_metadata("data_hash")
|
|
129
|
+
current_hash = self.get_data_hash()
|
|
130
|
+
return stored_hash == current_hash
|
nextog/core/reporter.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coverage Reporter - Track and display test coverage
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
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.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoverageReporter:
|
|
18
|
+
"""Track and report test coverage from 0% to 90%"""
|
|
19
|
+
|
|
20
|
+
COVERAGE_PHASES = [
|
|
21
|
+
{"name": "Smoke Tests", "range": (0, 20), "weight": 0.10, "description": "Basic element detection & smoke tests"},
|
|
22
|
+
{"name": "Functional Tests", "range": (20, 40), "weight": 0.25, "description": "Functional tests & API validation"},
|
|
23
|
+
{"name": "Integration Tests", "range": (40, 60), "weight": 0.25, "description": "Integration tests & edge cases"},
|
|
24
|
+
{"name": "Performance & Security", "range": (60, 80), "weight": 0.20, "description": "Performance & security tests"},
|
|
25
|
+
{"name": "Advanced & Regression", "range": (80, 90), "weight": 0.20, "description": "Advanced scenarios & regression suites"},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def __init__(self, db, settings):
|
|
29
|
+
self.db = db
|
|
30
|
+
self.settings = settings
|
|
31
|
+
|
|
32
|
+
def get_coverage(self, project: Optional[str] = None) -> Dict:
|
|
33
|
+
"""Get coverage data for a project"""
|
|
34
|
+
results = self.db.get_latest_results(project)
|
|
35
|
+
|
|
36
|
+
coverage_data = {
|
|
37
|
+
"total_coverage": 0.0,
|
|
38
|
+
"phases": {},
|
|
39
|
+
"elements_covered": 0,
|
|
40
|
+
"elements_total": 0,
|
|
41
|
+
"timestamp": datetime.now().isoformat(),
|
|
42
|
+
"project": project,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if results:
|
|
46
|
+
coverage_data["total_coverage"] = results.get("total_coverage", 0)
|
|
47
|
+
|
|
48
|
+
for phase_config in self.COVERAGE_PHASES:
|
|
49
|
+
phase_name = phase_config["name"].lower().split()[0]
|
|
50
|
+
phase_data = results.get("categories", {}).get(phase_name, {})
|
|
51
|
+
coverage_data["phases"][phase_config["name"]] = {
|
|
52
|
+
"coverage": phase_data.get("coverage", 0),
|
|
53
|
+
"target_range": f"{phase_config['range'][0]}-{phase_config['range'][1]}%",
|
|
54
|
+
"description": phase_config["description"],
|
|
55
|
+
"status": self._get_phase_status(phase_data.get("coverage", 0), phase_config["range"]),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return coverage_data
|
|
59
|
+
|
|
60
|
+
def display_coverage(self, data: Dict):
|
|
61
|
+
"""Display coverage in a rich table"""
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
# Overall coverage bar
|
|
65
|
+
total = data.get("total_coverage", 0)
|
|
66
|
+
color = "red" if total < 40 else "yellow" if total < 70 else "green"
|
|
67
|
+
|
|
68
|
+
console.print(Panel(
|
|
69
|
+
f"[bold {color}]{total}%[/bold {color}] Coverage",
|
|
70
|
+
title="📊 Total Coverage",
|
|
71
|
+
border_style=color,
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
# Phase breakdown
|
|
75
|
+
table = Table(title="Coverage by Phase")
|
|
76
|
+
table.add_column("Phase", style="cyan", width=25)
|
|
77
|
+
table.add_column("Coverage", style="white", width=10)
|
|
78
|
+
table.add_column("Target", style="dim", width=15)
|
|
79
|
+
table.add_column("Status", style="white", width=10)
|
|
80
|
+
table.add_column("Description", style="dim")
|
|
81
|
+
|
|
82
|
+
for phase_name, phase_data in data.get("phases", {}).items():
|
|
83
|
+
coverage = phase_data["coverage"]
|
|
84
|
+
status_emoji = "✅" if phase_data["status"] == "complete" else "🔄" if phase_data["status"] == "in_progress" else "⬜"
|
|
85
|
+
|
|
86
|
+
table.add_row(
|
|
87
|
+
phase_name,
|
|
88
|
+
f"{coverage}%",
|
|
89
|
+
phase_data["target_range"],
|
|
90
|
+
f"{status_emoji} {phase_data['status']}",
|
|
91
|
+
phase_data["description"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
console.print(table)
|
|
95
|
+
|
|
96
|
+
# Progress bar visualization
|
|
97
|
+
console.print()
|
|
98
|
+
with Progress(
|
|
99
|
+
TextColumn("[bold blue]Overall Progress"),
|
|
100
|
+
BarColumn(bar_width=50),
|
|
101
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
102
|
+
console=console,
|
|
103
|
+
) as progress:
|
|
104
|
+
task = progress.add_task("", total=90, completed=total)
|
|
105
|
+
|
|
106
|
+
def generate_report(self, data: Dict, format: str = "html") -> str:
|
|
107
|
+
"""Generate a coverage report"""
|
|
108
|
+
report_dir = Path(".nextog/reports")
|
|
109
|
+
report_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
112
|
+
|
|
113
|
+
if format == "json":
|
|
114
|
+
output_path = report_dir / f"coverage_{timestamp}.json"
|
|
115
|
+
output_path.write_text(json.dumps(data, indent=2, default=str))
|
|
116
|
+
|
|
117
|
+
elif format == "html":
|
|
118
|
+
output_path = report_dir / f"coverage_{timestamp}.html"
|
|
119
|
+
html = self._generate_html_report(data)
|
|
120
|
+
output_path.write_text(html)
|
|
121
|
+
|
|
122
|
+
elif format == "pdf":
|
|
123
|
+
output_path = report_dir / f"coverage_{timestamp}.pdf"
|
|
124
|
+
# PDF generation would use weasyprint or similar
|
|
125
|
+
html = self._generate_html_report(data)
|
|
126
|
+
output_path.write_text(html) # Fallback to HTML
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
output_path = report_dir / f"coverage_{timestamp}.txt"
|
|
130
|
+
output_path.write_text(self._generate_text_report(data))
|
|
131
|
+
|
|
132
|
+
return str(output_path)
|
|
133
|
+
|
|
134
|
+
def _get_phase_status(self, coverage: float, target_range: tuple) -> str:
|
|
135
|
+
"""Determine phase status"""
|
|
136
|
+
if coverage >= target_range[1]:
|
|
137
|
+
return "complete"
|
|
138
|
+
elif coverage >= target_range[0]:
|
|
139
|
+
return "in_progress"
|
|
140
|
+
return "pending"
|
|
141
|
+
|
|
142
|
+
def _generate_html_report(self, data: Dict) -> str:
|
|
143
|
+
"""Generate HTML report"""
|
|
144
|
+
total = data.get("total_coverage", 0)
|
|
145
|
+
phases_html = ""
|
|
146
|
+
|
|
147
|
+
for phase_name, phase_data in data.get("phases", {}).items():
|
|
148
|
+
coverage = phase_data["coverage"]
|
|
149
|
+
color = "#22c55e" if coverage >= 60 else "#eab308" if coverage >= 30 else "#ef4444"
|
|
150
|
+
phases_html += f"""
|
|
151
|
+
<div style="margin: 10px 0; padding: 15px; background: #1e293b; border-radius: 8px;">
|
|
152
|
+
<div style="display: flex; justify-content: space-between;">
|
|
153
|
+
<strong>{phase_name}</strong>
|
|
154
|
+
<span>{coverage}% / {phase_data['target_range']}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div style="background: #334155; border-radius: 4px; margin-top: 8px;">
|
|
157
|
+
<div style="background: {color}; height: 8px; border-radius: 4px; width: {coverage}%"></div>
|
|
158
|
+
</div>
|
|
159
|
+
<p style="color: #94a3b8; font-size: 0.85rem; margin-top: 5px;">{phase_data['description']}</p>
|
|
160
|
+
</div>
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
return f"""<!DOCTYPE html>
|
|
164
|
+
<html>
|
|
165
|
+
<head>
|
|
166
|
+
<title>nextOG Coverage Report</title>
|
|
167
|
+
<style>
|
|
168
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px; }}
|
|
169
|
+
.container {{ max-width: 800px; margin: 0 auto; }}
|
|
170
|
+
h1 {{ color: #38bdf8; }}
|
|
171
|
+
.summary {{ font-size: 2rem; text-align: center; padding: 30px; }}
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
<div class="container">
|
|
176
|
+
<h1>🚀 nextOG Coverage Report</h1>
|
|
177
|
+
<p>Generated: {data.get('timestamp', 'N/A')}</p>
|
|
178
|
+
<div class="summary">
|
|
179
|
+
<h2>Total Coverage: {total}%</h2>
|
|
180
|
+
</div>
|
|
181
|
+
<h3>Phase Breakdown</h3>
|
|
182
|
+
{phases_html}
|
|
183
|
+
</div>
|
|
184
|
+
</body>
|
|
185
|
+
</html>"""
|
|
186
|
+
|
|
187
|
+
def _generate_text_report(self, data: Dict) -> str:
|
|
188
|
+
"""Generate plain text report"""
|
|
189
|
+
lines = [
|
|
190
|
+
"=" * 60,
|
|
191
|
+
"nextOG Coverage Report",
|
|
192
|
+
"=" * 60,
|
|
193
|
+
f"Generated: {data.get('timestamp', 'N/A')}",
|
|
194
|
+
f"Total Coverage: {data.get('total_coverage', 0)}%",
|
|
195
|
+
"",
|
|
196
|
+
"Phase Breakdown:",
|
|
197
|
+
"-" * 40,
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
for phase_name, phase_data in data.get("phases", {}).items():
|
|
201
|
+
lines.append(f" {phase_name}: {phase_data['coverage']}% ({phase_data['target_range']})")
|
|
202
|
+
|
|
203
|
+
lines.append("=" * 60)
|
|
204
|
+
return "\n".join(lines)
|