attune-ai 2.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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Test Generation Report Formatter.
|
|
2
|
+
|
|
3
|
+
Format test generation output as human-readable reports.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_test_gen_report(result: dict, input_data: dict) -> str:
|
|
13
|
+
"""Format test generation output as a human-readable report.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
result: The review stage result
|
|
17
|
+
input_data: Input data from previous stages
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Formatted report string
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
lines = []
|
|
24
|
+
|
|
25
|
+
# Header
|
|
26
|
+
total_tests = result.get("total_tests", 0)
|
|
27
|
+
files_covered = result.get("files_covered", 0)
|
|
28
|
+
|
|
29
|
+
lines.append("=" * 60)
|
|
30
|
+
lines.append("TEST GAP ANALYSIS REPORT")
|
|
31
|
+
lines.append("=" * 60)
|
|
32
|
+
lines.append("")
|
|
33
|
+
|
|
34
|
+
# Summary stats
|
|
35
|
+
total_candidates = input_data.get("total_candidates", 0)
|
|
36
|
+
hotspot_count = input_data.get("hotspot_count", 0)
|
|
37
|
+
untested_count = input_data.get("untested_count", 0)
|
|
38
|
+
|
|
39
|
+
lines.append("-" * 60)
|
|
40
|
+
lines.append("SUMMARY")
|
|
41
|
+
lines.append("-" * 60)
|
|
42
|
+
lines.append(f"Tests Generated: {total_tests}")
|
|
43
|
+
lines.append(f"Files Covered: {files_covered}")
|
|
44
|
+
lines.append(f"Total Candidates: {total_candidates}")
|
|
45
|
+
lines.append(f"Bug Hotspots Found: {hotspot_count}")
|
|
46
|
+
lines.append(f"Untested Files: {untested_count}")
|
|
47
|
+
lines.append("")
|
|
48
|
+
|
|
49
|
+
# Status indicator
|
|
50
|
+
if total_tests == 0:
|
|
51
|
+
lines.append("⚠️ No tests were generated")
|
|
52
|
+
elif total_tests < 5:
|
|
53
|
+
lines.append(f"🟡 Generated {total_tests} test(s) - consider adding more coverage")
|
|
54
|
+
elif total_tests < 20:
|
|
55
|
+
lines.append(f"🟢 Generated {total_tests} tests - good coverage")
|
|
56
|
+
else:
|
|
57
|
+
lines.append(f"✅ Generated {total_tests} tests - excellent coverage")
|
|
58
|
+
lines.append("")
|
|
59
|
+
|
|
60
|
+
# Scope notice for enterprise clarity
|
|
61
|
+
total_source = input_data.get("total_source_files", 0)
|
|
62
|
+
existing_tests = input_data.get("existing_test_files", 0)
|
|
63
|
+
coverage_pct = input_data.get("analysis_coverage_percent", 100)
|
|
64
|
+
large_project = input_data.get("large_project_warning", False)
|
|
65
|
+
|
|
66
|
+
if total_source > 0 or existing_tests > 0:
|
|
67
|
+
lines.append("-" * 60)
|
|
68
|
+
lines.append("SCOPE NOTICE")
|
|
69
|
+
lines.append("-" * 60)
|
|
70
|
+
|
|
71
|
+
if large_project:
|
|
72
|
+
lines.append("⚠️ LARGE PROJECT: Only high-priority files analyzed")
|
|
73
|
+
lines.append(f" Coverage: {coverage_pct:.0f}% of candidate files")
|
|
74
|
+
lines.append("")
|
|
75
|
+
|
|
76
|
+
lines.append(f"Source Files Found: {total_source}")
|
|
77
|
+
lines.append(f"Existing Test Files: {existing_tests}")
|
|
78
|
+
lines.append(f"Files Analyzed: {files_covered}")
|
|
79
|
+
|
|
80
|
+
if existing_tests > 0:
|
|
81
|
+
lines.append("")
|
|
82
|
+
lines.append("Note: This report identifies gaps in untested files.")
|
|
83
|
+
lines.append("Run 'pytest --co -q' for full test suite statistics.")
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
# Parse XML review feedback if present
|
|
87
|
+
review = result.get("review_feedback", "")
|
|
88
|
+
xml_summary = ""
|
|
89
|
+
xml_findings = []
|
|
90
|
+
xml_tests = []
|
|
91
|
+
coverage_improvement = ""
|
|
92
|
+
|
|
93
|
+
if review and "<response>" in review:
|
|
94
|
+
# Extract summary
|
|
95
|
+
summary_match = re.search(r"<summary>(.*?)</summary>", review, re.DOTALL)
|
|
96
|
+
if summary_match:
|
|
97
|
+
xml_summary = summary_match.group(1).strip()
|
|
98
|
+
|
|
99
|
+
# Extract coverage improvement
|
|
100
|
+
coverage_match = re.search(
|
|
101
|
+
r"<coverage-improvement>(.*?)</coverage-improvement>",
|
|
102
|
+
review,
|
|
103
|
+
re.DOTALL,
|
|
104
|
+
)
|
|
105
|
+
if coverage_match:
|
|
106
|
+
coverage_improvement = coverage_match.group(1).strip()
|
|
107
|
+
|
|
108
|
+
# Extract findings
|
|
109
|
+
for finding_match in re.finditer(
|
|
110
|
+
r'<finding severity="(\w+)">(.*?)</finding>',
|
|
111
|
+
review,
|
|
112
|
+
re.DOTALL,
|
|
113
|
+
):
|
|
114
|
+
severity = finding_match.group(1)
|
|
115
|
+
finding_content = finding_match.group(2)
|
|
116
|
+
|
|
117
|
+
title_match = re.search(r"<title>(.*?)</title>", finding_content, re.DOTALL)
|
|
118
|
+
location_match = re.search(r"<location>(.*?)</location>", finding_content, re.DOTALL)
|
|
119
|
+
fix_match = re.search(r"<fix>(.*?)</fix>", finding_content, re.DOTALL)
|
|
120
|
+
|
|
121
|
+
xml_findings.append(
|
|
122
|
+
{
|
|
123
|
+
"severity": severity,
|
|
124
|
+
"title": title_match.group(1).strip() if title_match else "Unknown",
|
|
125
|
+
"location": location_match.group(1).strip() if location_match else "",
|
|
126
|
+
"fix": fix_match.group(1).strip() if fix_match else "",
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Extract suggested tests
|
|
131
|
+
for test_match in re.finditer(r'<test target="([^"]+)">(.*?)</test>', review, re.DOTALL):
|
|
132
|
+
target = test_match.group(1)
|
|
133
|
+
test_content = test_match.group(2)
|
|
134
|
+
|
|
135
|
+
type_match = re.search(r"<type>(.*?)</type>", test_content, re.DOTALL)
|
|
136
|
+
desc_match = re.search(r"<description>(.*?)</description>", test_content, re.DOTALL)
|
|
137
|
+
|
|
138
|
+
xml_tests.append(
|
|
139
|
+
{
|
|
140
|
+
"target": target,
|
|
141
|
+
"type": type_match.group(1).strip() if type_match else "unit",
|
|
142
|
+
"description": desc_match.group(1).strip() if desc_match else "",
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Show parsed summary
|
|
147
|
+
if xml_summary:
|
|
148
|
+
lines.append("-" * 60)
|
|
149
|
+
lines.append("QUALITY ASSESSMENT")
|
|
150
|
+
lines.append("-" * 60)
|
|
151
|
+
# Word wrap the summary
|
|
152
|
+
words = xml_summary.split()
|
|
153
|
+
current_line = ""
|
|
154
|
+
for word in words:
|
|
155
|
+
if len(current_line) + len(word) + 1 <= 58:
|
|
156
|
+
current_line += (" " if current_line else "") + word
|
|
157
|
+
else:
|
|
158
|
+
lines.append(current_line)
|
|
159
|
+
current_line = word
|
|
160
|
+
if current_line:
|
|
161
|
+
lines.append(current_line)
|
|
162
|
+
lines.append("")
|
|
163
|
+
|
|
164
|
+
if coverage_improvement:
|
|
165
|
+
lines.append(f"📈 {coverage_improvement}")
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
# Show findings by severity
|
|
169
|
+
if xml_findings:
|
|
170
|
+
lines.append("-" * 60)
|
|
171
|
+
lines.append("QUALITY FINDINGS")
|
|
172
|
+
lines.append("-" * 60)
|
|
173
|
+
|
|
174
|
+
severity_emoji = {"high": "🔴", "medium": "🟠", "low": "🟡", "info": "🔵"}
|
|
175
|
+
severity_order = {"high": 0, "medium": 1, "low": 2, "info": 3}
|
|
176
|
+
|
|
177
|
+
sorted_findings = sorted(xml_findings, key=lambda f: severity_order.get(f["severity"], 4))
|
|
178
|
+
|
|
179
|
+
for finding in sorted_findings:
|
|
180
|
+
emoji = severity_emoji.get(finding["severity"], "⚪")
|
|
181
|
+
lines.append(f"{emoji} [{finding['severity'].upper()}] {finding['title']}")
|
|
182
|
+
if finding["location"]:
|
|
183
|
+
lines.append(f" Location: {finding['location']}")
|
|
184
|
+
if finding["fix"]:
|
|
185
|
+
# Truncate long fix recommendations
|
|
186
|
+
fix_text = finding["fix"]
|
|
187
|
+
if len(fix_text) > 70:
|
|
188
|
+
fix_text = fix_text[:67] + "..."
|
|
189
|
+
lines.append(f" Fix: {fix_text}")
|
|
190
|
+
lines.append("")
|
|
191
|
+
|
|
192
|
+
# Show suggested tests
|
|
193
|
+
if xml_tests:
|
|
194
|
+
lines.append("-" * 60)
|
|
195
|
+
lines.append("SUGGESTED TESTS TO ADD")
|
|
196
|
+
lines.append("-" * 60)
|
|
197
|
+
|
|
198
|
+
for i, test in enumerate(xml_tests[:5], 1): # Limit to 5
|
|
199
|
+
lines.append(f"{i}. {test['target']} ({test['type']})")
|
|
200
|
+
if test["description"]:
|
|
201
|
+
desc = test["description"]
|
|
202
|
+
if len(desc) > 55:
|
|
203
|
+
desc = desc[:52] + "..."
|
|
204
|
+
lines.append(f" {desc}")
|
|
205
|
+
lines.append("")
|
|
206
|
+
|
|
207
|
+
if len(xml_tests) > 5:
|
|
208
|
+
lines.append(f" ... and {len(xml_tests) - 5} more suggested tests")
|
|
209
|
+
lines.append("")
|
|
210
|
+
|
|
211
|
+
# Generated tests breakdown (if no XML data)
|
|
212
|
+
generated_tests = input_data.get("generated_tests", [])
|
|
213
|
+
if generated_tests and not xml_findings:
|
|
214
|
+
lines.append("-" * 60)
|
|
215
|
+
lines.append("GENERATED TESTS BY FILE")
|
|
216
|
+
lines.append("-" * 60)
|
|
217
|
+
for test_file in generated_tests[:10]: # Limit display
|
|
218
|
+
source = test_file.get("source_file", "unknown")
|
|
219
|
+
test_count = test_file.get("test_count", 0)
|
|
220
|
+
# Shorten path for display
|
|
221
|
+
if len(source) > 50:
|
|
222
|
+
source = "..." + source[-47:]
|
|
223
|
+
lines.append(f" 📁 {source}")
|
|
224
|
+
lines.append(
|
|
225
|
+
f" └─ {test_count} test(s) → {test_file.get('test_file', 'test_*.py')}",
|
|
226
|
+
)
|
|
227
|
+
if len(generated_tests) > 10:
|
|
228
|
+
lines.append(f" ... and {len(generated_tests) - 10} more files")
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
# Written files section
|
|
232
|
+
written_files = input_data.get("written_files", [])
|
|
233
|
+
if written_files:
|
|
234
|
+
lines.append("-" * 60)
|
|
235
|
+
lines.append("TESTS WRITTEN TO DISK")
|
|
236
|
+
lines.append("-" * 60)
|
|
237
|
+
for file_path in written_files[:10]:
|
|
238
|
+
# Shorten path for display
|
|
239
|
+
if len(file_path) > 55:
|
|
240
|
+
file_path = "..." + file_path[-52:]
|
|
241
|
+
lines.append(f" ✅ {file_path}")
|
|
242
|
+
if len(written_files) > 10:
|
|
243
|
+
lines.append(f" ... and {len(written_files) - 10} more files")
|
|
244
|
+
lines.append("")
|
|
245
|
+
lines.append(" Run: pytest <file> to execute these tests")
|
|
246
|
+
lines.append("")
|
|
247
|
+
elif input_data.get("tests_written") is False and total_tests > 0:
|
|
248
|
+
lines.append("-" * 60)
|
|
249
|
+
lines.append("GENERATED TESTS (NOT WRITTEN)")
|
|
250
|
+
lines.append("-" * 60)
|
|
251
|
+
lines.append(" ⚠️ Tests were generated but not written to disk.")
|
|
252
|
+
lines.append(" To write tests, run with: write_tests=True")
|
|
253
|
+
lines.append("")
|
|
254
|
+
|
|
255
|
+
# Recommendations
|
|
256
|
+
lines.append("-" * 60)
|
|
257
|
+
lines.append("NEXT STEPS")
|
|
258
|
+
lines.append("-" * 60)
|
|
259
|
+
|
|
260
|
+
high_findings = sum(1 for f in xml_findings if f["severity"] == "high")
|
|
261
|
+
medium_findings = sum(1 for f in xml_findings if f["severity"] == "medium")
|
|
262
|
+
|
|
263
|
+
if high_findings > 0:
|
|
264
|
+
lines.append(f" 🔴 Address {high_findings} high-priority finding(s) first")
|
|
265
|
+
|
|
266
|
+
if medium_findings > 0:
|
|
267
|
+
lines.append(f" 🟠 Review {medium_findings} medium-priority finding(s)")
|
|
268
|
+
|
|
269
|
+
if xml_tests:
|
|
270
|
+
lines.append(f" 📝 Consider adding {len(xml_tests)} suggested test(s)")
|
|
271
|
+
|
|
272
|
+
if hotspot_count > 0:
|
|
273
|
+
lines.append(f" 🔥 {hotspot_count} bug hotspot file(s) need priority testing")
|
|
274
|
+
|
|
275
|
+
if untested_count > 0:
|
|
276
|
+
lines.append(f" 📁 {untested_count} file(s) have no existing tests")
|
|
277
|
+
|
|
278
|
+
if not any([high_findings, medium_findings, xml_tests, hotspot_count, untested_count]):
|
|
279
|
+
lines.append(" ✅ Test suite is in good shape!")
|
|
280
|
+
|
|
281
|
+
lines.append("")
|
|
282
|
+
|
|
283
|
+
# Footer
|
|
284
|
+
lines.append("=" * 60)
|
|
285
|
+
model_tier = result.get("model_tier_used", "unknown")
|
|
286
|
+
lines.append(f"Review completed using {model_tier} tier model")
|
|
287
|
+
lines.append("=" * 60)
|
|
288
|
+
|
|
289
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Test Template Generation.
|
|
2
|
+
|
|
3
|
+
Functions to generate pytest test code for functions and classes.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_test_for_function(module: str, func: dict) -> str:
|
|
12
|
+
"""Generate executable tests for a function based on AST analysis."""
|
|
13
|
+
name = func["name"]
|
|
14
|
+
params = func.get("params") or [] # List of (name, type, default) tuples, handle None
|
|
15
|
+
param_names = func.get("param_names") or [p[0] if isinstance(p, tuple) else p for p in params]
|
|
16
|
+
is_async = func.get("is_async", False)
|
|
17
|
+
return_type = func.get("return_type")
|
|
18
|
+
raises = func.get("raises") or []
|
|
19
|
+
has_side_effects = func.get("has_side_effects", False)
|
|
20
|
+
|
|
21
|
+
# Generate test values based on parameter types
|
|
22
|
+
test_cases = generate_test_cases_for_params(params)
|
|
23
|
+
param_str = ", ".join(test_cases.get("valid_args", [""] * len(params)))
|
|
24
|
+
|
|
25
|
+
# Build parametrized test if we have multiple test cases
|
|
26
|
+
parametrize_cases = test_cases.get("parametrize_cases", [])
|
|
27
|
+
|
|
28
|
+
tests = []
|
|
29
|
+
tests.append(f"import pytest\nfrom {module} import {name}\n")
|
|
30
|
+
|
|
31
|
+
# Generate parametrized test if we have cases
|
|
32
|
+
if parametrize_cases and len(parametrize_cases) > 1:
|
|
33
|
+
param_names_str = ", ".join(param_names) if param_names else "value"
|
|
34
|
+
cases_str = ",\n ".join(parametrize_cases)
|
|
35
|
+
|
|
36
|
+
if is_async:
|
|
37
|
+
tests.append(
|
|
38
|
+
f'''
|
|
39
|
+
@pytest.mark.parametrize("{param_names_str}", [
|
|
40
|
+
{cases_str},
|
|
41
|
+
])
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_{name}_with_various_inputs({param_names_str}):
|
|
44
|
+
"""Test {name} with various input combinations."""
|
|
45
|
+
result = await {name}({", ".join(param_names)})
|
|
46
|
+
assert result is not None
|
|
47
|
+
''',
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
tests.append(
|
|
51
|
+
f'''
|
|
52
|
+
@pytest.mark.parametrize("{param_names_str}", [
|
|
53
|
+
{cases_str},
|
|
54
|
+
])
|
|
55
|
+
def test_{name}_with_various_inputs({param_names_str}):
|
|
56
|
+
"""Test {name} with various input combinations."""
|
|
57
|
+
result = {name}({", ".join(param_names)})
|
|
58
|
+
assert result is not None
|
|
59
|
+
''',
|
|
60
|
+
)
|
|
61
|
+
# Simple valid input test
|
|
62
|
+
elif is_async:
|
|
63
|
+
tests.append(
|
|
64
|
+
f'''
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_{name}_returns_value():
|
|
67
|
+
"""Test that {name} returns a value with valid inputs."""
|
|
68
|
+
result = await {name}({param_str})
|
|
69
|
+
assert result is not None
|
|
70
|
+
''',
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
tests.append(
|
|
74
|
+
f'''
|
|
75
|
+
def test_{name}_returns_value():
|
|
76
|
+
"""Test that {name} returns a value with valid inputs."""
|
|
77
|
+
result = {name}({param_str})
|
|
78
|
+
assert result is not None
|
|
79
|
+
''',
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Generate edge case tests based on parameter types
|
|
83
|
+
edge_cases = test_cases.get("edge_cases", [])
|
|
84
|
+
if edge_cases:
|
|
85
|
+
edge_cases_str = ",\n ".join(edge_cases)
|
|
86
|
+
if is_async:
|
|
87
|
+
tests.append(
|
|
88
|
+
f'''
|
|
89
|
+
@pytest.mark.parametrize("edge_input", [
|
|
90
|
+
{edge_cases_str},
|
|
91
|
+
])
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_{name}_edge_cases(edge_input):
|
|
94
|
+
"""Test {name} with edge case inputs."""
|
|
95
|
+
try:
|
|
96
|
+
result = await {name}(edge_input)
|
|
97
|
+
# Function should either return a value or raise an expected error
|
|
98
|
+
assert result is not None or result == 0 or result == "" or result == []
|
|
99
|
+
except (ValueError, TypeError, KeyError) as e:
|
|
100
|
+
# Expected error for edge cases
|
|
101
|
+
assert str(e) # Error message should not be empty
|
|
102
|
+
''',
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
tests.append(
|
|
106
|
+
f'''
|
|
107
|
+
@pytest.mark.parametrize("edge_input", [
|
|
108
|
+
{edge_cases_str},
|
|
109
|
+
])
|
|
110
|
+
def test_{name}_edge_cases(edge_input):
|
|
111
|
+
"""Test {name} with edge case inputs."""
|
|
112
|
+
try:
|
|
113
|
+
result = {name}(edge_input)
|
|
114
|
+
# Function should either return a value or raise an expected error
|
|
115
|
+
assert result is not None or result == 0 or result == "" or result == []
|
|
116
|
+
except (ValueError, TypeError, KeyError) as e:
|
|
117
|
+
# Expected error for edge cases
|
|
118
|
+
assert str(e) # Error message should not be empty
|
|
119
|
+
''',
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Generate exception tests for each raised exception
|
|
123
|
+
for exc_type in raises[:3]: # Limit to 3 exception types
|
|
124
|
+
if is_async:
|
|
125
|
+
tests.append(
|
|
126
|
+
f'''
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_{name}_raises_{exc_type.lower()}():
|
|
129
|
+
"""Test that {name} raises {exc_type} for invalid inputs."""
|
|
130
|
+
with pytest.raises({exc_type}):
|
|
131
|
+
await {name}(None) # Adjust input to trigger {exc_type}
|
|
132
|
+
''',
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
tests.append(
|
|
136
|
+
f'''
|
|
137
|
+
def test_{name}_raises_{exc_type.lower()}():
|
|
138
|
+
"""Test that {name} raises {exc_type} for invalid inputs."""
|
|
139
|
+
with pytest.raises({exc_type}):
|
|
140
|
+
{name}(None) # Adjust input to trigger {exc_type}
|
|
141
|
+
''',
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Add return type assertion if we know the type
|
|
145
|
+
if return_type and return_type not in ("None", "Any"):
|
|
146
|
+
type_check = get_type_assertion(return_type)
|
|
147
|
+
if type_check and not has_side_effects:
|
|
148
|
+
if is_async:
|
|
149
|
+
tests.append(
|
|
150
|
+
f'''
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_{name}_returns_correct_type():
|
|
153
|
+
"""Test that {name} returns the expected type."""
|
|
154
|
+
result = await {name}({param_str})
|
|
155
|
+
{type_check}
|
|
156
|
+
''',
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
tests.append(
|
|
160
|
+
f'''
|
|
161
|
+
def test_{name}_returns_correct_type():
|
|
162
|
+
"""Test that {name} returns the expected type."""
|
|
163
|
+
result = {name}({param_str})
|
|
164
|
+
{type_check}
|
|
165
|
+
''',
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return "\n".join(tests)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def generate_test_cases_for_params(params: list) -> dict:
|
|
172
|
+
"""Generate test cases based on parameter types."""
|
|
173
|
+
valid_args = []
|
|
174
|
+
parametrize_cases = []
|
|
175
|
+
edge_cases = []
|
|
176
|
+
|
|
177
|
+
for param in params:
|
|
178
|
+
if isinstance(param, tuple) and len(param) >= 2:
|
|
179
|
+
_name, type_hint, default = param[0], param[1], param[2] if len(param) > 2 else None
|
|
180
|
+
else:
|
|
181
|
+
_name = param if isinstance(param, str) else str(param)
|
|
182
|
+
type_hint = "Any"
|
|
183
|
+
default = None
|
|
184
|
+
|
|
185
|
+
# Generate valid value based on type
|
|
186
|
+
if "str" in type_hint.lower():
|
|
187
|
+
valid_args.append('"test_value"')
|
|
188
|
+
parametrize_cases.extend(['"hello"', '"world"', '"test_string"'])
|
|
189
|
+
edge_cases.extend(['""', '" "', '"a" * 1000'])
|
|
190
|
+
elif "int" in type_hint.lower():
|
|
191
|
+
valid_args.append("42")
|
|
192
|
+
parametrize_cases.extend(["0", "1", "100", "-1"])
|
|
193
|
+
edge_cases.extend(["0", "-1", "2**31 - 1"])
|
|
194
|
+
elif "float" in type_hint.lower():
|
|
195
|
+
valid_args.append("3.14")
|
|
196
|
+
parametrize_cases.extend(["0.0", "1.0", "-1.5", "100.5"])
|
|
197
|
+
edge_cases.extend(["0.0", "-0.0", "float('inf')"])
|
|
198
|
+
elif "bool" in type_hint.lower():
|
|
199
|
+
valid_args.append("True")
|
|
200
|
+
parametrize_cases.extend(["True", "False"])
|
|
201
|
+
elif "list" in type_hint.lower():
|
|
202
|
+
valid_args.append("[1, 2, 3]")
|
|
203
|
+
parametrize_cases.extend(["[]", "[1]", "[1, 2, 3]"])
|
|
204
|
+
edge_cases.extend(["[]", "[None]"])
|
|
205
|
+
elif "dict" in type_hint.lower():
|
|
206
|
+
valid_args.append('{"key": "value"}')
|
|
207
|
+
parametrize_cases.extend(["{}", '{"a": 1}', '{"key": "value"}'])
|
|
208
|
+
edge_cases.extend(["{}"])
|
|
209
|
+
elif default is not None:
|
|
210
|
+
valid_args.append(str(default))
|
|
211
|
+
else:
|
|
212
|
+
valid_args.append("None")
|
|
213
|
+
edge_cases.append("None")
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"valid_args": valid_args,
|
|
217
|
+
"parametrize_cases": parametrize_cases[:5], # Limit cases
|
|
218
|
+
"edge_cases": list(dict.fromkeys(edge_cases))[
|
|
219
|
+
:5
|
|
220
|
+
], # Unique edge cases (preserves order)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_type_assertion(return_type: str) -> str | None:
|
|
225
|
+
"""Generate assertion for return type checking."""
|
|
226
|
+
type_map = {
|
|
227
|
+
"str": "assert isinstance(result, str)",
|
|
228
|
+
"int": "assert isinstance(result, int)",
|
|
229
|
+
"float": "assert isinstance(result, (int, float))",
|
|
230
|
+
"bool": "assert isinstance(result, bool)",
|
|
231
|
+
"list": "assert isinstance(result, list)",
|
|
232
|
+
"dict": "assert isinstance(result, dict)",
|
|
233
|
+
"tuple": "assert isinstance(result, tuple)",
|
|
234
|
+
}
|
|
235
|
+
for type_name, assertion in type_map.items():
|
|
236
|
+
if type_name in return_type.lower():
|
|
237
|
+
return assertion
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_param_test_values(type_hint: str) -> list[str]:
|
|
242
|
+
"""Get test values for a single parameter based on its type."""
|
|
243
|
+
type_hint_lower = type_hint.lower()
|
|
244
|
+
if "str" in type_hint_lower:
|
|
245
|
+
return ['"hello"', '"world"', '"test_string"']
|
|
246
|
+
if "int" in type_hint_lower:
|
|
247
|
+
return ["0", "1", "42", "-1"]
|
|
248
|
+
if "float" in type_hint_lower:
|
|
249
|
+
return ["0.0", "1.0", "3.14"]
|
|
250
|
+
if "bool" in type_hint_lower:
|
|
251
|
+
return ["True", "False"]
|
|
252
|
+
if "list" in type_hint_lower:
|
|
253
|
+
return ["[]", "[1, 2, 3]"]
|
|
254
|
+
if "dict" in type_hint_lower:
|
|
255
|
+
return ["{}", '{"key": "value"}']
|
|
256
|
+
return ['"test_value"']
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def generate_test_for_class(module: str, cls: dict) -> str:
|
|
260
|
+
"""Generate executable test class based on AST analysis."""
|
|
261
|
+
name = cls["name"]
|
|
262
|
+
init_params = cls.get("init_params", [])
|
|
263
|
+
methods = cls.get("methods", [])
|
|
264
|
+
required_params = cls.get("required_init_params", 0)
|
|
265
|
+
_docstring = cls.get("docstring", "") # Reserved for future use
|
|
266
|
+
|
|
267
|
+
# Generate constructor arguments - ensure we have values for ALL required params
|
|
268
|
+
init_args = generate_test_cases_for_params(init_params)
|
|
269
|
+
valid_args = init_args.get("valid_args", [])
|
|
270
|
+
|
|
271
|
+
# Ensure we have enough args for required params
|
|
272
|
+
while len(valid_args) < required_params:
|
|
273
|
+
valid_args.append('"test_value"')
|
|
274
|
+
|
|
275
|
+
init_arg_str = ", ".join(valid_args)
|
|
276
|
+
|
|
277
|
+
tests = []
|
|
278
|
+
tests.append(f"import pytest\nfrom {module} import {name}\n")
|
|
279
|
+
|
|
280
|
+
# Fixture for class instance
|
|
281
|
+
tests.append(
|
|
282
|
+
f'''
|
|
283
|
+
@pytest.fixture
|
|
284
|
+
def {name.lower()}_instance():
|
|
285
|
+
"""Create a {name} instance for testing."""
|
|
286
|
+
return {name}({init_arg_str})
|
|
287
|
+
''',
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Test initialization
|
|
291
|
+
tests.append(
|
|
292
|
+
f'''
|
|
293
|
+
class Test{name}:
|
|
294
|
+
"""Tests for {name} class."""
|
|
295
|
+
|
|
296
|
+
def test_initialization(self):
|
|
297
|
+
"""Test that {name} can be instantiated."""
|
|
298
|
+
instance = {name}({init_arg_str})
|
|
299
|
+
assert instance is not None
|
|
300
|
+
''',
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Only generate parametrized tests for single-param classes to avoid tuple mismatches
|
|
304
|
+
if len(init_params) == 1 and init_params[0][2] is None:
|
|
305
|
+
# Single required param - safe to parametrize
|
|
306
|
+
param_name = init_params[0][0]
|
|
307
|
+
param_type = init_params[0][1]
|
|
308
|
+
cases = get_param_test_values(param_type)
|
|
309
|
+
if len(cases) > 1:
|
|
310
|
+
cases_str = ",\n ".join(cases)
|
|
311
|
+
tests.append(
|
|
312
|
+
f'''
|
|
313
|
+
@pytest.mark.parametrize("{param_name}", [
|
|
314
|
+
{cases_str},
|
|
315
|
+
])
|
|
316
|
+
def test_initialization_with_various_args(self, {param_name}):
|
|
317
|
+
"""Test {name} initialization with various arguments."""
|
|
318
|
+
instance = {name}({param_name})
|
|
319
|
+
assert instance is not None
|
|
320
|
+
''',
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Generate tests for each public method
|
|
324
|
+
for method in methods[:5]: # Limit to 5 methods
|
|
325
|
+
method_name = method.get("name", "")
|
|
326
|
+
if method_name.startswith("_") and method_name != "__init__":
|
|
327
|
+
continue
|
|
328
|
+
if method_name == "__init__":
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
method_params = method.get("params", [])[1:] # Skip self
|
|
332
|
+
is_async = method.get("is_async", False)
|
|
333
|
+
raises = method.get("raises", [])
|
|
334
|
+
|
|
335
|
+
# Generate method call args
|
|
336
|
+
method_args = generate_test_cases_for_params(method_params)
|
|
337
|
+
method_arg_str = ", ".join(method_args.get("valid_args", []))
|
|
338
|
+
|
|
339
|
+
if is_async:
|
|
340
|
+
tests.append(
|
|
341
|
+
f'''
|
|
342
|
+
@pytest.mark.asyncio
|
|
343
|
+
async def test_{method_name}_returns_value(self, {name.lower()}_instance):
|
|
344
|
+
"""Test that {method_name} returns a value."""
|
|
345
|
+
result = await {name.lower()}_instance.{method_name}({method_arg_str})
|
|
346
|
+
assert result is not None or result == 0 or result == "" or result == []
|
|
347
|
+
''',
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
tests.append(
|
|
351
|
+
f'''
|
|
352
|
+
def test_{method_name}_returns_value(self, {name.lower()}_instance):
|
|
353
|
+
"""Test that {method_name} returns a value."""
|
|
354
|
+
result = {name.lower()}_instance.{method_name}({method_arg_str})
|
|
355
|
+
assert result is not None or result == 0 or result == "" or result == []
|
|
356
|
+
''',
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Add exception tests for methods that raise
|
|
360
|
+
for exc_type in raises[:2]:
|
|
361
|
+
if is_async:
|
|
362
|
+
tests.append(
|
|
363
|
+
f'''
|
|
364
|
+
@pytest.mark.asyncio
|
|
365
|
+
async def test_{method_name}_raises_{exc_type.lower()}(self, {name.lower()}_instance):
|
|
366
|
+
"""Test that {method_name} raises {exc_type} for invalid inputs."""
|
|
367
|
+
with pytest.raises({exc_type}):
|
|
368
|
+
await {name.lower()}_instance.{method_name}(None)
|
|
369
|
+
''',
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
tests.append(
|
|
373
|
+
f'''
|
|
374
|
+
def test_{method_name}_raises_{exc_type.lower()}(self, {name.lower()}_instance):
|
|
375
|
+
"""Test that {method_name} raises {exc_type} for invalid inputs."""
|
|
376
|
+
with pytest.raises({exc_type}):
|
|
377
|
+
{name.lower()}_instance.{method_name}(None)
|
|
378
|
+
''',
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return "\n".join(tests)
|