kailash 0.9.15__py3-none-any.whl → 0.9.17__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.
- kailash/__init__.py +4 -3
- kailash/middleware/database/base_models.py +7 -1
- kailash/migration/__init__.py +30 -0
- kailash/migration/cli.py +340 -0
- kailash/migration/compatibility_checker.py +662 -0
- kailash/migration/configuration_validator.py +837 -0
- kailash/migration/documentation_generator.py +1828 -0
- kailash/migration/examples/__init__.py +5 -0
- kailash/migration/examples/complete_migration_example.py +692 -0
- kailash/migration/migration_assistant.py +715 -0
- kailash/migration/performance_comparator.py +760 -0
- kailash/migration/regression_detector.py +1141 -0
- kailash/migration/tests/__init__.py +6 -0
- kailash/migration/tests/test_compatibility_checker.py +403 -0
- kailash/migration/tests/test_integration.py +463 -0
- kailash/migration/tests/test_migration_assistant.py +397 -0
- kailash/migration/tests/test_performance_comparator.py +433 -0
- kailash/monitoring/__init__.py +29 -2
- kailash/monitoring/asyncsql_metrics.py +275 -0
- kailash/nodes/data/async_sql.py +1828 -33
- kailash/runtime/local.py +1255 -8
- kailash/runtime/monitoring/__init__.py +1 -0
- kailash/runtime/monitoring/runtime_monitor.py +780 -0
- kailash/runtime/resource_manager.py +3033 -0
- kailash/sdk_exceptions.py +21 -0
- kailash/workflow/cyclic_runner.py +18 -2
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/METADATA +1 -1
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/RECORD +33 -14
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/WHEEL +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/entry_points.txt +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/licenses/NOTICE +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,397 @@
|
|
1
|
+
"""Tests for the MigrationAssistant class."""
|
2
|
+
|
3
|
+
import tempfile
|
4
|
+
import textwrap
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import pytest
|
8
|
+
|
9
|
+
from kailash.migration.compatibility_checker import CompatibilityChecker
|
10
|
+
from kailash.migration.migration_assistant import (
|
11
|
+
MigrationAssistant,
|
12
|
+
MigrationPlan,
|
13
|
+
MigrationResult,
|
14
|
+
MigrationStep,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.fixture
|
19
|
+
def assistant():
|
20
|
+
"""Create a MigrationAssistant instance for testing."""
|
21
|
+
return MigrationAssistant(dry_run=True, create_backups=False)
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.fixture
|
25
|
+
def temp_project_dir():
|
26
|
+
"""Create a temporary project directory with test files."""
|
27
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
28
|
+
temp_path = Path(temp_dir)
|
29
|
+
|
30
|
+
# Create test files with migration issues
|
31
|
+
(temp_path / "legacy_runtime.py").write_text(
|
32
|
+
textwrap.dedent(
|
33
|
+
"""
|
34
|
+
from kailash.runtime.local import LocalRuntime
|
35
|
+
|
36
|
+
# Legacy configuration that needs migration
|
37
|
+
runtime = LocalRuntime(
|
38
|
+
enable_parallel=True,
|
39
|
+
thread_pool_size=8,
|
40
|
+
debug_mode=True,
|
41
|
+
memory_limit=2048
|
42
|
+
)
|
43
|
+
|
44
|
+
# Legacy method usage
|
45
|
+
workflow = build_workflow()
|
46
|
+
runtime.execute_sync(workflow)
|
47
|
+
results = runtime.get_results()
|
48
|
+
"""
|
49
|
+
).strip()
|
50
|
+
)
|
51
|
+
|
52
|
+
(temp_path / "config_file.py").write_text(
|
53
|
+
textwrap.dedent(
|
54
|
+
"""
|
55
|
+
# Configuration dictionary usage
|
56
|
+
RUNTIME_CONFIG = {
|
57
|
+
'timeout': 300,
|
58
|
+
'retry_count': 3,
|
59
|
+
'log_level': 'INFO'
|
60
|
+
}
|
61
|
+
|
62
|
+
from kailash.runtime.local import LocalRuntime
|
63
|
+
runtime = LocalRuntime(**RUNTIME_CONFIG)
|
64
|
+
"""
|
65
|
+
).strip()
|
66
|
+
)
|
67
|
+
|
68
|
+
(temp_path / "modern_usage.py").write_text(
|
69
|
+
textwrap.dedent(
|
70
|
+
"""
|
71
|
+
from kailash.runtime.local import LocalRuntime
|
72
|
+
|
73
|
+
# Already modern - should not need changes
|
74
|
+
runtime = LocalRuntime(
|
75
|
+
debug=True,
|
76
|
+
max_concurrency=10,
|
77
|
+
enable_monitoring=True
|
78
|
+
)
|
79
|
+
|
80
|
+
results, run_id = runtime.execute(workflow)
|
81
|
+
"""
|
82
|
+
).strip()
|
83
|
+
)
|
84
|
+
|
85
|
+
yield temp_path
|
86
|
+
|
87
|
+
|
88
|
+
class TestMigrationAssistant:
|
89
|
+
"""Test cases for MigrationAssistant."""
|
90
|
+
|
91
|
+
def test_initialization(self, assistant):
|
92
|
+
"""Test MigrationAssistant initialization."""
|
93
|
+
assert assistant is not None
|
94
|
+
assert assistant.dry_run is True
|
95
|
+
assert assistant.create_backups is False
|
96
|
+
assert isinstance(assistant.compatibility_checker, CompatibilityChecker)
|
97
|
+
assert isinstance(assistant.parameter_mappings, dict)
|
98
|
+
assert isinstance(assistant.value_transformations, dict)
|
99
|
+
assert isinstance(assistant.method_migrations, dict)
|
100
|
+
|
101
|
+
def test_create_migration_plan(self, assistant, temp_project_dir):
|
102
|
+
"""Test migration plan creation."""
|
103
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
104
|
+
|
105
|
+
assert isinstance(plan, MigrationPlan)
|
106
|
+
assert len(plan.steps) > 0
|
107
|
+
assert plan.estimated_duration_minutes > 0
|
108
|
+
assert plan.risk_level in ["low", "medium", "high"]
|
109
|
+
assert isinstance(plan.prerequisites, list)
|
110
|
+
assert isinstance(plan.post_migration_tests, list)
|
111
|
+
|
112
|
+
def test_migration_step_creation(self, assistant, temp_project_dir):
|
113
|
+
"""Test creation of individual migration steps."""
|
114
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
115
|
+
|
116
|
+
# Should have steps for files with issues
|
117
|
+
step_files = {step.file_path for step in plan.steps}
|
118
|
+
assert any("legacy_runtime.py" in path for path in step_files)
|
119
|
+
|
120
|
+
# Check step properties
|
121
|
+
for step in plan.steps:
|
122
|
+
assert isinstance(step, MigrationStep)
|
123
|
+
assert step.step_id
|
124
|
+
assert step.description
|
125
|
+
assert step.file_path
|
126
|
+
assert step.original_code
|
127
|
+
assert step.migrated_code
|
128
|
+
assert isinstance(step.automated, bool)
|
129
|
+
assert isinstance(step.validation_required, bool)
|
130
|
+
|
131
|
+
def test_parameter_transformation_parallel(self, assistant):
|
132
|
+
"""Test transformation of enable_parallel to max_concurrency."""
|
133
|
+
content = "runtime = LocalRuntime(enable_parallel=True)"
|
134
|
+
issue = type("Issue", (), {"code_snippet": content})()
|
135
|
+
|
136
|
+
result = assistant._transform_parallel_to_concurrency(content, issue)
|
137
|
+
|
138
|
+
assert "max_concurrency=10" in result
|
139
|
+
assert "enable_parallel=True" not in result
|
140
|
+
|
141
|
+
def test_parameter_transformation_thread_pool(self, assistant):
|
142
|
+
"""Test transformation of thread_pool_size to max_concurrency."""
|
143
|
+
content = "runtime = LocalRuntime(thread_pool_size=5)"
|
144
|
+
issue = type("Issue", (), {"code_snippet": content})()
|
145
|
+
|
146
|
+
result = assistant._transform_thread_pool_size(content, issue)
|
147
|
+
|
148
|
+
assert "max_concurrency=5" in result
|
149
|
+
assert "thread_pool_size=5" not in result
|
150
|
+
|
151
|
+
def test_parameter_transformation_memory_limit(self, assistant):
|
152
|
+
"""Test transformation of memory_limit to resource_limits."""
|
153
|
+
content = "runtime = LocalRuntime(memory_limit=1024)"
|
154
|
+
issue = type("Issue", (), {"code_snippet": content})()
|
155
|
+
|
156
|
+
result = assistant._transform_memory_limit(content, issue)
|
157
|
+
|
158
|
+
assert 'resource_limits={"memory_mb": 1024}' in result
|
159
|
+
assert "memory_limit=1024" not in result
|
160
|
+
|
161
|
+
def test_method_migration_execute_sync(self, assistant):
|
162
|
+
"""Test migration of execute_sync to execute."""
|
163
|
+
content = "results = runtime.execute_sync(workflow)"
|
164
|
+
issue = type("Issue", (), {"code_snippet": content})()
|
165
|
+
|
166
|
+
result = assistant._migrate_execute_sync(content, issue)
|
167
|
+
|
168
|
+
assert "runtime.execute(" in result
|
169
|
+
assert ".execute_sync(" not in result
|
170
|
+
|
171
|
+
def test_method_migration_get_results(self, assistant):
|
172
|
+
"""Test migration of get_results to direct result access."""
|
173
|
+
content = "results = runtime.get_results()"
|
174
|
+
issue = type("Issue", (), {"code_snippet": content})()
|
175
|
+
|
176
|
+
result = assistant._migrate_get_results(content, issue)
|
177
|
+
|
178
|
+
assert "[0]" in result
|
179
|
+
assert ".get_results()" not in result
|
180
|
+
|
181
|
+
def test_execute_migration_dry_run(self, assistant, temp_project_dir):
|
182
|
+
"""Test migration execution in dry run mode."""
|
183
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
184
|
+
result = assistant.execute_migration(plan)
|
185
|
+
|
186
|
+
assert isinstance(result, MigrationResult)
|
187
|
+
assert result.steps_completed > 0
|
188
|
+
assert result.steps_failed == 0
|
189
|
+
assert result.success is True
|
190
|
+
|
191
|
+
# In dry run, no actual files should be modified
|
192
|
+
original_content = (temp_project_dir / "legacy_runtime.py").read_text()
|
193
|
+
assert "enable_parallel=True" in original_content # Should not be changed
|
194
|
+
|
195
|
+
def test_execute_migration_real(self, temp_project_dir):
|
196
|
+
"""Test actual migration execution (not dry run)."""
|
197
|
+
# Use non-dry-run assistant
|
198
|
+
assistant = MigrationAssistant(dry_run=False, create_backups=False)
|
199
|
+
|
200
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
201
|
+
result = assistant.execute_migration(plan)
|
202
|
+
|
203
|
+
assert isinstance(result, MigrationResult)
|
204
|
+
assert result.success is True
|
205
|
+
|
206
|
+
# Check that files were actually modified
|
207
|
+
modified_content = (temp_project_dir / "legacy_runtime.py").read_text()
|
208
|
+
# Should have some migrations applied
|
209
|
+
assert modified_content != ""
|
210
|
+
|
211
|
+
def test_migration_plan_metadata(self, assistant, temp_project_dir):
|
212
|
+
"""Test migration plan metadata calculation."""
|
213
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
214
|
+
|
215
|
+
# Should have reasonable duration estimate
|
216
|
+
assert plan.estimated_duration_minutes > 0
|
217
|
+
assert plan.estimated_duration_minutes < 1000 # Not too crazy
|
218
|
+
|
219
|
+
# Should assess risk level
|
220
|
+
assert plan.risk_level in ["low", "medium", "high"]
|
221
|
+
|
222
|
+
# Should have prerequisites and tests
|
223
|
+
assert len(plan.prerequisites) > 0
|
224
|
+
assert len(plan.post_migration_tests) > 0
|
225
|
+
|
226
|
+
# Prerequisites should be meaningful
|
227
|
+
for prereq in plan.prerequisites:
|
228
|
+
assert isinstance(prereq, str)
|
229
|
+
assert len(prereq) > 5
|
230
|
+
|
231
|
+
def test_migration_report_generation(self, assistant, temp_project_dir):
|
232
|
+
"""Test migration report generation."""
|
233
|
+
plan = assistant.create_migration_plan(temp_project_dir)
|
234
|
+
result = assistant.execute_migration(plan)
|
235
|
+
|
236
|
+
report = assistant.generate_migration_report(plan, result)
|
237
|
+
|
238
|
+
assert isinstance(report, str)
|
239
|
+
assert len(report) > 0
|
240
|
+
assert "Migration Report" in report
|
241
|
+
assert "MIGRATION PLAN SUMMARY" in report
|
242
|
+
assert "EXECUTION RESULTS" in report
|
243
|
+
assert "MIGRATION STEPS" in report
|
244
|
+
|
245
|
+
def test_migration_validation(self, assistant):
|
246
|
+
"""Test migration step validation."""
|
247
|
+
# Create a valid migration step
|
248
|
+
step = MigrationStep(
|
249
|
+
step_id="test_1",
|
250
|
+
description="Test step",
|
251
|
+
file_path="/dev/null", # Won't exist, but that's OK for this test
|
252
|
+
original_code="x = 1",
|
253
|
+
migrated_code="x = 2",
|
254
|
+
)
|
255
|
+
|
256
|
+
# Should validate syntax without error
|
257
|
+
try:
|
258
|
+
assistant._validate_migration_step(step)
|
259
|
+
except ValueError:
|
260
|
+
pytest.fail("Valid Python code should not raise validation error")
|
261
|
+
|
262
|
+
def test_migration_validation_syntax_error(self, assistant):
|
263
|
+
"""Test migration step validation with syntax errors."""
|
264
|
+
# Create migration step with syntax error
|
265
|
+
step = MigrationStep(
|
266
|
+
step_id="test_1",
|
267
|
+
description="Test step",
|
268
|
+
file_path="/dev/null",
|
269
|
+
original_code="x = 1",
|
270
|
+
migrated_code="x = (", # Syntax error
|
271
|
+
)
|
272
|
+
|
273
|
+
# Should raise validation error
|
274
|
+
with pytest.raises(ValueError):
|
275
|
+
assistant._validate_migration_step(step)
|
276
|
+
|
277
|
+
def test_value_transformation_functions(self, assistant):
|
278
|
+
"""Test all value transformation functions."""
|
279
|
+
transformations = assistant.value_transformations
|
280
|
+
|
281
|
+
for param_name, transform_func in transformations.items():
|
282
|
+
# Create mock issue
|
283
|
+
issue = type("Issue", (), {"code_snippet": f"{param_name}=test"})()
|
284
|
+
content = f"runtime = LocalRuntime({param_name}=10)"
|
285
|
+
|
286
|
+
# Should not raise exception
|
287
|
+
result = transform_func(content, issue)
|
288
|
+
assert isinstance(result, str)
|
289
|
+
assert len(result) > 0
|
290
|
+
|
291
|
+
def test_method_migration_functions(self, assistant):
|
292
|
+
"""Test all method migration functions."""
|
293
|
+
migrations = assistant.method_migrations
|
294
|
+
|
295
|
+
for method_name, migrate_func in migrations.items():
|
296
|
+
# Create mock issue
|
297
|
+
issue = type("Issue", (), {"code_snippet": f".{method_name}("})()
|
298
|
+
content = f"result = runtime.{method_name}(workflow)"
|
299
|
+
|
300
|
+
# Should not raise exception
|
301
|
+
result = migrate_func(content, issue)
|
302
|
+
assert isinstance(result, str)
|
303
|
+
assert len(result) > 0
|
304
|
+
|
305
|
+
def test_rollback_functionality(self, assistant):
|
306
|
+
"""Test rollback functionality."""
|
307
|
+
# Create a mock result with backup path
|
308
|
+
with tempfile.TemporaryDirectory() as backup_dir:
|
309
|
+
result = MigrationResult(
|
310
|
+
success=True,
|
311
|
+
steps_completed=5,
|
312
|
+
steps_failed=0,
|
313
|
+
backup_path=backup_dir,
|
314
|
+
rollback_available=True,
|
315
|
+
)
|
316
|
+
|
317
|
+
# Test rollback (won't actually work without proper backup structure)
|
318
|
+
rollback_success = assistant.rollback_migration(result)
|
319
|
+
# Should return False since backup structure is empty
|
320
|
+
assert rollback_success is False
|
321
|
+
|
322
|
+
def test_include_exclude_patterns(self, assistant, temp_project_dir):
|
323
|
+
"""Test migration plan creation with include/exclude patterns."""
|
324
|
+
# Test with include patterns
|
325
|
+
plan_py_only = assistant.create_migration_plan(
|
326
|
+
temp_project_dir, include_patterns=["*.py"]
|
327
|
+
)
|
328
|
+
|
329
|
+
# Test with exclude patterns
|
330
|
+
plan_exclude_modern = assistant.create_migration_plan(
|
331
|
+
temp_project_dir, exclude_patterns=["modern_usage.py"]
|
332
|
+
)
|
333
|
+
|
334
|
+
# Both should create valid plans
|
335
|
+
assert isinstance(plan_py_only, MigrationPlan)
|
336
|
+
assert isinstance(plan_exclude_modern, MigrationPlan)
|
337
|
+
|
338
|
+
# Exclude pattern should result in fewer or equal steps
|
339
|
+
assert len(plan_exclude_modern.steps) <= len(plan_py_only.steps)
|
340
|
+
|
341
|
+
|
342
|
+
class TestMigrationStep:
|
343
|
+
"""Test cases for MigrationStep dataclass."""
|
344
|
+
|
345
|
+
def test_creation(self):
|
346
|
+
"""Test MigrationStep creation."""
|
347
|
+
step = MigrationStep(
|
348
|
+
step_id="test_1",
|
349
|
+
description="Test migration step",
|
350
|
+
file_path="/test/file.py",
|
351
|
+
original_code="old_code = True",
|
352
|
+
migrated_code="new_code = True",
|
353
|
+
)
|
354
|
+
|
355
|
+
assert step.step_id == "test_1"
|
356
|
+
assert step.description == "Test migration step"
|
357
|
+
assert step.file_path == "/test/file.py"
|
358
|
+
assert step.original_code == "old_code = True"
|
359
|
+
assert step.migrated_code == "new_code = True"
|
360
|
+
assert step.automated is True
|
361
|
+
assert step.validation_required is False
|
362
|
+
assert step.rollback_available is True
|
363
|
+
|
364
|
+
|
365
|
+
class TestMigrationPlan:
|
366
|
+
"""Test cases for MigrationPlan dataclass."""
|
367
|
+
|
368
|
+
def test_creation(self):
|
369
|
+
"""Test MigrationPlan creation."""
|
370
|
+
plan = MigrationPlan()
|
371
|
+
|
372
|
+
assert isinstance(plan.steps, list)
|
373
|
+
assert plan.estimated_duration_minutes == 0
|
374
|
+
assert plan.risk_level == "low"
|
375
|
+
assert isinstance(plan.prerequisites, list)
|
376
|
+
assert isinstance(plan.post_migration_tests, list)
|
377
|
+
assert plan.backup_required is True
|
378
|
+
|
379
|
+
|
380
|
+
class TestMigrationResult:
|
381
|
+
"""Test cases for MigrationResult dataclass."""
|
382
|
+
|
383
|
+
def test_creation(self):
|
384
|
+
"""Test MigrationResult creation."""
|
385
|
+
result = MigrationResult(success=True, steps_completed=5, steps_failed=0)
|
386
|
+
|
387
|
+
assert result.success is True
|
388
|
+
assert result.steps_completed == 5
|
389
|
+
assert result.steps_failed == 0
|
390
|
+
assert isinstance(result.errors, list)
|
391
|
+
assert isinstance(result.warnings, list)
|
392
|
+
assert result.backup_path is None
|
393
|
+
assert result.rollback_available is True
|
394
|
+
|
395
|
+
|
396
|
+
if __name__ == "__main__":
|
397
|
+
pytest.main([__file__])
|