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,622 @@
|
|
|
1
|
+
"""Test Execution and Coverage Tracking Utilities for Tier 1 Automation.
|
|
2
|
+
|
|
3
|
+
Provides explicit opt-in utilities for tracking test executions and coverage metrics.
|
|
4
|
+
Use these functions when you want to track test/coverage data for Tier 1 monitoring.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart-AI-Memory
|
|
7
|
+
Licensed under Fair Source License 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import shlex
|
|
12
|
+
import subprocess
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import defusedxml.ElementTree as ET
|
|
20
|
+
except ImportError:
|
|
21
|
+
import xml.etree.ElementTree as ET # noqa: S405
|
|
22
|
+
|
|
23
|
+
# Import Element for type hints only (defusedxml doesn't expose it)
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from xml.etree.ElementTree import Element
|
|
26
|
+
|
|
27
|
+
from attune.models import (
|
|
28
|
+
CoverageRecord,
|
|
29
|
+
FileTestRecord,
|
|
30
|
+
TestExecutionRecord,
|
|
31
|
+
get_telemetry_store,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_tests_with_tracking(
|
|
38
|
+
test_suite: str = "unit",
|
|
39
|
+
test_files: list[str] | None = None,
|
|
40
|
+
command: str | None = None,
|
|
41
|
+
workflow_id: str | None = None,
|
|
42
|
+
triggered_by: str = "manual",
|
|
43
|
+
) -> TestExecutionRecord:
|
|
44
|
+
"""Run tests with explicit tracking (opt-in for Tier 1 monitoring).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
test_suite: Test suite name (unit, integration, e2e, all)
|
|
48
|
+
test_files: Specific test files to run (optional)
|
|
49
|
+
command: Custom test command (defaults to pytest)
|
|
50
|
+
workflow_id: Optional workflow ID to link this execution
|
|
51
|
+
triggered_by: Who/what triggered this (manual, workflow, ci, pre_commit)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
TestExecutionRecord with execution results
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> from attune.workflows.test_runner import run_tests_with_tracking
|
|
58
|
+
>>> result = run_tests_with_tracking(
|
|
59
|
+
... test_suite="unit",
|
|
60
|
+
... test_files=["tests/unit/test_config.py"],
|
|
61
|
+
... )
|
|
62
|
+
>>> print(f"Tests passed: {result.success}")
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
execution_id = f"test-{uuid.uuid4()}"
|
|
66
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
67
|
+
started_at = datetime.utcnow()
|
|
68
|
+
|
|
69
|
+
# Build command
|
|
70
|
+
if command is None:
|
|
71
|
+
if test_files:
|
|
72
|
+
files_str = " ".join(test_files)
|
|
73
|
+
command = f"pytest {files_str} -v --tb=short"
|
|
74
|
+
else:
|
|
75
|
+
if test_suite == "all":
|
|
76
|
+
command = "pytest tests/ -v --tb=short"
|
|
77
|
+
else:
|
|
78
|
+
command = f"pytest tests/{test_suite}/ -v --tb=short"
|
|
79
|
+
|
|
80
|
+
# Determine working directory
|
|
81
|
+
working_directory = str(Path.cwd())
|
|
82
|
+
|
|
83
|
+
# Run tests
|
|
84
|
+
logger.info(f"Running tests: {command}")
|
|
85
|
+
try:
|
|
86
|
+
# Use shlex.split to safely parse command without shell=True
|
|
87
|
+
cmd_args = shlex.split(command)
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
cmd_args,
|
|
90
|
+
shell=False,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
timeout=600, # 10 minute timeout
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Parse pytest output for test counts
|
|
97
|
+
output = result.stdout + result.stderr
|
|
98
|
+
total_tests, passed, failed, skipped, errors = _parse_pytest_output(output)
|
|
99
|
+
|
|
100
|
+
success = result.returncode == 0
|
|
101
|
+
exit_code = result.returncode
|
|
102
|
+
|
|
103
|
+
# Parse failures from output
|
|
104
|
+
failed_tests = _parse_pytest_failures(output) if failed > 0 or errors > 0 else []
|
|
105
|
+
|
|
106
|
+
except subprocess.TimeoutExpired:
|
|
107
|
+
logger.error("Test execution timed out after 600 seconds")
|
|
108
|
+
total_tests, passed, failed, skipped, errors = 0, 0, 0, 0, 1
|
|
109
|
+
success = False
|
|
110
|
+
exit_code = 124 # Timeout exit code
|
|
111
|
+
failed_tests = [{"name": "timeout", "file": "unknown", "error": "Test execution timed out"}]
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Test execution failed: {e}")
|
|
115
|
+
total_tests, passed, failed, skipped, errors = 0, 0, 0, 0, 1
|
|
116
|
+
success = False
|
|
117
|
+
exit_code = 1
|
|
118
|
+
failed_tests = [{"name": "execution_error", "file": "unknown", "error": str(e)}]
|
|
119
|
+
|
|
120
|
+
# Calculate duration
|
|
121
|
+
completed_at = datetime.utcnow()
|
|
122
|
+
duration_seconds = (completed_at - started_at).total_seconds()
|
|
123
|
+
|
|
124
|
+
# Create test execution record
|
|
125
|
+
record = TestExecutionRecord(
|
|
126
|
+
execution_id=execution_id,
|
|
127
|
+
timestamp=timestamp,
|
|
128
|
+
test_suite=test_suite,
|
|
129
|
+
test_files=test_files or [],
|
|
130
|
+
triggered_by=triggered_by,
|
|
131
|
+
command=command,
|
|
132
|
+
working_directory=working_directory,
|
|
133
|
+
duration_seconds=duration_seconds,
|
|
134
|
+
total_tests=total_tests,
|
|
135
|
+
passed=passed,
|
|
136
|
+
failed=failed,
|
|
137
|
+
skipped=skipped,
|
|
138
|
+
errors=errors,
|
|
139
|
+
success=success,
|
|
140
|
+
exit_code=exit_code,
|
|
141
|
+
failed_tests=failed_tests,
|
|
142
|
+
workflow_id=workflow_id,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Log to telemetry store
|
|
146
|
+
try:
|
|
147
|
+
store = get_telemetry_store()
|
|
148
|
+
store.log_test_execution(record)
|
|
149
|
+
logger.info(f"Test execution tracked: {execution_id}")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Failed to log test execution: {e}")
|
|
152
|
+
|
|
153
|
+
return record
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def track_coverage(
|
|
157
|
+
coverage_file: str = "coverage.xml",
|
|
158
|
+
workflow_id: str | None = None,
|
|
159
|
+
) -> CoverageRecord:
|
|
160
|
+
"""Track test coverage from coverage.xml file (opt-in for Tier 1 monitoring).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
coverage_file: Path to coverage.xml file
|
|
164
|
+
workflow_id: Optional workflow ID to link this record
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
CoverageRecord with coverage metrics
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> from attune.workflows.test_runner import track_coverage
|
|
171
|
+
>>> coverage = track_coverage("coverage.xml")
|
|
172
|
+
>>> print(f"Coverage: {coverage.overall_percentage:.1f}%")
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
record_id = f"cov-{uuid.uuid4()}"
|
|
176
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
177
|
+
|
|
178
|
+
coverage_path = Path(coverage_file)
|
|
179
|
+
if not coverage_path.exists():
|
|
180
|
+
raise FileNotFoundError(f"Coverage file not found: {coverage_file}")
|
|
181
|
+
|
|
182
|
+
# Parse coverage.xml
|
|
183
|
+
try:
|
|
184
|
+
# Uses defusedxml when available (see imports), coverage.xml is from trusted pytest/coverage tools
|
|
185
|
+
tree = ET.parse(coverage_path) # nosec B314
|
|
186
|
+
root = tree.getroot()
|
|
187
|
+
|
|
188
|
+
# Get overall metrics
|
|
189
|
+
lines_total = int(root.attrib.get("lines-valid", 0))
|
|
190
|
+
lines_covered = int(root.attrib.get("lines-covered", 0))
|
|
191
|
+
branches_total = int(root.attrib.get("branches-valid", 0))
|
|
192
|
+
branches_covered = int(root.attrib.get("branches-covered", 0))
|
|
193
|
+
|
|
194
|
+
if lines_total > 0:
|
|
195
|
+
overall_percentage = (lines_covered / lines_total) * 100
|
|
196
|
+
else:
|
|
197
|
+
overall_percentage = 0.0
|
|
198
|
+
|
|
199
|
+
# Get previous coverage if available
|
|
200
|
+
previous_percentage = _get_previous_coverage()
|
|
201
|
+
|
|
202
|
+
# Determine trend
|
|
203
|
+
if previous_percentage is not None:
|
|
204
|
+
change = overall_percentage - previous_percentage
|
|
205
|
+
if change > 1.0:
|
|
206
|
+
trend = "improving"
|
|
207
|
+
elif change < -1.0:
|
|
208
|
+
trend = "declining"
|
|
209
|
+
else:
|
|
210
|
+
trend = "stable"
|
|
211
|
+
else:
|
|
212
|
+
trend = "stable"
|
|
213
|
+
|
|
214
|
+
# Analyze files
|
|
215
|
+
files_analyzed = _analyze_coverage_files(root)
|
|
216
|
+
|
|
217
|
+
record = CoverageRecord(
|
|
218
|
+
record_id=record_id,
|
|
219
|
+
timestamp=timestamp,
|
|
220
|
+
overall_percentage=overall_percentage,
|
|
221
|
+
lines_total=lines_total,
|
|
222
|
+
lines_covered=lines_covered,
|
|
223
|
+
branches_total=branches_total,
|
|
224
|
+
branches_covered=branches_covered,
|
|
225
|
+
files_total=files_analyzed["total"],
|
|
226
|
+
files_well_covered=files_analyzed["well_covered"],
|
|
227
|
+
files_critical=files_analyzed["critical"],
|
|
228
|
+
untested_files=files_analyzed["untested"],
|
|
229
|
+
critical_gaps=files_analyzed["gaps"],
|
|
230
|
+
previous_percentage=previous_percentage,
|
|
231
|
+
trend=trend,
|
|
232
|
+
coverage_format="xml",
|
|
233
|
+
coverage_file=str(coverage_path),
|
|
234
|
+
workflow_id=workflow_id,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Log to telemetry store
|
|
238
|
+
try:
|
|
239
|
+
store = get_telemetry_store()
|
|
240
|
+
store.log_coverage(record)
|
|
241
|
+
logger.info(f"Coverage tracked: {record_id} ({overall_percentage:.1f}%)")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.warning(f"Failed to log coverage: {e}")
|
|
244
|
+
|
|
245
|
+
return record
|
|
246
|
+
|
|
247
|
+
except ET.ParseError as e:
|
|
248
|
+
raise ValueError(f"Invalid coverage.xml format: {e}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Helper functions
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parse_pytest_output(output: str) -> tuple[int, int, int, int, int]:
|
|
255
|
+
"""Parse pytest output for test counts.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Tuple of (total_tests, passed, failed, skipped, errors)
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
import re
|
|
262
|
+
|
|
263
|
+
# Look for pytest summary line like "5 passed, 2 failed, 1 skipped in 1.23s"
|
|
264
|
+
match = re.search(r"(\d+)\s+passed", output)
|
|
265
|
+
passed = int(match.group(1)) if match else 0
|
|
266
|
+
|
|
267
|
+
match = re.search(r"(\d+)\s+failed", output)
|
|
268
|
+
failed = int(match.group(1)) if match else 0
|
|
269
|
+
|
|
270
|
+
match = re.search(r"(\d+)\s+skipped", output)
|
|
271
|
+
skipped = int(match.group(1)) if match else 0
|
|
272
|
+
|
|
273
|
+
match = re.search(r"(\d+)\s+error", output)
|
|
274
|
+
errors = int(match.group(1)) if match else 0
|
|
275
|
+
|
|
276
|
+
total_tests = passed + failed + skipped + errors
|
|
277
|
+
|
|
278
|
+
return total_tests, passed, failed, skipped, errors
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_pytest_failures(output: str) -> list[dict[str, str]]:
|
|
282
|
+
"""Parse pytest output for failure details.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of dicts with name, file, error, traceback
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
failures = []
|
|
289
|
+
lines = output.split("\n")
|
|
290
|
+
|
|
291
|
+
# Simple parser - looks for FAILED lines
|
|
292
|
+
for line in lines:
|
|
293
|
+
if "FAILED " in line:
|
|
294
|
+
parts = line.split("::")
|
|
295
|
+
if len(parts) >= 2:
|
|
296
|
+
file_path = parts[0].replace("FAILED ", "").strip()
|
|
297
|
+
test_name = parts[1].split()[0] if len(parts) > 1 else "unknown"
|
|
298
|
+
|
|
299
|
+
failures.append({"name": test_name, "file": file_path, "error": "Test failed"})
|
|
300
|
+
|
|
301
|
+
return failures[:10] # Limit to 10 failures
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _get_previous_coverage() -> float | None:
|
|
305
|
+
"""Get previous coverage percentage from telemetry store.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Previous coverage percentage or None
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
store = get_telemetry_store()
|
|
313
|
+
records = store.get_coverage_history(limit=2)
|
|
314
|
+
|
|
315
|
+
if len(records) >= 2:
|
|
316
|
+
# Second-to-last record is the previous one
|
|
317
|
+
return records[-2].overall_percentage
|
|
318
|
+
elif len(records) == 1:
|
|
319
|
+
return records[0].overall_percentage
|
|
320
|
+
else:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
except Exception:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _analyze_coverage_files(root: "Element") -> dict[str, Any]:
|
|
328
|
+
"""Analyze file-level coverage from XML.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Dict with total, well_covered, critical, untested, gaps
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
files_total = 0
|
|
335
|
+
files_well_covered = 0 # >= 80%
|
|
336
|
+
files_critical = 0 # < 50%
|
|
337
|
+
untested_files = []
|
|
338
|
+
critical_gaps = []
|
|
339
|
+
|
|
340
|
+
for package in root.findall(".//package"):
|
|
341
|
+
for class_elem in package.findall("classes/class"):
|
|
342
|
+
files_total += 1
|
|
343
|
+
filename = class_elem.attrib.get("filename", "unknown")
|
|
344
|
+
line_rate = float(class_elem.attrib.get("line-rate", 0))
|
|
345
|
+
coverage_pct = line_rate * 100
|
|
346
|
+
|
|
347
|
+
if coverage_pct >= 80:
|
|
348
|
+
files_well_covered += 1
|
|
349
|
+
elif coverage_pct < 50:
|
|
350
|
+
files_critical += 1
|
|
351
|
+
critical_gaps.append(
|
|
352
|
+
{"file": filename, "coverage": coverage_pct, "priority": "high"}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if coverage_pct == 0:
|
|
356
|
+
untested_files.append(filename)
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"total": files_total,
|
|
360
|
+
"well_covered": files_well_covered,
|
|
361
|
+
"critical": files_critical,
|
|
362
|
+
"untested": untested_files[:10], # Limit to 10
|
|
363
|
+
"gaps": critical_gaps[:10], # Limit to 10
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def track_file_tests(
|
|
368
|
+
source_file: str,
|
|
369
|
+
test_file: str | None = None,
|
|
370
|
+
workflow_id: str | None = None,
|
|
371
|
+
) -> FileTestRecord:
|
|
372
|
+
"""Track test execution for a specific source file.
|
|
373
|
+
|
|
374
|
+
Runs tests associated with a source file and creates a FileTestRecord.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
source_file: Path to the source file to test
|
|
378
|
+
test_file: Path to the test file (auto-detected if not provided)
|
|
379
|
+
workflow_id: Optional workflow ID to link this execution
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
FileTestRecord with per-file test results
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> from attune.workflows.test_runner import track_file_tests
|
|
386
|
+
>>> result = track_file_tests("src/attune/config.py")
|
|
387
|
+
>>> print(f"Tests for config.py: {result.last_test_result}")
|
|
388
|
+
"""
|
|
389
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
390
|
+
started_at = datetime.utcnow()
|
|
391
|
+
|
|
392
|
+
source_path = Path(source_file)
|
|
393
|
+
|
|
394
|
+
# Auto-detect test file if not provided
|
|
395
|
+
if test_file is None:
|
|
396
|
+
test_file = _find_test_file(source_file)
|
|
397
|
+
|
|
398
|
+
# Get file modification times
|
|
399
|
+
source_modified_at = None
|
|
400
|
+
tests_modified_at = None
|
|
401
|
+
|
|
402
|
+
if source_path.exists():
|
|
403
|
+
source_modified_at = datetime.fromtimestamp(source_path.stat().st_mtime).isoformat() + "Z"
|
|
404
|
+
|
|
405
|
+
if test_file:
|
|
406
|
+
test_path = Path(test_file)
|
|
407
|
+
if test_path.exists():
|
|
408
|
+
tests_modified_at = datetime.fromtimestamp(test_path.stat().st_mtime).isoformat() + "Z"
|
|
409
|
+
|
|
410
|
+
# Check if we have tests to run
|
|
411
|
+
if test_file is None or not Path(test_file).exists():
|
|
412
|
+
# No tests found for this file
|
|
413
|
+
record = FileTestRecord(
|
|
414
|
+
file_path=source_file,
|
|
415
|
+
timestamp=timestamp,
|
|
416
|
+
last_test_result="no_tests",
|
|
417
|
+
test_count=0,
|
|
418
|
+
test_file_path=test_file,
|
|
419
|
+
source_modified_at=source_modified_at,
|
|
420
|
+
tests_modified_at=tests_modified_at,
|
|
421
|
+
is_stale=False,
|
|
422
|
+
workflow_id=workflow_id,
|
|
423
|
+
)
|
|
424
|
+
_log_file_test(record)
|
|
425
|
+
return record
|
|
426
|
+
|
|
427
|
+
# Run pytest for this specific test file
|
|
428
|
+
command = f"pytest {test_file} -v --tb=short"
|
|
429
|
+
|
|
430
|
+
logger.info(f"Running tests for {source_file}: {command}")
|
|
431
|
+
try:
|
|
432
|
+
cmd_args = shlex.split(command)
|
|
433
|
+
result = subprocess.run(
|
|
434
|
+
cmd_args,
|
|
435
|
+
shell=False,
|
|
436
|
+
capture_output=True,
|
|
437
|
+
text=True,
|
|
438
|
+
timeout=300, # 5 minute timeout per file
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
output = result.stdout + result.stderr
|
|
442
|
+
total_tests, passed, failed, skipped, errors = _parse_pytest_output(output)
|
|
443
|
+
|
|
444
|
+
# Determine result status
|
|
445
|
+
if result.returncode == 0:
|
|
446
|
+
last_test_result = "passed"
|
|
447
|
+
elif failed > 0:
|
|
448
|
+
last_test_result = "failed"
|
|
449
|
+
elif errors > 0:
|
|
450
|
+
last_test_result = "error"
|
|
451
|
+
elif skipped == total_tests:
|
|
452
|
+
last_test_result = "skipped"
|
|
453
|
+
else:
|
|
454
|
+
last_test_result = "failed"
|
|
455
|
+
|
|
456
|
+
failed_tests = _parse_pytest_failures(output) if failed > 0 or errors > 0 else []
|
|
457
|
+
execution_id = f"file-{uuid.uuid4()}"
|
|
458
|
+
|
|
459
|
+
except subprocess.TimeoutExpired:
|
|
460
|
+
logger.error(f"Test execution timed out for {source_file}")
|
|
461
|
+
total_tests, passed, failed, skipped, errors = 0, 0, 0, 0, 1
|
|
462
|
+
last_test_result = "error"
|
|
463
|
+
failed_tests = [{"name": "timeout", "file": test_file, "error": "Timed out"}]
|
|
464
|
+
execution_id = f"file-{uuid.uuid4()}"
|
|
465
|
+
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.error(f"Test execution failed for {source_file}: {e}")
|
|
468
|
+
total_tests, passed, failed, skipped, errors = 0, 0, 0, 0, 1
|
|
469
|
+
last_test_result = "error"
|
|
470
|
+
failed_tests = [{"name": "execution_error", "file": test_file, "error": str(e)}]
|
|
471
|
+
execution_id = f"file-{uuid.uuid4()}"
|
|
472
|
+
|
|
473
|
+
# Calculate duration
|
|
474
|
+
completed_at = datetime.utcnow()
|
|
475
|
+
duration_seconds = (completed_at - started_at).total_seconds()
|
|
476
|
+
|
|
477
|
+
# Check staleness (source modified after tests last modified)
|
|
478
|
+
is_stale = False
|
|
479
|
+
if source_modified_at and tests_modified_at:
|
|
480
|
+
is_stale = source_modified_at > tests_modified_at
|
|
481
|
+
|
|
482
|
+
record = FileTestRecord(
|
|
483
|
+
file_path=source_file,
|
|
484
|
+
timestamp=timestamp,
|
|
485
|
+
last_test_result=last_test_result,
|
|
486
|
+
test_count=total_tests,
|
|
487
|
+
passed=passed,
|
|
488
|
+
failed=failed,
|
|
489
|
+
skipped=skipped,
|
|
490
|
+
errors=errors,
|
|
491
|
+
duration_seconds=duration_seconds,
|
|
492
|
+
test_file_path=test_file,
|
|
493
|
+
failed_tests=failed_tests,
|
|
494
|
+
source_modified_at=source_modified_at,
|
|
495
|
+
tests_modified_at=tests_modified_at,
|
|
496
|
+
is_stale=is_stale,
|
|
497
|
+
execution_id=execution_id,
|
|
498
|
+
workflow_id=workflow_id,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
_log_file_test(record)
|
|
502
|
+
return record
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def get_file_test_status(file_path: str) -> FileTestRecord | None:
|
|
506
|
+
"""Get the latest test status for a specific file.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
file_path: Path to the source file
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Latest FileTestRecord or None if no tests recorded
|
|
513
|
+
"""
|
|
514
|
+
store = get_telemetry_store()
|
|
515
|
+
return store.get_latest_file_test(file_path)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_files_needing_tests(
|
|
519
|
+
stale_only: bool = False,
|
|
520
|
+
failed_only: bool = False,
|
|
521
|
+
) -> list[FileTestRecord]:
|
|
522
|
+
"""Get files that need test attention.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
stale_only: Only return files with stale tests
|
|
526
|
+
failed_only: Only return files with failed tests
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of FileTestRecord for files needing attention
|
|
530
|
+
"""
|
|
531
|
+
store = get_telemetry_store()
|
|
532
|
+
return store.get_files_needing_tests(stale_only=stale_only, failed_only=failed_only)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _find_test_file(source_file: str) -> str | None:
|
|
536
|
+
"""Find the test file for a given source file.
|
|
537
|
+
|
|
538
|
+
Uses comprehensive search to find test files:
|
|
539
|
+
1. First checks explicit patterns based on source file location
|
|
540
|
+
2. Falls back to glob search for test_{filename}.py anywhere in tests/
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
source_file: Path to the source file
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Path to test file or None if not found
|
|
547
|
+
"""
|
|
548
|
+
source_path = Path(source_file)
|
|
549
|
+
filename = source_path.stem
|
|
550
|
+
parent = source_path.parent
|
|
551
|
+
|
|
552
|
+
# Skip __init__.py - rarely have dedicated tests
|
|
553
|
+
if filename == "__init__":
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
# Build list of explicit patterns to check first (most specific)
|
|
557
|
+
patterns = []
|
|
558
|
+
|
|
559
|
+
# Extract module info from source path
|
|
560
|
+
# e.g., src/attune/models/registry.py -> module="models"
|
|
561
|
+
module_name = None
|
|
562
|
+
if "src" in source_path.parts:
|
|
563
|
+
try:
|
|
564
|
+
src_idx = source_path.parts.index("src")
|
|
565
|
+
rel_parts = source_path.parts[src_idx + 1 : -1] # Exclude src and filename
|
|
566
|
+
if len(rel_parts) >= 2:
|
|
567
|
+
# e.g., ('attune', 'models') -> module_name = 'models'
|
|
568
|
+
module_name = rel_parts[-1]
|
|
569
|
+
except (ValueError, IndexError):
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
# Priority 1: Module-specific test directory
|
|
573
|
+
# e.g., src/attune/models/registry.py -> tests/unit/models/test_registry.py
|
|
574
|
+
if module_name:
|
|
575
|
+
patterns.extend(
|
|
576
|
+
[
|
|
577
|
+
Path("tests") / "unit" / module_name / f"test_{filename}.py",
|
|
578
|
+
Path("tests") / module_name / f"test_{filename}.py",
|
|
579
|
+
Path("tests") / "integration" / module_name / f"test_{filename}.py",
|
|
580
|
+
]
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Priority 2: Standard locations
|
|
584
|
+
patterns.extend(
|
|
585
|
+
[
|
|
586
|
+
Path("tests") / "unit" / f"test_{filename}.py",
|
|
587
|
+
Path("tests") / f"test_{filename}.py",
|
|
588
|
+
Path("tests") / "integration" / f"test_{filename}.py",
|
|
589
|
+
parent / f"test_{filename}.py",
|
|
590
|
+
]
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Check explicit patterns first
|
|
594
|
+
for pattern in patterns:
|
|
595
|
+
if pattern.exists():
|
|
596
|
+
return str(pattern)
|
|
597
|
+
|
|
598
|
+
# Priority 3: Glob search - find test_{filename}.py anywhere in tests/
|
|
599
|
+
tests_dir = Path("tests")
|
|
600
|
+
if tests_dir.exists():
|
|
601
|
+
# Search for exact match first
|
|
602
|
+
matches = list(tests_dir.rglob(f"test_{filename}.py"))
|
|
603
|
+
if matches:
|
|
604
|
+
# Return the first match (preferring shorter paths)
|
|
605
|
+
matches.sort(key=lambda p: len(p.parts))
|
|
606
|
+
return str(matches[0])
|
|
607
|
+
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _log_file_test(record: FileTestRecord) -> None:
|
|
612
|
+
"""Log a FileTestRecord to the telemetry store.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
record: FileTestRecord to log
|
|
616
|
+
"""
|
|
617
|
+
try:
|
|
618
|
+
store = get_telemetry_store()
|
|
619
|
+
store.log_file_test(record)
|
|
620
|
+
logger.info(f"File test tracked: {record.file_path} ({record.last_test_result})")
|
|
621
|
+
except Exception as e:
|
|
622
|
+
logger.warning(f"Failed to log file test: {e}")
|