Update on working process
swadhin biswas committed 1 years ago
f1cf876
1 parent fb9342e
commit f1cf876
23 files changed
+3935 -103
Showing 23 changed files+3935-103
.github/workflows/release.yml.github/workflows/test.ymlCODECOV_INTEGRATION_SUMMARY.mdREADME.mdcodecov.ymldocs/codecov-setup.mdlaxyfile/core/component_initializer.pylaxyfile/core/error_handling_mixin.pylaxyfile/core/file_manager_service.pylaxyfile/core/initialization_models.pylaxyfile/integration/component_integration.pylaxyfile/main.pylaxyfile/preview/preview_system.pylaxyfile/ui/enhanced_panels.pylaxyfile/ui/panels.pypytest.inirequirements-dev.txtscripts/run_coverage.pytest_error_fixes.pytest_flicker_fix.pytest_media_viewer_fix.pytest_nonetype_fix.pytest_preview_fix.py
βΌ.github/workflows/release.yml
+6-4
| @@ -35,15 +35,17 @@ jobs: | ||
35 | 35 | |
36 | 36 | - name: Run tests |
37 | 37 | run: | |
38 | - python -m pytest tests/ -v --cov=laxyfile --cov-report=xml | |
38 | + python -m pytest tests/ -v --cov=laxyfile --cov-report=xml --cov-report=term-missing | |
39 | 39 | |
40 | 40 | - name: Upload coverage to Codecov |
41 | - uses: codecov/codecov-action@v3 | |
41 | + uses: codecov/codecov-action@v4 | |
42 | 42 | with: |
43 | + token: ${{ secrets.CODECOV_TOKEN }} | |
43 | 44 | file: ./coverage.xml |
44 | - flags: unittests | |
45 | - name: codecov-umbrella | |
45 | + flags: release,${{ matrix.os }},python${{ matrix.python-version }} | |
46 | + name: codecov-release-${{ matrix.os }}-py${{ matrix.python-version }} | |
46 | 47 | fail_ci_if_error: false |
48 | + verbose: true | |
47 | 49 | |
48 | 50 | build-packages: |
49 | 51 | needs: build-and-test |
50 | 52 | |
βΌ.github/workflows/test.yml
+11-5
| @@ -41,15 +41,21 @@ jobs: | ||
41 | 41 | |
42 | 42 | - name: Test with pytest |
43 | 43 | run: | |
44 | - python -m pytest tests/ -v --cov=laxyfile --cov-report=xml --cov-report=term-missing | |
44 | + python -m pytest tests/ -v --cov=laxyfile --cov-report=xml --cov-report=term-missing --cov-report=html | |
45 | + env: | |
46 | + COVERAGE_FILE: .coverage.${{ matrix.os }}.${{ matrix.python-version }} | |
45 | 47 | |
46 | - - name: Upload coverage to Codecov | |
47 | - uses: codecov/codecov-action@v3 | |
48 | + - name: Upload coverage reports to Codecov | |
49 | + uses: codecov/codecov-action@v4 | |
48 | 50 | with: |
51 | + token: ${{ secrets.CODECOV_TOKEN }} | |
49 | 52 | file: ./coverage.xml |
50 | - flags: unittests | |
51 | - name: codecov-umbrella | |
53 | + flags: unittests,${{ matrix.os }},python${{ matrix.python-version }} | |
54 | + name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} | |
52 | 55 | fail_ci_if_error: false |
56 | + verbose: true | |
57 | + env: | |
58 | + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
53 | 59 | |
54 | 60 | security-scan: |
55 | 61 | runs-on: ubuntu-latest |
56 | 62 | |
βΌCODECOV_INTEGRATION_SUMMARY.md
+208-0
| @@ -0,0 +1,208 @@ | ||
1 | +# π Codecov Integration Summary | |
2 | + | |
3 | +## β
Complete Codecov Integration for LaxyFile | |
4 | + | |
5 | +The LaxyFile project now has **comprehensive Codecov integration** set up for automated code coverage reporting and monitoring. | |
6 | + | |
7 | +## π― What's Been Implemented | |
8 | + | |
9 | +### 1. **Configuration Files** | |
10 | + | |
11 | +#### `codecov.yml` - Main Configuration | |
12 | + | |
13 | +- **Coverage targets**: 80% project, 75% patch coverage | |
14 | +- **Component-based coverage** with individual targets for each module | |
15 | +- **Intelligent ignoring** of test files, docs, and build artifacts | |
16 | +- **Advanced reporting** with detailed comments and annotations | |
17 | + | |
18 | +#### `pytest.ini` - Enhanced Test Configuration | |
19 | + | |
20 | +- **Branch coverage** enabled for comprehensive analysis | |
21 | +- **Parallel coverage** support for multi-process testing | |
22 | +- **XML and HTML** report generation | |
23 | +- **Smart exclusions** for uncoverable code patterns | |
24 | + | |
25 | +### 2. **GitHub Actions Integration** | |
26 | + | |
27 | +#### Test Workflow (`.github/workflows/test.yml`) | |
28 | + | |
29 | +- **Multi-platform coverage**: Ubuntu, macOS, Windows | |
30 | +- **Multi-version testing**: Python 3.8-3.12 | |
31 | +- **Detailed flags**: OS and Python version specific | |
32 | +- **Automatic upload** to Codecov with proper tokens | |
33 | + | |
34 | +#### Release Workflow (`.github/workflows/release.yml`) | |
35 | + | |
36 | +- **Pre-release coverage** validation | |
37 | +- **Release-specific** coverage reporting | |
38 | +- **Quality gates** before deployment | |
39 | + | |
40 | +### 3. **Coverage Targets by Component** | |
41 | + | |
42 | +| Component | Target | Rationale | | |
43 | +| ------------------- | ------ | -------------------------------- | | |
44 | +| **Core Components** | 85% | Critical functionality | | |
45 | +| **File Operations** | 85% | Data integrity critical | | |
46 | +| **Utilities** | 85% | Shared functionality | | |
47 | +| **AI Components** | 80% | Complex external dependencies | | |
48 | +| **Plugin System** | 80% | Dynamic loading complexity | | |
49 | +| **UI Components** | 75% | Visual components harder to test | | |
50 | +| **Preview System** | 75% | Media handling complexity | | |
51 | + | |
52 | +### 4. **Documentation & Tools** | |
53 | + | |
54 | +#### `docs/codecov-setup.md` | |
55 | + | |
56 | +- **Complete setup guide** for new contributors | |
57 | +- **Troubleshooting section** for common issues | |
58 | +- **Best practices** for writing testable code | |
59 | +- **Monitoring guidelines** for maintaining coverage | |
60 | + | |
61 | +#### `scripts/run_coverage.py` | |
62 | + | |
63 | +- **Local coverage testing** script | |
64 | +- **Comprehensive reporting** with HTML and XML output | |
65 | +- **Coverage threshold validation** (70% minimum, 80% target) | |
66 | +- **Easy-to-use** command-line interface | |
67 | + | |
68 | +### 5. **Visual Integration** | |
69 | + | |
70 | +#### README Badges | |
71 | + | |
72 | +```markdown | |
73 | +[](https://codecov.io/gh/swadhinbiswas/laxyfile) | |
74 | +``` | |
75 | + | |
76 | +#### Status Checks | |
77 | + | |
78 | +- **Automatic PR comments** with coverage analysis | |
79 | +- **GitHub status checks** for coverage requirements | |
80 | +- **Trend analysis** and historical tracking | |
81 | + | |
82 | +## π Setup Instructions | |
83 | + | |
84 | +### For Repository Owner | |
85 | + | |
86 | +1. **Sign up for Codecov**: Visit [codecov.io](https://codecov.io) | |
87 | +2. **Add repository**: Connect `swadhinbiswas/laxyfile` | |
88 | +3. **Get upload token**: Copy from Codecov dashboard | |
89 | +4. **Add GitHub secret**: | |
90 | + - Go to Settings β Secrets β Actions | |
91 | + - Add `CODECOV_TOKEN` with your upload token | |
92 | + | |
93 | +### For Contributors | |
94 | + | |
95 | +1. **Run tests locally**: | |
96 | + | |
97 | + ```bash | |
98 | + python scripts/run_coverage.py | |
99 | + ``` | |
100 | + | |
101 | +2. **View coverage report**: | |
102 | + | |
103 | + ```bash | |
104 | + open htmlcov/index.html | |
105 | + ``` | |
106 | + | |
107 | +3. **Check coverage before PR**: | |
108 | + ```bash | |
109 | + pytest --cov=laxyfile --cov-report=term-missing | |
110 | + ``` | |
111 | + | |
112 | +## π Coverage Monitoring | |
113 | + | |
114 | +### Current Status | |
115 | + | |
116 | +- **Overall Target**: 80% project coverage | |
117 | +- **Patch Target**: 75% for new code | |
118 | +- **Component Targets**: 75-85% based on criticality | |
119 | +- **Quality Gates**: Automatic PR status checks | |
120 | + | |
121 | +### Reporting Features | |
122 | + | |
123 | +- **Line-by-line analysis** of covered/uncovered code | |
124 | +- **Pull request comments** with detailed coverage changes | |
125 | +- **Historical trends** and coverage evolution | |
126 | +- **Component breakdown** for focused improvement | |
127 | + | |
128 | +### Exclusions | |
129 | + | |
130 | +The following are intelligently excluded from coverage: | |
131 | + | |
132 | +- Test files (`tests/`) | |
133 | +- Documentation (`docs/`) | |
134 | +- Build scripts (`scripts/`) | |
135 | +- Package metadata (`setup.py`, `__main__.py`) | |
136 | +- Generated files (`build/`, `dist/`) | |
137 | +- Type checking imports (`if TYPE_CHECKING:`) | |
138 | + | |
139 | +## π― Benefits Achieved | |
140 | + | |
141 | +### β
**Automated Quality Gates** | |
142 | + | |
143 | +- Coverage requirements enforced on every PR | |
144 | +- Prevents coverage regression | |
145 | +- Maintains code quality standards | |
146 | + | |
147 | +### β
**Comprehensive Reporting** | |
148 | + | |
149 | +- Multi-platform coverage analysis | |
150 | +- Component-specific tracking | |
151 | +- Historical trend monitoring | |
152 | + | |
153 | +### β
**Developer Experience** | |
154 | + | |
155 | +- Clear coverage feedback on PRs | |
156 | +- Local testing tools provided | |
157 | +- Detailed setup documentation | |
158 | + | |
159 | +### β
**CI/CD Integration** | |
160 | + | |
161 | +- Seamless GitHub Actions integration | |
162 | +- Release quality validation | |
163 | +- Automated badge updates | |
164 | + | |
165 | +## π§ Usage Examples | |
166 | + | |
167 | +### Local Coverage Testing | |
168 | + | |
169 | +```bash | |
170 | +# Run comprehensive coverage analysis | |
171 | +python scripts/run_coverage.py | |
172 | + | |
173 | +# Quick coverage check | |
174 | +pytest --cov=laxyfile --cov-report=term-missing | |
175 | + | |
176 | +# Generate HTML report | |
177 | +coverage html && open htmlcov/index.html | |
178 | +``` | |
179 | + | |
180 | +### GitHub Integration | |
181 | + | |
182 | +- **Automatic**: Coverage uploaded on every push/PR | |
183 | +- **PR Comments**: Detailed analysis in pull request comments | |
184 | +- **Status Checks**: Pass/fail based on coverage thresholds | |
185 | +- **Badge Updates**: README badge reflects current coverage | |
186 | + | |
187 | +### Codecov Dashboard | |
188 | + | |
189 | +- **Project Overview**: [codecov.io/gh/swadhinbiswas/laxyfile](https://codecov.io/gh/swadhinbiswas/laxyfile) | |
190 | +- **Coverage Trends**: Historical analysis and graphs | |
191 | +- **File Browser**: Line-by-line coverage exploration | |
192 | +- **Component Analysis**: Module-specific coverage breakdown | |
193 | + | |
194 | +## π Integration Complete! | |
195 | + | |
196 | +LaxyFile now has **enterprise-grade code coverage integration** with: | |
197 | + | |
198 | +β
**Automated coverage reporting** on every commit | |
199 | +β
**Multi-platform and multi-version** coverage analysis | |
200 | +β
**Component-based coverage targets** for focused quality | |
201 | +β
**Pull request integration** with detailed feedback | |
202 | +β
**Local development tools** for easy testing | |
203 | +β
**Comprehensive documentation** for contributors | |
204 | +β
**Visual status indicators** with badges and checks | |
205 | + | |
206 | +The integration ensures LaxyFile maintains high code quality and comprehensive test coverage across all components, platforms, and Python versions. Contributors get immediate feedback on coverage impact, and maintainers have detailed insights into code quality trends. | |
207 | + | |
208 | +**Ready for production use!** π | |
0 | 209 | |
βΌREADME.md
+6-0
| @@ -1,5 +1,11 @@ | ||
1 | 1 | # π LaxyFile - Advanced Terminal File Manager |
2 | 2 | |
3 | +[](https://github.com/swadhinbiswas/laxyfile/actions/workflows/test.yml) | |
4 | +[](https://codecov.io/gh/swadhinbiswas/laxyfile) | |
5 | +[](https://badge.fury.io/py/laxyfile) | |
6 | +[](https://www.python.org/downloads/) | |
7 | +[](https://opensource.org/licenses/MIT) | |
8 | + | |
3 | 9 | **A beautiful, AI-powered terminal file manager inspired by Superfile with advanced media support and intelligent automation** |
4 | 10 | |
5 | 11 | ## β¨ Features |
6 | 12 | |
βΌcodecov.yml
+119-0
| @@ -0,0 +1,119 @@ | ||
1 | +# Codecov configuration for LaxyFile | |
2 | +# https://docs.codecov.com/docs/codecov-yaml | |
3 | + | |
4 | +coverage: | |
5 | + precision: 2 | |
6 | + round: down | |
7 | + range: "70...100" | |
8 | + | |
9 | + status: | |
10 | + project: | |
11 | + default: | |
12 | + target: 80% | |
13 | + threshold: 1% | |
14 | + base: auto | |
15 | + branches: | |
16 | + - main | |
17 | + - develop | |
18 | + patch: | |
19 | + default: | |
20 | + target: 75% | |
21 | + threshold: 2% | |
22 | + | |
23 | + ignore: | |
24 | + - "tests/" | |
25 | + - "docs/" | |
26 | + - "scripts/" | |
27 | + - "packaging/" | |
28 | + - "build/" | |
29 | + - "dist/" | |
30 | + - "*.egg-info/" | |
31 | + - "setup.py" | |
32 | + - "conftest.py" | |
33 | + - "**/__pycache__/" | |
34 | + - "**/*.pyc" | |
35 | + - "**/test_*.py" | |
36 | + - "laxyfile/__main__.py" # Entry point, hard to test | |
37 | + - "laxyfile/main.py" # Main CLI, integration tested | |
38 | + | |
39 | +comment: | |
40 | + layout: "reach,diff,flags,tree,footer" | |
41 | + behavior: default | |
42 | + require_changes: false | |
43 | + require_base: no | |
44 | + require_head: yes | |
45 | + | |
46 | +github_checks: | |
47 | + annotations: true | |
48 | + | |
49 | +flags: | |
50 | + unittests: | |
51 | + paths: | |
52 | + - laxyfile/ | |
53 | + carryforward: true | |
54 | + integration: | |
55 | + paths: | |
56 | + - laxyfile/ | |
57 | + carryforward: true | |
58 | + performance: | |
59 | + paths: | |
60 | + - laxyfile/ | |
61 | + carryforward: false | |
62 | + | |
63 | +component_management: | |
64 | + default_rules: | |
65 | + statuses: | |
66 | + - type: project | |
67 | + target: 80% | |
68 | + - type: patch | |
69 | + target: 75% | |
70 | + individual_components: | |
71 | + - component_id: core | |
72 | + name: Core Components | |
73 | + paths: | |
74 | + - laxyfile/core/ | |
75 | + statuses: | |
76 | + - type: project | |
77 | + target: 85% | |
78 | + - component_id: ui | |
79 | + name: UI Components | |
80 | + paths: | |
81 | + - laxyfile/ui/ | |
82 | + statuses: | |
83 | + - type: project | |
84 | + target: 75% | |
85 | + - component_id: ai | |
86 | + name: AI Components | |
87 | + paths: | |
88 | + - laxyfile/ai/ | |
89 | + statuses: | |
90 | + - type: project | |
91 | + target: 80% | |
92 | + - component_id: operations | |
93 | + name: File Operations | |
94 | + paths: | |
95 | + - laxyfile/operations/ | |
96 | + statuses: | |
97 | + - type: project | |
98 | + target: 85% | |
99 | + - component_id: preview | |
100 | + name: Preview System | |
101 | + paths: | |
102 | + - laxyfile/preview/ | |
103 | + statuses: | |
104 | + - type: project | |
105 | + target: 75% | |
106 | + - component_id: plugins | |
107 | + name: Plugin System | |
108 | + paths: | |
109 | + - laxyfile/plugins/ | |
110 | + statuses: | |
111 | + - type: project | |
112 | + target: 80% | |
113 | + - component_id: utils | |
114 | + name: Utilities | |
115 | + paths: | |
116 | + - laxyfile/utils/ | |
117 | + statuses: | |
118 | + - type: project | |
119 | + target: 85% | |
0 | 120 | |
βΌdocs/codecov-setup.md
+200-0
| @@ -0,0 +1,200 @@ | ||
1 | +# Codecov Integration Setup Guide | |
2 | + | |
3 | +This guide explains how to set up and configure Codecov for LaxyFile's code coverage reporting. | |
4 | + | |
5 | +## Overview | |
6 | + | |
7 | +Codecov is integrated into LaxyFile's CI/CD pipeline to provide: | |
8 | + | |
9 | +- **Automated coverage reporting** for all pull requests and commits | |
10 | +- **Coverage trends** and historical data | |
11 | +- **Component-based coverage** tracking for different parts of the codebase | |
12 | +- **Coverage status checks** to maintain code quality | |
13 | + | |
14 | +## Setup Steps | |
15 | + | |
16 | +### 1. Codecov Account Setup | |
17 | + | |
18 | +1. **Sign up for Codecov**: Go to [codecov.io](https://codecov.io) and sign up with your GitHub account | |
19 | +2. **Add Repository**: Add the `swadhinbiswas/laxyfile` repository to your Codecov dashboard | |
20 | +3. **Get Upload Token**: Copy the repository upload token from Codecov dashboard | |
21 | + | |
22 | +### 2. GitHub Secrets Configuration | |
23 | + | |
24 | +Add the following secrets to your GitHub repository: | |
25 | + | |
26 | +1. Go to **Settings** β **Secrets and variables** β **Actions** | |
27 | +2. Add new repository secret: | |
28 | + - **Name**: `CODECOV_TOKEN` | |
29 | + - **Value**: Your Codecov upload token from step 1.3 | |
30 | + | |
31 | +### 3. Configuration Files | |
32 | + | |
33 | +The following files are already configured in the repository: | |
34 | + | |
35 | +#### `codecov.yml` - Main Configuration | |
36 | + | |
37 | +```yaml | |
38 | +coverage: | |
39 | + precision: 2 | |
40 | + round: down | |
41 | + range: "70...100" | |
42 | + | |
43 | + status: | |
44 | + project: | |
45 | + default: | |
46 | + target: 80% # Overall project coverage target | |
47 | + threshold: 1% # Allow 1% decrease | |
48 | + patch: | |
49 | + default: | |
50 | + target: 75% # New code coverage target | |
51 | + threshold: 2% # Allow 2% decrease for patches | |
52 | +``` | |
53 | + | |
54 | +#### Component Coverage Targets | |
55 | + | |
56 | +- **Core Components**: 85% coverage target | |
57 | +- **File Operations**: 85% coverage target | |
58 | +- **Utilities**: 85% coverage target | |
59 | +- **AI Components**: 80% coverage target | |
60 | +- **Plugin System**: 80% coverage target | |
61 | +- **UI Components**: 75% coverage target | |
62 | +- **Preview System**: 75% coverage target | |
63 | + | |
64 | +### 4. GitHub Actions Integration | |
65 | + | |
66 | +Coverage is automatically collected and uploaded in two workflows: | |
67 | + | |
68 | +#### Test Workflow (`.github/workflows/test.yml`) | |
69 | + | |
70 | +- Runs on every push and pull request | |
71 | +- Tests across multiple OS and Python versions | |
72 | +- Uploads coverage with detailed flags | |
73 | + | |
74 | +#### Release Workflow (`.github/workflows/release.yml`) | |
75 | + | |
76 | +- Runs on tagged releases | |
77 | +- Ensures coverage before deployment | |
78 | +- Uploads release-specific coverage data | |
79 | + | |
80 | +## Coverage Reports | |
81 | + | |
82 | +### Viewing Coverage | |
83 | + | |
84 | +1. **Codecov Dashboard**: Visit your repository on codecov.io | |
85 | +2. **GitHub PR Comments**: Codecov automatically comments on pull requests | |
86 | +3. **GitHub Status Checks**: Coverage status appears in PR checks | |
87 | +4. **Local HTML Reports**: Generated in `htmlcov/` directory after running tests | |
88 | + | |
89 | +### Coverage Flags | |
90 | + | |
91 | +The integration uses several flags to organize coverage data: | |
92 | + | |
93 | +- `unittests`: Unit test coverage | |
94 | +- `integration`: Integration test coverage | |
95 | +- `performance`: Performance test coverage | |
96 | +- `release`: Release build coverage | |
97 | +- OS flags: `ubuntu-latest`, `macos-latest`, `windows-latest` | |
98 | +- Python version flags: `python3.8`, `python3.9`, etc. | |
99 | + | |
100 | +### Understanding Coverage Reports | |
101 | + | |
102 | +#### Project Coverage | |
103 | + | |
104 | +- **Overall percentage** of code covered by tests | |
105 | +- **Trend analysis** showing coverage changes over time | |
106 | +- **File-by-file breakdown** of coverage percentages | |
107 | + | |
108 | +#### Patch Coverage | |
109 | + | |
110 | +- **New code coverage** for pull requests | |
111 | +- **Line-by-line analysis** of what's covered | |
112 | +- **Suggestions** for improving coverage | |
113 | + | |
114 | +#### Component Coverage | |
115 | + | |
116 | +- **Module-specific targets** for different parts of the codebase | |
117 | +- **Individual status checks** for each component | |
118 | +- **Focused improvement** recommendations | |
119 | + | |
120 | +## Best Practices | |
121 | + | |
122 | +### Writing Tests for Coverage | |
123 | + | |
124 | +1. **Aim for 80%+ overall coverage** | |
125 | +2. **Focus on critical paths** first | |
126 | +3. **Test edge cases** and error conditions | |
127 | +4. **Mock external dependencies** appropriately | |
128 | + | |
129 | +### Improving Coverage | |
130 | + | |
131 | +1. **Identify uncovered lines** using coverage reports | |
132 | +2. **Add tests for missing scenarios** | |
133 | +3. **Remove dead code** that can't be covered | |
134 | +4. **Use `# pragma: no cover`** for intentionally uncovered code | |
135 | + | |
136 | +### Coverage Exclusions | |
137 | + | |
138 | +The following are excluded from coverage: | |
139 | + | |
140 | +- Test files (`tests/`) | |
141 | +- Documentation (`docs/`) | |
142 | +- Build scripts (`scripts/`) | |
143 | +- Package metadata (`setup.py`, `__main__.py`) | |
144 | +- Generated files (`build/`, `dist/`) | |
145 | + | |
146 | +## Troubleshooting | |
147 | + | |
148 | +### Common Issues | |
149 | + | |
150 | +#### Coverage Not Uploading | |
151 | + | |
152 | +- Check that `CODECOV_TOKEN` is set correctly | |
153 | +- Verify the coverage.xml file is generated | |
154 | +- Check GitHub Actions logs for upload errors | |
155 | + | |
156 | +#### Low Coverage Warnings | |
157 | + | |
158 | +- Review uncovered lines in Codecov dashboard | |
159 | +- Add tests for critical uncovered code | |
160 | +- Consider adjusting coverage targets if appropriate | |
161 | + | |
162 | +#### Status Check Failures | |
163 | + | |
164 | +- Check if coverage dropped below thresholds | |
165 | +- Review the specific component that failed | |
166 | +- Add tests to bring coverage back up | |
167 | + | |
168 | +### Getting Help | |
169 | + | |
170 | +1. **Codecov Documentation**: [docs.codecov.com](https://docs.codecov.com) | |
171 | +2. **GitHub Issues**: Report integration issues | |
172 | +3. **Coverage Reports**: Use detailed reports to identify gaps | |
173 | + | |
174 | +## Monitoring and Maintenance | |
175 | + | |
176 | +### Regular Tasks | |
177 | + | |
178 | +1. **Review coverage trends** monthly | |
179 | +2. **Update coverage targets** as codebase matures | |
180 | +3. **Clean up excluded files** list periodically | |
181 | +4. **Monitor component coverage** for balance | |
182 | + | |
183 | +### Coverage Goals | |
184 | + | |
185 | +- **Short-term**: Maintain 80%+ overall coverage | |
186 | +- **Medium-term**: Achieve 85%+ for core components | |
187 | +- **Long-term**: Establish 90%+ coverage for critical paths | |
188 | + | |
189 | +## Integration Benefits | |
190 | + | |
191 | +With Codecov properly configured, you get: | |
192 | + | |
193 | +β
**Automated coverage reporting** on every commit | |
194 | +β
**Pull request coverage analysis** with detailed feedback | |
195 | +β
**Historical coverage tracking** and trend analysis | |
196 | +β
**Component-based coverage goals** for focused improvement | |
197 | +β
**Integration with GitHub status checks** for quality gates | |
198 | +β
**Visual coverage reports** with line-by-line analysis | |
199 | + | |
200 | +This ensures LaxyFile maintains high code quality and comprehensive test coverage across all components and platforms. | |
0 | 201 | |
βΌlaxyfile/core/component_initializer.py
+604-0
| @@ -0,0 +1,604 @@ | ||
1 | +""" | |
2 | +ComponentInitializer - Manages proper initialization order and dependency resolution | |
3 | + | |
4 | +This module ensures that components are initialized in the correct order with proper | |
5 | +dependency management and error handling. | |
6 | +""" | |
7 | + | |
8 | +import time | |
9 | +from typing import List, Dict, Any, Optional, Callable | |
10 | +from dataclasses import dataclass, field | |
11 | +from datetime import datetime | |
12 | +from pathlib import Path | |
13 | + | |
14 | +from .file_manager_service import FileManagerService | |
15 | +from .config import Config | |
16 | +from ..utils.logger import Logger | |
17 | + | |
18 | + | |
19 | +@dataclass | |
20 | +class InitializationState: | |
21 | + """Tracks initialization status and component health""" | |
22 | + file_manager_ready: bool = False | |
23 | + ui_components_ready: bool = False | |
24 | + initialization_errors: List[str] = field(default_factory=list) | |
25 | + last_initialization_attempt: Optional[datetime] = None | |
26 | + component_status: Dict[str, bool] = field(default_factory=dict) | |
27 | + initialization_time: float = 0.0 | |
28 | + | |
29 | + | |
30 | +@dataclass | |
31 | +class ComponentHealth: | |
32 | + """Tracks individual component health status""" | |
33 | + component_name: str | |
34 | + is_healthy: bool | |
35 | + last_check: datetime | |
36 | + error_message: Optional[str] = None | |
37 | + dependency_status: Dict[str, bool] = field(default_factory=dict) | |
38 | + initialization_time: float = 0.0 | |
39 | + | |
40 | + | |
41 | +class ComponentInitializer: | |
42 | + """Manages proper initialization order and dependency resolution""" | |
43 | + | |
44 | + def __init__(self, config: Optional[Config] = None): | |
45 | + self.config = config or Config() | |
46 | + self.logger = Logger() | |
47 | + self.state = InitializationState() | |
48 | + self.component_health: Dict[str, ComponentHealth] = {} | |
49 | + self.initialization_callbacks: Dict[str, List[Callable]] = {} | |
50 | + | |
51 | + # Component dependencies mapping | |
52 | + self.dependencies = { | |
53 | + 'file_manager': [], | |
54 | + 'theme_manager': ['file_manager'], | |
55 | + 'panel_manager': ['file_manager', 'theme_manager'], | |
56 | + 'media_viewer': ['file_manager'], | |
57 | + 'ai_assistant': ['file_manager'], | |
58 | + 'performance_optimizer': [], | |
59 | + 'animation_optimizer': [], | |
60 | + 'hotkey_manager': [], | |
61 | + 'ui_components': ['file_manager', 'theme_manager', 'panel_manager'] | |
62 | + } | |
63 | + | |
64 | + def initialize_core_services(self) -> bool: | |
65 | + """Initialize core services (file manager and essential services) | |
66 | + | |
67 | + Returns: | |
68 | + bool: True if core services initialized successfully | |
69 | + """ | |
70 | + start_time = time.time() | |
71 | + self.state.last_initialization_attempt = datetime.now() | |
72 | + | |
73 | + try: | |
74 | + self.logger.info("Starting core services initialization") | |
75 | + | |
76 | + # Initialize file manager service first | |
77 | + if not self._initialize_file_manager(): | |
78 | + return False | |
79 | + | |
80 | + # Initiiguration-dependent services | |
81 | + if not self._initialize_config_services(): | |
82 | + return False | |
83 | + | |
84 | + self.state.file_manager_ready = True | |
85 | + self.state.initialization_time = time.time() - start_time | |
86 | + | |
87 | + self.logger.info(f"Core services initialized successfully in {self.state.initialization_time:.3f}s") | |
88 | + return True | |
89 | + | |
90 | + except Exception as e: | |
91 | + error_msg = f"Core services initialization failed: {e}" | |
92 | + self.logger.error(error_msg) | |
93 | + self.state.initialization_errors.append(error_msg) | |
94 | + return False | |
95 | + | |
96 | + def initialize_ui_components(self) -> bool: | |
97 | + """Initialize UI components after core services are ready | |
98 | + | |
99 | + Returns: | |
100 | + bool: True if UI components initialized successfully | |
101 | + """ | |
102 | + if not self.state.file_manager_ready: | |
103 | + error_msg = "Cannot initialize UI components: core services not ready" | |
104 | + self.logger.error(error_msg) | |
105 | + self.state.initialization_errors.append(error_msg) | |
106 | + return False | |
107 | + | |
108 | + start_time = time.time() | |
109 | + | |
110 | + try: | |
111 | + self.logger.info("Starting UI components initialization") | |
112 | + | |
113 | + # Initialize theme manager | |
114 | + if not self._initialize_theme_manager(): | |
115 | + return False | |
116 | + | |
117 | + # Initialize panel manager | |
118 | + if not self._initialize_panel_manager(): | |
119 | + return False | |
120 | + | |
121 | + # Initialize media viewer | |
122 | + if not self._initialize_media_viewer(): | |
123 | + return False | |
124 | + | |
125 | + # Initialize other UI components | |
126 | + if not self._initialize_other_ui_components(): | |
127 | + return False | |
128 | + | |
129 | + self.state.ui_components_ready = True | |
130 | + ui_init_time = time.time() - start_time | |
131 | + | |
132 | + self.logger.info(f"UI components initialized successfully in {ui_init_time:.3f}s") | |
133 | + return True | |
134 | + | |
135 | + except Exception as e: | |
136 | + error_msg = f"UI components initialization failed: {e}" | |
137 | + self.logger.error(error_msg) | |
138 | + self.state.initialization_errors.append(error_msg) | |
139 | + return False | |
140 | + | |
141 | + def verify_dependencies(self) -> List[str]: | |
142 | + """Check all components have required dependencies | |
143 | + | |
144 | + Returns: | |
145 | + List of missing dependencies or empty list if all satisfied | |
146 | + """ | |
147 | + missing_dependencies = [] | |
148 | + | |
149 | + try: | |
150 | + # Check file manager service | |
151 | + file_manager_service = FileManagerService.get_instance() | |
152 | + if not file_manager_service.is_healthy(): | |
153 | + missing_dependencies.append("file_manager_service") | |
154 | + | |
155 | + # Check each component's dependencies | |
156 | + for component, deps in self.dependencies.items(): | |
157 | + component_health = self.component_health.get(component) | |
158 | + if component_health and not component_health.is_healthy: | |
159 | + continue # Skip unhealthy components | |
160 | + | |
161 | + for dep in deps: | |
162 | + if not self._check_dependency_available(dep): | |
163 | + missing_dependencies.append(f"{component} -> {dep}") | |
164 | + | |
165 | + return missing_dependencies | |
166 | + | |
167 | + except Exception as e: | |
168 | + self.logger.error(f"Error verifying dependencies: {e}") | |
169 | + return [f"dependency_check_error: {e}"] | |
170 | + | |
171 | + def get_initialization_status(self) -> Dict[str, Any]: | |
172 | + """Get detailed initialization status | |
173 | + | |
174 | + Returns: | |
175 | + Dict containing initialization status information | |
176 | + """ | |
177 | + return { | |
178 | + 'core_services_ready': self.state.file_manager_ready, | |
179 | + 'ui_components_ready': self.state.ui_components_ready, | |
180 | + 'initialization_errors': self.state.initialization_errors, | |
181 | + 'last_attempt': self.state.last_initialization_attempt, | |
182 | + 'initialization_time': self.state.initialization_time, | |
183 | + 'component_status': self.state.component_status, | |
184 | + 'missing_dependencies': self.verify_dependencies(), | |
185 | + 'component_health': { | |
186 | + name: { | |
187 | + 'healthy': health.is_healthy, | |
188 | + 'last_check': health.last_check, | |
189 | + 'error': health.error_message, | |
190 | + 'dependencies': health.dependency_status | |
191 | + } | |
192 | + for name, health in self.component_health.items() | |
193 | + } | |
194 | + } | |
195 | + | |
196 | + def register_initialization_callback(self, component: str, callback: Callable) -> None: | |
197 | + """Register a callback to be called when a component is initialized | |
198 | + | |
199 | + Args: | |
200 | + component: Component name | |
201 | + callback: Callback function to call | |
202 | + """ | |
203 | + if component not in self.initialization_callbacks: | |
204 | + self.initialization_callbacks[component] = [] | |
205 | + self.initialization_callbacks[component].append(callback) | |
206 | + | |
207 | + def _initialize_file_manager(self) -> bool: | |
208 | + """Initialize the file manager service""" | |
209 | + try: | |
210 | + start_time = time.time() | |
211 | + | |
212 | + file_manager_service = FileManagerService.get_instance() | |
213 | + | |
214 | + # Determine if we should use advanced file manager | |
215 | + use_advanced = self.config.get('performance.use_advanced_file_manager', False) | |
216 | + | |
217 | + success = file_manager_service.initialize(self.config, use_advanced) | |
218 | + | |
219 | + init_time = time.time() - start_time | |
220 | + | |
221 | + # Update component health | |
222 | + self.component_health['file_manager'] = ComponentHealth( | |
223 | + component_name='file_manager', | |
224 | + is_healthy=success and file_manager_service.is_healthy(), | |
225 | + last_check=datetime.now(), | |
226 | + error_message=None if success else "Initialization failed", | |
227 | + initialization_time=init_time | |
228 | + ) | |
229 | + | |
230 | + self.state.component_status['file_manager'] = success | |
231 | + | |
232 | + if success: | |
233 | + self.logger.info(f"File manager initialized in {init_time:.3f}s") | |
234 | + self._call_initialization_callbacks('file_manager') | |
235 | + else: | |
236 | + self.logger.error("File manager initialization failed") | |
237 | + | |
238 | + return success | |
239 | + | |
240 | + except Exception as e: | |
241 | + error_msg = f"File manager initialization error: {e}" | |
242 | + self.logger.error(error_msg) | |
243 | + self.state.initialization_errors.append(error_msg) | |
244 | + | |
245 | + self.component_health['file_manager'] = ComponentHealth( | |
246 | + component_name='file_manager', | |
247 | + is_healthy=False, | |
248 | + last_check=datetime.now(), | |
249 | + error_message=str(e) | |
250 | + ) | |
251 | + | |
252 | + return False | |
253 | + | |
254 | + def _initialize_config_services(self) -> bool: | |
255 | + """Initialize configuration-dependent services""" | |
256 | + try: | |
257 | + # Initialize performance optimizer if enabled | |
258 | + if self.config.get('performance.enable_optimizer', True): | |
259 | + if not self._initialize_performance_optimizer(): | |
260 | + self.logger.warning("Performance optimizer initialization failed") | |
261 | + | |
262 | + # Initialize other core services as needed | |
263 | + return True | |
264 | + | |
265 | + except Exception as e: | |
266 | + self.logger.error(f"Config services initialization error: {e}") | |
267 | + return False | |
268 | + | |
269 | + def _initialize_theme_manager(self) -> bool: | |
270 | + """Initialize theme manager""" | |
271 | + try: | |
272 | + start_time = time.time() | |
273 | + | |
274 | + # Import here to avoid circular imports | |
275 | + from ..ui.theme import ThemeManager | |
276 | + | |
277 | + theme_manager = ThemeManager(self.config) | |
278 | + init_time = time.time() - start_time | |
279 | + | |
280 | + self.component_health['theme_manager'] = ComponentHealth( | |
281 | + component_name='theme_manager', | |
282 | + is_healthy=True, | |
283 | + last_check=datetime.now(), | |
284 | + initialization_time=init_time, | |
285 | + dependency_status={'file_manager': True} | |
286 | + ) | |
287 | + | |
288 | + self.state.component_status['theme_manager'] = True | |
289 | + self.logger.info(f"Theme manager initialized in {init_time:.3f}s") | |
290 | + self._call_initialization_callbacks('theme_manager') | |
291 | + | |
292 | + return True | |
293 | + | |
294 | + except Exception as e: | |
295 | + error_msg = f"Theme manager initialization error: {e}" | |
296 | + self.logger.error(error_msg) | |
297 | + self.state.initialization_errors.append(error_msg) | |
298 | + | |
299 | + self.component_health['theme_manager'] = ComponentHealth( | |
300 | + component_name='theme_manager', | |
301 | + is_healthy=False, | |
302 | + last_check=datetime.now(), | |
303 | + error_message=str(e) | |
304 | + ) | |
305 | + | |
306 | + return False | |
307 | + | |
308 | + def _initialize_panel_manager(self) -> bool: | |
309 | + """Initialize panel manager""" | |
310 | + try: | |
311 | + start_time = time.time() | |
312 | + | |
313 | + # Import here to avoid circular imports | |
314 | + from ..ui.panels import PanelManager | |
315 | + from ..ui.theme import ThemeManager | |
316 | + | |
317 | + # Get theme manager (should be initialized by now) | |
318 | + theme_manager = ThemeManager(self.config) | |
319 | + panel_manager = PanelManager(theme_manager) | |
320 | + | |
321 | + init_time = time.time() - start_time | |
322 | + | |
323 | + self.component_health['panel_manager'] = ComponentHealth( | |
324 | + component_name='panel_manager', | |
325 | + is_healthy=True, | |
326 | + last_check=datetime.now(), | |
327 | + initialization_time=init_time, | |
328 | + dependency_status={ | |
329 | + 'file_manager': True, | |
330 | + 'theme_manager': True | |
331 | + } | |
332 | + ) | |
333 | + | |
334 | + self.state.component_status['panel_manager'] = True | |
335 | + self.logger.info(f"Panel manager initialized in {init_time:.3f}s") | |
336 | + self._call_initialization_callbacks('panel_manager') | |
337 | + | |
338 | + return True | |
339 | + | |
340 | + except Exception as e: | |
341 | + error_msg = f"Panel manager initialization error: {e}" | |
342 | + self.logger.error(error_msg) | |
343 | + self.state.initialization_errors.append(error_msg) | |
344 | + | |
345 | + self.component_health['panel_manager'] = ComponentHealth( | |
346 | + component_name='panel_manager', | |
347 | + is_healthy=False, | |
348 | + last_check=datetime.now(), | |
349 | + error_message=str(e) | |
350 | + ) | |
351 | + | |
352 | + return False | |
353 | + | |
354 | + def _initialize_media_viewer(self) -> bool: | |
355 | + """Initialize media viewer""" | |
356 | + try: | |
357 | + start_time = time.time() | |
358 | + | |
359 | + # Import here to avoid circular imports | |
360 | + from ..ui.image_viewer import MediaViewer | |
361 | + from rich.console import Console | |
362 | + | |
363 | + console = Console() | |
364 | + media_viewer = MediaViewer(console) | |
365 | + | |
366 | + init_time = time.time() - start_time | |
367 | + | |
368 | + self.component_health['media_viewer'] = ComponentHealth( | |
369 | + component_name='media_viewer', | |
370 | + is_healthy=True, | |
371 | + last_check=datetime.now(), | |
372 | + initialization_time=init_time, | |
373 | + dependency_status={'file_manager': True} | |
374 | + ) | |
375 | + | |
376 | + self.state.component_status['media_viewer'] = True | |
377 | + self.logger.info(f"Media viewer initialized in {init_time:.3f}s") | |
378 | + self._call_initialization_callbacks('media_viewer') | |
379 | + | |
380 | + return True | |
381 | + | |
382 | + except Exception as e: | |
383 | + error_msg = f"Media viewer initialization error: {e}" | |
384 | + self.logger.error(error_msg) | |
385 | + self.state.initialization_errors.append(error_msg) | |
386 | + | |
387 | + self.component_health['media_viewer'] = ComponentHealth( | |
388 | + component_name='media_viewer', | |
389 | + is_healthy=False, | |
390 | + last_check=datetime.now(), | |
391 | + error_message=str(e) | |
392 | + ) | |
393 | + | |
394 | + return False | |
395 | + | |
396 | + def _initialize_performance_optimizer(self) -> bool: | |
397 | + """Initialize performance optimizer""" | |
398 | + try: | |
399 | + start_time = time.time() | |
400 | + | |
401 | + # Import here to avoid circular imports | |
402 | + from ..core.performance_optimizer import PerformanceOptimizer, PerformanceConfig | |
403 | + | |
404 | + perf_config = PerformanceConfig( | |
405 | + max_concurrent_operations=self.config.get('performance.max_concurrent_operations', 10), | |
406 | + chunk_size=self.config.get('performance.chunk_size', 100), | |
407 | + memory_threshold_mb=self.config.get('performance.memory_threshold_mb', 500), | |
408 | + lazy_loading_threshold=self.config.get('performance.lazy_loading_threshold', 1000), | |
409 | + background_processing=self.config.get('performance.background_processing', True), | |
410 | + use_threading=self.config.get('performance.use_threading', True), | |
411 | + max_worker_threads=self.config.get('performance.max_worker_threads', 4) | |
412 | + ) | |
413 | + | |
414 | + performance_optimizer = PerformanceOptimizer(perf_config) | |
415 | + init_time = time.time() - start_time | |
416 | + | |
417 | + self.component_health['performance_optimizer'] = ComponentHealth( | |
418 | + component_name='performance_optimizer', | |
419 | + is_healthy=True, | |
420 | + last_check=datetime.now(), | |
421 | + initialization_time=init_time | |
422 | + ) | |
423 | + | |
424 | + self.state.component_status['performance_optimizer'] = True | |
425 | + self.logger.info(f"Performance optimizer initialized in {init_time:.3f}s") | |
426 | + self._call_initialization_callbacks('performance_optimizer') | |
427 | + | |
428 | + return True | |
429 | + | |
430 | + except Exception as e: | |
431 | + error_msg = f"Performance optimizer initialization error: {e}" | |
432 | + self.logger.error(error_msg) | |
433 | + self.state.initialization_errors.append(error_msg) | |
434 | + | |
435 | + self.component_health['performance_optimizer'] = ComponentHealth( | |
436 | + component_name='performance_optimizer', | |
437 | + is_healthy=False, | |
438 | + last_check=datetime.now(), | |
439 | + error_message=str(e) | |
440 | + ) | |
441 | + | |
442 | + return False | |
443 | + | |
444 | + def _initialize_other_ui_components(self) -> bool: | |
445 | + """Initialize other UI components""" | |
446 | + try: | |
447 | + # Initialize hotkey manager | |
448 | + if not self._initialize_hotkey_manager(): | |
449 | + self.logger.warning("Hotkey manager initialization failed") | |
450 | + | |
451 | + # Initialize AI assistant if enabled | |
452 | + if self.config.get('ai.enable_assistant', False): | |
453 | + if not self._initialize_ai_assistant(): | |
454 | + self.logger.warning("AI assistant initialization failed") | |
455 | + | |
456 | + return True | |
457 | + | |
458 | + except Exception as e: | |
459 | + self.logger.error(f"Other UI components initialization error: {e}") | |
460 | + return False | |
461 | + | |
462 | + def _initialize_hotkey_manager(self) -> bool: | |
463 | + """Initialize hotkey manager""" | |
464 | + try: | |
465 | + start_time = time.time() | |
466 | + | |
467 | + # Import here to avoid circular imports | |
468 | + from ..utils.hotkeys import HotkeyManager | |
469 | + | |
470 | + hotkey_manager = HotkeyManager() | |
471 | + init_time = time.time() - start_time | |
472 | + | |
473 | + self.component_health['hotkey_manager'] = ComponentHealth( | |
474 | + component_name='hotkey_manager', | |
475 | + is_healthy=True, | |
476 | + last_check=datetime.now(), | |
477 | + initialization_time=init_time | |
478 | + ) | |
479 | + | |
480 | + self.state.component_status['hotkey_manager'] = True | |
481 | + self.logger.info(f"Hotkey manager initialized in {init_time:.3f}s") | |
482 | + self._call_initialization_callbacks('hotkey_manager') | |
483 | + | |
484 | + return True | |
485 | + | |
486 | + except Exception as e: | |
487 | + error_msg = f"Hotkey manager initialization error: {e}" | |
488 | + self.logger.error(error_msg) | |
489 | + | |
490 | + self.component_health['hotkey_manager'] = ComponentHealth( | |
491 | + component_name='hotkey_manager', | |
492 | + is_healthy=False, | |
493 | + last_check=datetime.now(), | |
494 | + error_message=str(e) | |
495 | + ) | |
496 | + | |
497 | + return False | |
498 | + | |
499 | + def _initialize_ai_assistant(self) -> bool: | |
500 | + """Initialize AI assistant""" | |
501 | + try: | |
502 | + start_time = time.time() | |
503 | + | |
504 | + # Import here to avoid circular imports | |
505 | + from ..ai.assistant import AdvancedAIAssistant | |
506 | + | |
507 | + ai_assistant = AdvancedAIAssistant(self.config) | |
508 | + init_time = time.time() - start_time | |
509 | + | |
510 | + self.component_health['ai_assistant'] = ComponentHealth( | |
511 | + component_name='ai_assistant', | |
512 | + is_healthy=True, | |
513 | + last_check=datetime.now(), | |
514 | + initialization_time=init_time, | |
515 | + dependency_status={'file_manager': True} | |
516 | + ) | |
517 | + | |
518 | + self.state.component_status['ai_assistant'] = True | |
519 | + self.logger.info(f"AI assistant initialized in {init_time:.3f}s") | |
520 | + self._call_initialization_callbacks('ai_assistant') | |
521 | + | |
522 | + return True | |
523 | + | |
524 | + except Exception as e: | |
525 | + error_msg = f"AI assistant initialization error: {e}" | |
526 | + self.logger.error(error_msg) | |
527 | + | |
528 | + self.component_health['ai_assistant'] = ComponentHealth( | |
529 | + component_name='ai_assistant', | |
530 | + is_healthy=False, | |
531 | + last_check=datetime.now(), | |
532 | + error_message=str(e) | |
533 | + ) | |
534 | + | |
535 | + return False | |
536 | + | |
537 | + def _check_dependency_available(self, dependency: str) -> bool: | |
538 | + """Check if a dependency is available and healthy""" | |
539 | + if dependency == 'file_manager': | |
540 | + file_manager_service = FileManagerService.get_instance() | |
541 | + return file_manager_service.is_healthy() | |
542 | + | |
543 | + component_health = self.component_health.get(dependency) | |
544 | + return component_health is not None and component_health.is_healthy | |
545 | + | |
546 | + def _call_initialization_callbacks(self, component: str) -> None: | |
547 | + """Call initialization callbacks for a component""" | |
548 | + callbacks = self.initialization_callbacks.get(component, []) | |
549 | + for callback in callbacks: | |
550 | + try: | |
551 | + callback() | |
552 | + except Exception as e: | |
553 | + self.logger.error(f"Error in initialization callback for {component}: {e}") | |
554 | + | |
555 | + def health_check(self) -> bool: | |
556 | + """Perform a comprehensive health check of all components | |
557 | + | |
558 | + Returns: | |
559 | + bool: True if all components are healthy | |
560 | + """ | |
561 | + try: | |
562 | + all_healthy = True | |
563 | + | |
564 | + # Check file manager service | |
565 | + file_manager_service = FileManagerService.get_instance() | |
566 | + if not file_manager_service.is_healthy(): | |
567 | + all_healthy = False | |
568 | + self.logger.warning("File manager service is not healthy") | |
569 | + | |
570 | + # Check each component | |
571 | + for component_name, health in self.component_health.items(): | |
572 | + # Update health status | |
573 | + try: | |
574 | + # Perform component-specific health checks here | |
575 | + health.last_check = datetime.now() | |
576 | + # For now, assume components remain healthy once initialized | |
577 | + # In a real implementation, you'd add specific health checks | |
578 | + | |
579 | + except Exception as e: | |
580 | + health.is_healthy = False | |
581 | + health.error_message = str(e) | |
582 | + all_healthy = False | |
583 | + self.logger.warning(f"Component {component_name} health check failed: {e}") | |
584 | + | |
585 | + return all_healthy | |
586 | + | |
587 | + except Exception as e: | |
588 | + self.logger.error(f"Health check error: {e}") | |
589 | + return False | |
590 | + | |
591 | + def cleanup(self) -> None: | |
592 | + """Clean up resources and reset state""" | |
593 | + try: | |
594 | + self.logger.info("Cleaning up component initializer") | |
595 | + | |
596 | + # Clear callbacks | |
597 | + self.initialization_callbacks.clear() | |
598 | + | |
599 | + # Reset state | |
600 | + self.state = InitializationState() | |
601 | + self.component_health.clear() | |
602 | + | |
603 | + except Exception as e: | |
604 | + self.logger.error(f"Cleanup error: {e}") | |
0 | 605 | |
βΌlaxyfile/core/error_handling_mixin.py
+558-0
| @@ -0,0 +1,558 @@ | ||
1 | +""" | |
2 | +ErrorHandlingMixin - Provides consistent error handling across all components | |
3 | + | |
4 | +This mixin provides standardized error handling, recovery mechanisms, and fallback | |
5 | +behaviors for components that interact with the file manager. | |
6 | +""" | |
7 | + | |
8 | +import time | |
9 | +import functools | |
10 | +from typing import Any, Optional, Callable, Dict, List, Union | |
11 | +from pathlib import Path | |
12 | +from datetime import datetime, timedelta | |
13 | + | |
14 | +from .file_manager_service import FileManagerService | |
15 | +from ..utils.logger import Logger | |
16 | + | |
17 | + | |
18 | +class ErrorHandlingMixin: | |
19 | + """Mixin providing consistent error handling across all components""" | |
20 | + | |
21 | + def __init__(self): | |
22 | + """Initialize error handling mixin""" | |
23 | + if not hasattr(self, 'logger'): | |
24 | + self.logger = Logger() | |
25 | + | |
26 | + # Error tracking | |
27 | + self._error_count = 0 | |
28 | + self._last_error_time: Optional[datetime] = None | |
29 | + self._error_history: List[Dict[str, Any]] = [] | |
30 | + self._max_error_history = 50 | |
31 | + | |
32 | + # Recovery settings | |
33 | + self._max_retry_attempts = 3 | |
34 | + self._retry_delay_base = 1.0 # Base delay in seconds | |
35 | + self._recovery_timeout = 30.0 # Max time to spend on recovery | |
36 | + self._exponential_backoff_max = 10.0 # Max backoff delay | |
37 | + | |
38 | + # Automatic recovery settings | |
39 | + self._auto_recovery_enabled = True | |
40 | + self._recovery_attempts = 0 | |
41 | + self._max_recovery_attempts = 5 | |
42 | + self._last_recovery_attempt: Optional[datetime] = None | |
43 | + self._recovery_cooldown = timedelta(minutes=1) # Wait between recovery attempts | |
44 | + | |
45 | + # User notification callbacks | |
46 | + self._notification_callbacks: List[Callable] = [] | |
47 | + self._recovery_status_callbacks: List[Callable] = [] | |
48 | + | |
49 | + # Fallback cache | |
50 | + self._fallback_cache: Dict[str, Any] = {} | |
51 | + self._cache_ttl = timedelta(minutes=5) | |
52 | + | |
53 | + def safe_file_operation(self, operation: Callable, *args, | |
54 | + fallback_value: Any = None, | |
55 | + cache_key: Optional[str] = None, | |
56 | + max_retries: Optional[int] = None, | |
57 | + **kwargs) -> Optional[Any]: | |
58 | + """Execute file operations with comprehensive error handling | |
59 | + | |
60 | + Args: | |
61 | + operation: The operation function to execute | |
62 | + *args: Arguments for the operation | |
63 | + fallback_value: Value to return if operation fails | |
64 | + cache_key: Key for caching results/fallbacks | |
65 | + max_retries: Maximum retry attempts (overrides default) | |
66 | + **kwargs: Keyword arguments for the operation | |
67 | + | |
68 | + Returns: | |
69 | + Operation result or fallback value on error | |
70 | + """ | |
71 | + operation_name = getattr(operation, '__name__', str(operation)) | |
72 | + max_retries = max_retries or self._max_retry_attempts | |
73 | + | |
74 | + # Check cache first if cache_key provided | |
75 | + if cache_key: | |
76 | + cached_result = self._get_cached_result(cache_key) | |
77 | + if cached_result is not None: | |
78 | + return cached_result | |
79 | + | |
80 | + for attempt in range(max_retries + 1): | |
81 | + try: | |
82 | + start_time = time.time() | |
83 | + | |
84 | + # Execute the operation | |
85 | + result = operation(*args, **kwargs) | |
86 | + | |
87 | + # Cache successful result | |
88 | + if cache_key and result is not None: | |
89 | + self._cache_result(cache_key, result) | |
90 | + | |
91 | + # Log successful recovery if this was a retry | |
92 | + if attempt > 0: | |
93 | + self.logger.info(f"Operation {operation_name} succeeded on attempt {attempt + 1}") | |
94 | + | |
95 | + return result | |
96 | + | |
97 | + except Exception as e: | |
98 | + error_info = { | |
99 | + 'operation': operation_name, | |
100 | + 'attempt': attempt + 1, | |
101 | + 'error': str(e), | |
102 | + 'error_type': type(e).__name__, | |
103 | + 'timestamp': datetime.now(), | |
104 | + 'args': str(args)[:100], # Truncate for logging | |
105 | + 'kwargs': str(kwargs)[:100] | |
106 | + } | |
107 | + | |
108 | + self._record_error(error_info) | |
109 | + | |
110 | + # If this is the last attempt, handle final failure | |
111 | + if attempt >= max_retries: | |
112 | + return self._handle_final_failure( | |
113 | + operation_name, e, fallback_value, cache_key | |
114 | + ) | |
115 | + | |
116 | + # Attempt recovery before retry | |
117 | + if not self._attempt_recovery(operation_name, e, attempt): | |
118 | + # Recovery failed, try fallback | |
119 | + return self._handle_final_failure( | |
120 | + operation_name, e, fallback_value, cache_key | |
121 | + ) | |
122 | + | |
123 | + # Wait before retry with exponential backoff | |
124 | + retry_delay = self._retry_delay_base * (2 ** attempt) | |
125 | + time.sleep(min(retry_delay, 5.0)) # Cap at 5 seconds | |
126 | + | |
127 | + return fallback_value | |
128 | + | |
129 | + def handle_file_manager_error(self, error: Exception, context: str = "") -> None: | |
130 | + """Standardized file manager error handling | |
131 | + | |
132 | + Args: | |
133 | + error: The exception that occurred | |
134 | + context: Additional context about where the error occurred | |
135 | + """ | |
136 | + error_type = type(error).__name__ | |
137 | + error_msg = str(error) | |
138 | + | |
139 | + # Log the error with context | |
140 | + if context: | |
141 | + self.logger.error(f"File manager error in {context}: {error_type}: {error_msg}") | |
142 | + else: | |
143 | + self.logger.error(f"File manager error: {error_type}: {error_msg}") | |
144 | + | |
145 | + # Check if this is a critical file manager error | |
146 | + critical_errors = [ | |
147 | + 'AttributeError', # Usually 'NoneType' object has no attribute | |
148 | + 'TypeError', # File manager is None | |
149 | + 'ImportError', # Module loading issues | |
150 | + ] | |
151 | + | |
152 | + if error_type in critical_errors and 'NoneType' in error_msg: | |
153 | + self.logger.critical("Critical file manager error detected - attempting recovery") | |
154 | + self._attempt_file_manager_recovery() | |
155 | + | |
156 | + # Update error statistics | |
157 | + self._error_count += 1 | |
158 | + self._last_error_time = datetime.now() | |
159 | + | |
160 | + def get_fallback_behavior(self, operation_type: str, **kwargs) -> Any: | |
161 | + """Provide fallback behavior when file manager is unavailable | |
162 | + | |
163 | + Args: | |
164 | + operation_type: Type of operation that failed | |
165 | + **kwargs: Additional context for fallback behavior | |
166 | + | |
167 | + Returns: | |
168 | + Appropriate fallback value or behavior | |
169 | + """ | |
170 | + fallback_behaviors = { | |
171 | + 'list_directory': self._fallback_list_directory, | |
172 | + 'get_file_info': self._fallback_get_file_info, | |
173 | + 'file_operations': self._fallback_file_operations, | |
174 | + 'preview': self._fallback_preview, | |
175 | + 'search': self._fallback_search | |
176 | + } | |
177 | + | |
178 | + fallback_func = fallback_behaviors.get(operation_type) | |
179 | + if fallback_func: | |
180 | + try: | |
181 | + return fallback_func(**kwargs) | |
182 | + except Exception as e: | |
183 | + self.logger.error(f"Fallback behavior failed for {operation_type}: {e}") | |
184 | + | |
185 | + return self._get_generic_fallback(operation_type) | |
186 | + | |
187 | + def is_file_manager_available(self) -> bool: | |
188 | + """Check if file manager is available and healthy | |
189 | + | |
190 | + Returns: | |
191 | + bool: True if file manager is available and working | |
192 | + """ | |
193 | + try: | |
194 | + service = FileManagerService.get_instance() | |
195 | + return service.is_healthy() | |
196 | + except Exception as e: | |
197 | + self.logger.error(f"Error checking file manager availability: {e}") | |
198 | + return False | |
199 | + | |
200 | + def ensure_file_manager_available(self) -> bool: | |
201 | + """Ensure file manager is available, attempt recovery if needed | |
202 | + | |
203 | + Returns: | |
204 | + bool: True if file manager is now available | |
205 | + """ | |
206 | + if self.is_file_manager_available(): | |
207 | + return True | |
208 | + | |
209 | + self.logger.warning("File manager not available, attempting recovery") | |
210 | + return self._attempt_file_manager_recovery() | |
211 | + | |
212 | + def get_error_statistics(self) -> Dict[str, Any]: | |
213 | + """Get error statistics for monitoring and debugging | |
214 | + | |
215 | + Returns: | |
216 | + Dict containing error statistics | |
217 | + """ | |
218 | + return { | |
219 | + 'total_errors': self._error_count, | |
220 | + 'last_error_time': self._last_error_time, | |
221 | + 'recent_errors': self._error_history[-10:], # Last 10 errors | |
222 | + 'error_rate': self._calculate_error_rate(), | |
223 | + 'file_manager_available': self.is_file_manager_available() | |
224 | + } | |
225 | + | |
226 | + def clear_error_history(self) -> None: | |
227 | + """Clear error history and reset statistics""" | |
228 | + self._error_count = 0 | |
229 | + self._last_error_time = None | |
230 | + self._error_history.clear() | |
231 | + self._fallback_cache.clear() | |
232 | + self._recovery_attempts = 0 | |
233 | + self._last_recovery_attempt = None | |
234 | + self.logger.info("Error history cleared") | |
235 | + | |
236 | + def register_notification_callback(self, callback: Callable[[str, str], None]) -> None: | |
237 | + """Register a callback for user notifications | |
238 | + | |
239 | + Args: | |
240 | + callback: Function that takes (message, level) parameters | |
241 | + """ | |
242 | + if callback not in self._notification_callbacks: | |
243 | + self._notification_callbacks.append(callback) | |
244 | + | |
245 | + def register_recovery_status_callback(self, callback: Callable[[Dict[str, Any]], None]) -> None: | |
246 | + """Register a callback for recovery status updates | |
247 | + | |
248 | + Args: | |
249 | + callback: Function that takes recovery status dict | |
250 | + """ | |
251 | + if callback not in self._recovery_status_callbacks: | |
252 | + self._recovery_status_callbacks.append(callback) | |
253 | + | |
254 | + def enable_auto_recovery(self, enabled: bool = True) -> None: | |
255 | + """Enable or disable automatic recovery | |
256 | + | |
257 | + Args: | |
258 | + enabled: Whether to enable automatic recovery | |
259 | + """ | |
260 | + self._auto_recovery_enabled = enabled | |
261 | + self.logger.info(f"Automatic recovery {'enabled' if enabled else 'disabled'}") | |
262 | + | |
263 | + def trigger_manual_recovery(self) -> bool: | |
264 | + """Manually trigger recovery process | |
265 | + | |
266 | + Returns: | |
267 | + bool: True if recovery was successful | |
268 | + """ | |
269 | + self.logger.info("Manual recovery triggered") | |
270 | + self._notify_users("Attempting manual recovery...", "info") | |
271 | + | |
272 | + success = self._attempt_file_manager_recovery_with_backoff() | |
273 | + | |
274 | + if success: | |
275 | + self._notify_users("Recovery successful!", "success") | |
276 | + else: | |
277 | + self._notify_users("Recovery failed. Please restart the application.", "error") | |
278 | + | |
279 | + return success | |
280 | + | |
281 | + def _record_error(self, error_info: Dict[str, Any]) -> None: | |
282 | + """Record error information for tracking and analysis""" | |
283 | + self._error_history.append(error_info) | |
284 | + | |
285 | + # Limit history size | |
286 | + if len(self._error_history) > self._max_error_history: | |
287 | + self._error_history = self._error_history[-self._max_error_history:] | |
288 | + | |
289 | + self._error_count += 1 | |
290 | + self._last_error_time = error_info['timestamp'] | |
291 | + | |
292 | + def _attempt_recovery(self, operation_name: str, error: Exception, attempt: int) -> bool: | |
293 | + """Attempt to recover from an error | |
294 | + | |
295 | + Args: | |
296 | + operation_name: Name of the failed operation | |
297 | + error: The exception that occurred | |
298 | + attempt: Current attempt number | |
299 | + | |
300 | + Returns: | |
301 | + bool: True if recovery was successful | |
302 | + """ | |
303 | + try: | |
304 | + self.logger.info(f"Attempting recovery for {operation_name} (attempt {attempt + 1})") | |
305 | + | |
306 | + # Check if this is a file manager related error | |
307 | + if self._is_file_manager_error(error): | |
308 | + return self._attempt_file_manager_recovery() | |
309 | + | |
310 | + # Generic recovery attempts | |
311 | + time.sleep(0.1) # Brief pause | |
312 | + return True | |
313 | + | |
314 | + except Exception as recovery_error: | |
315 | + self.logger.error(f"Recovery attempt failed: {recovery_error}") | |
316 | + return False | |
317 | + | |
318 | + def _attempt_file_manager_recovery(self) -> bool: | |
319 | + """Attempt to recover the file manager service | |
320 | + | |
321 | + Returns: | |
322 | + bool: True if recovery was successful | |
323 | + """ | |
324 | + try: | |
325 | + self.logger.info("Attempting file manager recovery") | |
326 | + | |
327 | + service = FileManagerService.get_instance() | |
328 | + | |
329 | + # Try reinitialization | |
330 | + if service.reinitialize(force=True): | |
331 | + self.logger.info("File manager recovery successful") | |
332 | + return True | |
333 | + | |
334 | + # If reinitialize failed, try getting a new instance | |
335 | + FileManagerService.reset() | |
336 | + new_service = FileManagerService.get_instance() | |
337 | + | |
338 | + if new_service.ensure_initialized(): | |
339 | + self.logger.info("File manager recovery successful with new instance") | |
340 | + return True | |
341 | + | |
342 | + self.logger.error("File manager recovery failed") | |
343 | + return False | |
344 | + | |
345 | + except Exception as e: | |
346 | + self.logger.error(f"File manager recovery error: {e}") | |
347 | + return False | |
348 | + | |
349 | + def _handle_final_failure(self, operation_name: str, error: Exception, | |
350 | + fallback_value: Any, cache_key: Optional[str]) -> Any: | |
351 | + """Handle final failure after all retries exhausted | |
352 | + | |
353 | + Args: | |
354 | + operation_name: Name of the failed operation | |
355 | + error: The final exception | |
356 | + fallback_value: Fallback value to return | |
357 | + cache_key: Cache key for storing fallback | |
358 | + | |
359 | + Returns: | |
360 | + Fallback value or cached result | |
361 | + """ | |
362 | + self.handle_file_manager_error(error, operation_name) | |
363 | + | |
364 | + # Try to get a cached fallback | |
365 | + if cache_key: | |
366 | + cached_fallback = self._get_cached_fallback(cache_key) | |
367 | + if cached_fallback is not None: | |
368 | + self.logger.info(f"Using cached fallback for {operation_name}") | |
369 | + return cached_fallback | |
370 | + | |
371 | + # Use provided fallback or generate one | |
372 | + if fallback_value is not None: | |
373 | + result = fallback_value | |
374 | + else: | |
375 | + result = self.get_fallback_behavior(operation_name) | |
376 | + | |
377 | + # Cache the fallback for future use | |
378 | + if cache_key and result is not None: | |
379 | + self._cache_fallback(cache_key, result) | |
380 | + | |
381 | + return result | |
382 | + | |
383 | + def _is_file_manager_error(self, error: Exception) -> bool: | |
384 | + """Check if an error is related to file manager issues""" | |
385 | + error_msg = str(error).lower() | |
386 | + error_type = type(error).__name__ | |
387 | + | |
388 | + file_manager_indicators = [ | |
389 | + 'nonetype', | |
390 | + 'file_manager', | |
391 | + 'list_directory', | |
392 | + 'has no attribute', | |
393 | + 'not initialized' | |
394 | + ] | |
395 | + | |
396 | + return (error_type in ['AttributeError', 'TypeError'] or | |
397 | + any(indicator in error_msg for indicator in file_manager_indicators)) | |
398 | + | |
399 | + def _get_cached_result(self, cache_key: str) -> Optional[Any]: | |
400 | + """Get cached result if available and not expired""" | |
401 | + if cache_key not in self._fallback_cache: | |
402 | + return None | |
403 | + | |
404 | + cached_data = self._fallback_cache[cache_key] | |
405 | + if datetime.now() - cached_data['timestamp'] > self._cache_ttl: | |
406 | + del self._fallback_cache[cache_key] | |
407 | + return None | |
408 | + | |
409 | + return cached_data['value'] | |
410 | + | |
411 | + def _cache_result(self, cache_key: str, result: Any) -> None: | |
412 | + """Cache a successful result""" | |
413 | + self._fallback_cache[cache_key] = { | |
414 | + 'value': result, | |
415 | + 'timestamp': datetime.now(), | |
416 | + 'type': 'result' | |
417 | + } | |
418 | + | |
419 | + def _get_cached_fallback(self, cache_key: str) -> Optional[Any]: | |
420 | + """Get cached fallback if available""" | |
421 | + fallback_key = f"fallback_{cache_key}" | |
422 | + return self._get_cached_result(fallback_key) | |
423 | + | |
424 | + def _cache_fallback(self, cache_key: str, fallback: Any) -> None: | |
425 | + """Cache a fallback value""" | |
426 | + fallback_key = f"fallback_{cache_key}" | |
427 | + self._fallback_cache[fallback_key] = { | |
428 | + 'value': fallback, | |
429 | + 'timestamp': datetime.now(), | |
430 | + 'type': 'fallback' | |
431 | + } | |
432 | + | |
433 | + def _calculate_error_rate(self) -> float: | |
434 | + """Calculate recent error rate""" | |
435 | + if not self._error_history: | |
436 | + return 0.0 | |
437 | + | |
438 | + # Calculate errors in the last hour | |
439 | + one_hour_ago = datetime.now() - timedelta(hours=1) | |
440 | + recent_errors = [ | |
441 | + error for error in self._error_history | |
442 | + if error['timestamp'] > one_hour_ago | |
443 | + ] | |
444 | + | |
445 | + return len(recent_errors) / 60.0 # Errors per minute | |
446 | + | |
447 | + # Fallback behavior implementations | |
448 | + | |
449 | + def _fallback_list_directory(self, path: Optional[Path] = None, **kwargs) -> List[Any]: | |
450 | + """Fallback behavior for directory listing""" | |
451 | + if path is None: | |
452 | + path = Path.cwd() | |
453 | + | |
454 | + try: | |
455 | + # Try basic Python directory listing as fallback | |
456 | + items = [] | |
457 | + if path.exists() and path.is_dir(): | |
458 | + for item in path.iterdir(): | |
459 | + # Create minimal file info | |
460 | + items.append({ | |
461 | + 'name': item.name, | |
462 | + 'path': item, | |
463 | + 'is_dir': item.is_dir(), | |
464 | + 'is_file': item.is_file(), | |
465 | + 'size': item.stat().st_size if item.is_file() else 0, | |
466 | + 'modified': item.stat().st_mtime | |
467 | + }) | |
468 | + return items | |
469 | + except Exception as e: | |
470 | + self.logger.error(f"Fallback directory listing failed: {e}") | |
471 | + return [] | |
472 | + | |
473 | + def _fallback_get_file_info(self, path: Optional[Path] = None, **kwargs) -> Optional[Dict[str, Any]]: | |
474 | + """Fallback behavior for getting file information""" | |
475 | + if path is None: | |
476 | + return None | |
477 | + | |
478 | + try: | |
479 | + if path.exists(): | |
480 | + stat_info = path.stat() | |
481 | + return { | |
482 | + 'name': path.name, | |
483 | + 'path': path, | |
484 | + 'is_dir': path.is_dir(), | |
485 | + 'is_file': path.is_file(), | |
486 | + 'size': stat_info.st_size, | |
487 | + 'modified': stat_info.st_mtime, | |
488 | + 'file_type': 'directory' if path.is_dir() else 'file' | |
489 | + } | |
490 | + except Exception as e: | |
491 | + self.logger.error(f"Fallback file info failed: {e}") | |
492 | + | |
493 | + return None | |
494 | + | |
495 | + def _fallback_file_operations(self, **kwargs) -> bool: | |
496 | + """Fallback behavior for file operations""" | |
497 | + self.logger.warning("File operations not available - file manager unavailable") | |
498 | + return False | |
499 | + | |
500 | + def _fallback_preview(self, **kwargs) -> str: | |
501 | + """Fallback behavior for file preview""" | |
502 | + return "Preview not available - file manager unavailable" | |
503 | + | |
504 | + def _fallback_search(self, **kwargs) -> List[Any]: | |
505 | + """Fallback behavior for file search""" | |
506 | + return [] | |
507 | + | |
508 | + def _get_generic_fallback(self, operation_type: str) -> Any: | |
509 | + """Get generic fallback for unknown operation types""" | |
510 | + fallback_map = { | |
511 | + 'list': [], | |
512 | + 'get': None, | |
513 | + 'search': [], | |
514 | + 'operation': False, | |
515 | + 'preview': "Not available", | |
516 | + 'info': None | |
517 | + } | |
518 | + | |
519 | + # Try to match operation type to fallback | |
520 | + for key, value in fallback_map.items(): | |
521 | + if key in operation_type.lower(): | |
522 | + return value | |
523 | + | |
524 | + return None | |
525 | + | |
526 | + | |
527 | +def safe_file_operation(fallback_value: Any = None, cache_key: Optional[str] = None, | |
528 | + max_retries: int = 3): | |
529 | + """Decorator for safe file operations with error handling | |
530 | + | |
531 | + Args: | |
532 | + fallback_value: Value to return on failure | |
533 | + cache_key: Key for caching results | |
534 | + max_retries: Maximum retry attempts | |
535 | + """ | |
536 | + def decorator(func: Callable) -> Callable: | |
537 | + @functools.wraps(func) | |
538 | + def wrapper(self, *args, **kwargs): | |
539 | + if not isinstance(self, ErrorHandlingMixin): | |
540 | + # If not using mixin, create temporary error handler | |
541 | + error_handler = ErrorHandlingMixin() | |
542 | + return error_handler.safe_file_operation( | |
543 | + func, self, *args, | |
544 | + fallback_value=fallback_value, | |
545 | + cache_key=cache_key, | |
546 | + max_retries=max_retries, | |
547 | + **kwargs | |
548 | + ) | |
549 | + else: | |
550 | + return self.safe_file_operation( | |
551 | + func, self, *args, | |
552 | + fallback_value=fallback_value, | |
553 | + cache_key=cache_key, | |
554 | + max_retries=max_retries, | |
555 | + **kwargs | |
556 | + ) | |
557 | + return wrapper | |
558 | + return decorator | |
0 | 559 | |
βΌlaxyfile/core/file_manager_service.py
+312-0
| @@ -0,0 +1,312 @@ | ||
1 | +""" | |
2 | +FileManagerService - Thread-safe singleton service for file management | |
3 | + | |
4 | +This module provides a centralized, thread-safe file manager service that ensures | |
5 | +the file manager is always available when needed and handles initialization errors gracefully. | |
6 | +""" | |
7 | + | |
8 | +import threading | |
9 | +import time | |
10 | +from pathlib import Path | |
11 | +from typing import Optional, Dict, Any, List | |
12 | +from datetime import datetime | |
13 | + | |
14 | +from .file_manager import FileManager | |
15 | +from .advanced_file_manager import AdvancedFileManager | |
16 | +from .config import Config | |
17 | +from ..utils.logger import Logger | |
18 | + | |
19 | + | |
20 | +class FileManagerService: | |
21 | + """Thread-safe singleton service for file management""" | |
22 | + | |
23 | + _instance: Optional['FileManagerService'] = None | |
24 | + _lock = threading.Lock() | |
25 | + _initialized = False | |
26 | + | |
27 | + def __init__(self): | |
28 | + """Private constructor - use get_instance() instead""" | |
29 | + if FileManagerService._instance is not None: | |
30 | + raise RuntimeError("Use FileManagerService.get_instance() instead of direct instantiation") | |
31 | + | |
32 | + self.logger = Logger() | |
33 | + self._file_manager: Optional[FileManager] = None | |
34 | + self._advanced_file_manager: Optional[AdvancedFileManager] = None | |
35 | + self._config: Optional[Config] = None | |
36 | + self._initialization_lock = threading.Lock() | |
37 | + self._initialization_attempts = 0 | |
38 | + self._last_initialization_attempt: Optional[datetime] = None | |
39 | + self._initialization_error: Optional[str] = None | |
40 | + self._use_advanced = False | |
41 | + | |
42 | + @classmethod | |
43 | + def get_instance(cls) -> 'FileManagerService': | |
44 | + """Get the singleton instance with thread-safe lazy initialization""" | |
45 | + if cls._instance is None: | |
46 | + with cls._lock: | |
47 | + if cls._instance is None: | |
48 | + cls._instance = cls() | |
49 | + return cls._instance | |
50 | + | |
51 | + def initialize(self, config: Optional[Config] = None, use_advanced: bool = False) -> bool: | |
52 | + """Initialize the file manager service | |
53 | + | |
54 | + Args: | |
55 | + config: Configuration object (optional) | |
56 | + use_advanced: Whether to use AdvancedFileManager (default: False) | |
57 | + | |
58 | + Returns: | |
59 | + bool: True if initialization successful, False otherwise | |
60 | + """ | |
61 | + with self._initialization_lock: | |
62 | + try: | |
63 | + self._config = config or Config() | |
64 | + self._use_advanced = use_advanced | |
65 | + self._initialization_attempts += 1 | |
66 | + self._last_initialization_attempt = datetime.now() | |
67 | + | |
68 | + if use_advanced: | |
69 | + self._advanced_file_manager = AdvancedFileManager(self._config) | |
70 | + # Initialize async components if available | |
71 | + import asyncio | |
72 | + try: | |
73 | + loop = asyncio.get_event_loop() | |
74 | + if loop.is_running(): | |
75 | + # Schedule initialization in the background | |
76 | + asyncio.create_task(self._advanced_file_manager.initialize()) | |
77 | + else: | |
78 | + # Run initialization synchronously | |
79 | + loop.run_until_complete(self._advanced_file_manager.initialize()) | |
80 | + except RuntimeError: | |
81 | + # No event loop, skip async initialization | |
82 | + self.logger.warning("No event loop available for advanced file manager initialization") | |
83 | + else: | |
84 | + self._file_manager = FileManager() | |
85 | + | |
86 | + FileManagerService._initialized = True | |
87 | + self._initialization_error = None | |
88 | + self.logger.info(f"FileManagerService initialized successfully (advanced={use_advanced})") | |
89 | + return True | |
90 | + | |
91 | + except Exception as e: | |
92 | + self._initialization_error = str(e) | |
93 | + self.logger.error(f"Failed to initialize FileManagerService: {e}") | |
94 | + return False | |
95 | + | |
96 | + def ensure_initialized(self) -> bool: | |
97 | + """Ensure the file manager is properly initialized | |
98 | + | |
99 | + Returns: | |
100 | + bool: True if file manager is available, False otherwise | |
101 | + """ | |
102 | + if not FileManagerService._initialized or (not self._file_manager and not self._advanced_file_manager): | |
103 | + return self.initialize() | |
104 | + return True | |
105 | + | |
106 | + def reinitialize(self, force: bool = False) -> bool: | |
107 | + """Attempt to reinitialize the file manager | |
108 | + | |
109 | + Args: | |
110 | + force: Force reinitialization even if already initialized | |
111 | + | |
112 | + Returns: | |
113 | + bool: True if reinitialization successful, False otherwise | |
114 | + """ | |
115 | + if not force and self.is_healthy(): | |
116 | + return True | |
117 | + | |
118 | + with self._initialization_lock: | |
119 | + try: | |
120 | + # Reset state | |
121 | + FileManagerService._initialized = False | |
122 | + self._file_manager = None | |
123 | + self._advanced_file_manager = None | |
124 | + | |
125 | + # Attempt reinitialization | |
126 | + return self.initialize(self._config, self._use_advanced) | |
127 | + | |
128 | + except Exception as e: | |
129 | + self.logger.error(f"Failed to reinitialize FileManagerService: {e}") | |
130 | + return False | |
131 | + | |
132 | + def get_file_manager(self) -> Optional[FileManager]: | |
133 | + """Get the file manager instance with automatic initialization | |
134 | + | |
135 | + Returns: | |
136 | + FileManager instance or None if initialization failed | |
137 | + """ | |
138 | + if not self.ensure_initialized(): | |
139 | + return None | |
140 | + | |
141 | + if self._use_advanced: | |
142 | + return self._advanced_file_manager | |
143 | + else: | |
144 | + return self._file_manager | |
145 | + | |
146 | + def is_healthy(self) -> bool: | |
147 | + """Check if the file manager service is healthy | |
148 | + | |
149 | + Returns: | |
150 | + bool: True if service is healthy and ready to use | |
151 | + """ | |
152 | + try: | |
153 | + if not FileManagerService._initialized: | |
154 | + return False | |
155 | + | |
156 | + file_manager = self._advanced_file_manager if self._use_advanced else self._file_manager | |
157 | + if file_manager is None: | |
158 | + return False | |
159 | + | |
160 | + # Test basic functionality | |
161 | + if hasattr(file_manager, 'list_directory'): | |
162 | + # Try to list current directory as a health check | |
163 | + test_path = Path.cwd() | |
164 | + if self._use_advanced: | |
165 | + # For advanced file manager, we need to handle async | |
166 | + import asyncio | |
167 | + try: | |
168 | + loop = asyncio.get_event_loop() | |
169 | + if loop.is_running(): | |
170 | + # Can't test async in running loop, assume healthy | |
171 | + return True | |
172 | + else: | |
173 | + loop.run_until_complete(file_manager.list_directory(test_path)) | |
174 | + except RuntimeError: | |
175 | + # No event loop, assume healthy if object exists | |
176 | + return True | |
177 | + else: | |
178 | + file_manager.list_directory(test_path) | |
179 | + return True | |
180 | + | |
181 | + return False | |
182 | + | |
183 | + except Exception as e: | |
184 | + self.logger.warning(f"Health check failed: {e}") | |
185 | + return False | |
186 | + | |
187 | + def get_status(self) -> Dict[str, Any]: | |
188 | + """Get detailed status information about the service | |
189 | + | |
190 | + Returns: | |
191 | + Dict containing status information | |
192 | + """ | |
193 | + return { | |
194 | + 'initialized': FileManagerService._initialized, | |
195 | + 'healthy': self.is_healthy(), | |
196 | + 'use_advanced': self._use_advanced, | |
197 | + 'initialization_attempts': self._initialization_attempts, | |
198 | + 'last_initialization_attempt': self._last_initialization_attempt, | |
199 | + 'initialization_error': self._initialization_error, | |
200 | + 'has_file_manager': self._file_manager is not None, | |
201 | + 'has_advanced_file_manager': self._advanced_file_manager is not None | |
202 | + } | |
203 | + | |
204 | + def list_directory(self, path: Path, show_hidden: bool = False, **kwargs): | |
205 | + """Safe wrapper for list_directory operation | |
206 | + | |
207 | + Args: | |
208 | + path: Directory path to list | |
209 | + show_hidden: Whether to show hidden files | |
210 | + **kwargs: Additional arguments for advanced file manager | |
211 | + | |
212 | + Returns: | |
213 | + List of file information or empty list on error | |
214 | + """ | |
215 | + try: | |
216 | + file_manager = self.get_file_manager() | |
217 | + if file_manager is None: | |
218 | + self.logger.error("File manager not available for list_directory") | |
219 | + return [] | |
220 | + | |
221 | + if self._use_advanced: | |
222 | + # Handle async call for advanced file manager | |
223 | + import asyncio | |
224 | + try: | |
225 | + loop = asyncio.get_event_loop() | |
226 | + if loop.is_running(): | |
227 | + # Create a task for async execution | |
228 | + task = asyncio.create_task( | |
229 | + file_manager.list_directory(path, show_hidden, **kwargs) | |
230 | + ) | |
231 | + # For now, return empty list and log warning | |
232 | + # In a real implementation, this would need proper async handling | |
233 | + self.logger.warning("Async list_directory called from sync context") | |
234 | + return [] | |
235 | + else: | |
236 | + return loop.run_until_complete( | |
237 | + file_manager.list_directory(path, show_hidden, **kwargs) | |
238 | + ) | |
239 | + except RuntimeError: | |
240 | + # Fallback to basic file manager | |
241 | + if self._file_manager is None: | |
242 | + self._file_manager = FileManager() | |
243 | + return self._file_manager.list_directory(path, show_hidden) | |
244 | + else: | |
245 | + return file_manager.list_directory(path, show_hidden) | |
246 | + | |
247 | + except Exception as e: | |
248 | + self.logger.error(f"Error in list_directory: {e}") | |
249 | + return [] | |
250 | + | |
251 | + def safe_operation(self, operation_name: str, operation_func, *args, **kwargs): | |
252 | + """Execute a file operation safely with error handling | |
253 | + | |
254 | + Args: | |
255 | + operation_name: Name of the operation for logging | |
256 | + operation_func: Function to execute | |
257 | + *args: Arguments for the operation | |
258 | + **kwargs: Keyword arguments for the operation | |
259 | + | |
260 | + Returns: | |
261 | + Operation result or None on error | |
262 | + """ | |
263 | + try: | |
264 | + file_manager = self.get_file_manager() | |
265 | + if file_manager is None: | |
266 | + self.logger.error(f"File manager not available for {operation_name}") | |
267 | + return None | |
268 | + | |
269 | + if not hasattr(file_manager, operation_func.__name__): | |
270 | + self.logger.error(f"Operation {operation_func.__name__} not available on file manager") | |
271 | + return None | |
272 | + | |
273 | + return operation_func(*args, **kwargs) | |
274 | + | |
275 | + except Exception as e: | |
276 | + self.logger.error(f"Error in {operation_name}: {e}") | |
277 | + | |
278 | + # Attempt recovery | |
279 | + if self.reinitialize(): | |
280 | + try: | |
281 | + file_manager = self.get_file_manager() | |
282 | + if file_manager and hasattr(file_manager, operation_func.__name__): | |
283 | + return operation_func(*args, **kwargs) | |
284 | + except Exception as recovery_error: | |
285 | + self.logger.error(f"Recovery attempt failed for {operation_name}: {recovery_error}") | |
286 | + | |
287 | + return None | |
288 | + | |
289 | + @classmethod | |
290 | + def reset(cls): | |
291 | + """Reset the singleton instance (mainly for testing)""" | |
292 | + with cls._lock: | |
293 | + cls._instance = None | |
294 | + cls._initialized = False | |
295 | + | |
296 | + | |
297 | +# Convenience functions for common operations | |
298 | +def get_file_manager_service() -> FileManagerService: | |
299 | + """Get the file manager service instance""" | |
300 | + return FileManagerService.get_instance() | |
301 | + | |
302 | + | |
303 | +def safe_list_directory(path: Path, show_hidden: bool = False, **kwargs): | |
304 | + """Safely list directory contents""" | |
305 | + service = get_file_manager_service() | |
306 | + return service.list_directory(path, show_hidden, **kwargs) | |
307 | + | |
308 | + | |
309 | +def ensure_file_manager_available() -> bool: | |
310 | + """Ensure file manager is available and healthy""" | |
311 | + service = get_file_manager_service() | |
312 | + return service.ensure_initialized() and service.is_healthy() | |
0 | 313 | |
βΌlaxyfile/core/initialization_models.py
+510-0
| @@ -0,0 +1,510 @@ | ||
1 | +""" | |
2 | +Initialization Models - Data models for tracking initialization status and component health | |
3 | + | |
4 | +This module provides data models for tracking the initialization state and health | |
5 | +of various components in the LaxyFile application. | |
6 | +""" | |
7 | + | |
8 | +from dataclasses import dataclass, field | |
9 | +from typing import Dict, List, Optional, Any | |
10 | +from datetime import datetime | |
11 | +from enum import Enum | |
12 | + | |
13 | + | |
14 | +class ComponentStatus(Enum): | |
15 | + """Enumeration of component status states""" | |
16 | + NOT_INITIALIZED = "not_initialized" | |
17 | + INITIALIZING = "initializing" | |
18 | + INITIALIZED = "initialized" | |
19 | + HEALTHY = "healthy" | |
20 | + UNHEALTHY = "unhealthy" | |
21 | + FAILED = "failed" | |
22 | + RECOVERING = "recovering" | |
23 | + | |
24 | + | |
25 | +class InitializationPhase(Enum): | |
26 | + """Enumeration of initialization phases""" | |
27 | + NOT_STARTED = "not_started" | |
28 | + CORE_SERVICES = "core_services" | |
29 | + UI_COMPONENTS = "ui_components" | |
30 | + INTEGRATION = "integration" | |
31 | + COMPLETED = "completed" | |
32 | + FAILED = "failed" | |
33 | + | |
34 | + | |
35 | +@dataclass | |
36 | +class InitializationState: | |
37 | + """Tracks overall initialization status and component health""" | |
38 | + | |
39 | + # Phase tracking | |
40 | + current_phase: InitializationPhase = InitializationPhase.NOT_STARTED | |
41 | + file_manager_ready: bool = False | |
42 | + ui_components_ready: bool = False | |
43 | + integration_complete: bool = False | |
44 | + | |
45 | + # Error tracking | |
46 | + initialization_errors: List[str] = field(default_factory=list) | |
47 | + last_initialization_attempt: Optional[datetime] = None | |
48 | + initialization_start_time: Optional[datetime] = None | |
49 | + initialization_end_time: Optional[datetime] = None | |
50 | + | |
51 | + # Component status tracking | |
52 | + component_status: Dict[str, ComponentStatus] = field(default_factory=dict) | |
53 | + | |
54 | + # Performance metrics | |
55 | + initialization_time: float = 0.0 | |
56 | + total_components: int = 0 | |
57 | + successful_components: int = 0 | |
58 | + failed_components: int = 0 | |
59 | + | |
60 | + # Recovery tracking | |
61 | + recovery_attempts: int = 0 | |
62 | + last_recovery_attempt: Optional[datetime] = None | |
63 | + recovery_successful: bool = False | |
64 | + | |
65 | + def get_initialization_progress(self) -> float: | |
66 | + """Get initialization progress as a percentage | |
67 | + | |
68 | + Returns: | |
69 | + float: Progress percentage (0.0 to 100.0) | |
70 | + """ | |
71 | + if self.total_components == 0: | |
72 | + return 0.0 | |
73 | + | |
74 | + return (self.successful_components / self.total_components) * 100.0 | |
75 | + | |
76 | + def is_initialization_complete(self) -> bool: | |
77 | + """Check if initialization is complete | |
78 | + | |
79 | + Returns: | |
80 | + bool: True if initialization is complete and successful | |
81 | + """ | |
82 | + return (self.current_phase == InitializationPhase.COMPLETED and | |
83 | + self.file_manager_ready and | |
84 | + self.ui_components_ready and | |
85 | + self.integration_complete) | |
86 | + | |
87 | + def has_critical_errors(self) -> bool: | |
88 | + """Check if there are critical initialization errors | |
89 | + | |
90 | + Returns: | |
91 | + bool: True if there are critical errors | |
92 | + """ | |
93 | + critical_keywords = ['file_manager', 'critical', 'fatal', 'core'] | |
94 | + return any( | |
95 | + any(keyword in error.lower() for keyword in critical_keywords) | |
96 | + for error in self.initialization_errors | |
97 | + ) | |
98 | + | |
99 | + def get_failed_components(self) -> List[str]: | |
100 | + """Get list of components that failed to initialize | |
101 | + | |
102 | + Returns: | |
103 | + List of component names that failed | |
104 | + """ | |
105 | + return [ | |
106 | + component for component, status in self.component_status.items() | |
107 | + if status == ComponentStatus.FAILED | |
108 | + ] | |
109 | + | |
110 | + def get_healthy_components(self) -> List[str]: | |
111 | + """Get list of healthy components | |
112 | + | |
113 | + Returns: | |
114 | + List of component names that are healthy | |
115 | + """ | |
116 | + return [ | |
117 | + component for component, status ionent_status.items() | |
118 | + if status == ComponentStatus.HEALTHY | |
119 | + ] | |
120 | + | |
121 | + def add_error(self, error_message: str) -> None: | |
122 | + """Add an initialization error | |
123 | + | |
124 | + Args: | |
125 | + error_message: Error message to add | |
126 | + """ | |
127 | + self.initialization_errors.append(error_message) | |
128 | + | |
129 | + # Update failed components count | |
130 | + if not any('recovery' in error_message.lower() for _ in [None]): | |
131 | + self.failed_components += 1 | |
132 | + | |
133 | + def set_component_status(self, component: str, status: ComponentStatus) -> None: | |
134 | + """Set the status of a component | |
135 | + | |
136 | + Args: | |
137 | + component: Component name | |
138 | + status: New status | |
139 | + """ | |
140 | + old_status = self.component_status.get(component) | |
141 | + self.component_status[component] = status | |
142 | + | |
143 | + # Update counters | |
144 | + if old_status != status: | |
145 | + if status == ComponentStatus.HEALTHY: | |
146 | + if old_status != ComponentStatus.HEALTHY: | |
147 | + self.successful_components += 1 | |
148 | + if old_status == ComponentStatus.FAILED: | |
149 | + self.failed_components -= 1 | |
150 | + elif status == ComponentStatus.FAILED: | |
151 | + if old_status != ComponentStatus.FAILED: | |
152 | + self.failed_components += 1 | |
153 | + if old_status == ComponentStatus.HEALTHY: | |
154 | + self.successful_components -= 1 | |
155 | + | |
156 | + def to_dict(self) -> Dict[str, Any]: | |
157 | + """Convert to dictionary for serialization | |
158 | + | |
159 | + Returns: | |
160 | + Dict representation of the initialization state | |
161 | + """ | |
162 | + return { | |
163 | + 'current_phase': self.current_phase.value, | |
164 | + 'file_manager_ready': self.file_manager_ready, | |
165 | + 'ui_components_ready': self.ui_components_ready, | |
166 | + 'integration_complete': self.integration_complete, | |
167 | + 'initialization_errors': self.initialization_errors, | |
168 | + 'last_initialization_attempt': self.last_initialization_attempt.isoformat() if self.last_initialization_attempt else None, | |
169 | + 'initialization_time': self.initialization_time, | |
170 | + 'total_components': self.total_components, | |
171 | + 'successful_components': self.successful_components, | |
172 | + 'failed_components': self.failed_components, | |
173 | + 'progress_percentage': self.get_initialization_progress(), | |
174 | + 'is_complete': self.is_initialization_complete(), | |
175 | + 'has_critical_errors': self.has_critical_errors(), | |
176 | + 'component_status': {k: v.value for k, v in self.component_status.items()}, | |
177 | + 'recovery_attempts': self.recovery_attempts, | |
178 | + 'recovery_successful': self.recovery_successful | |
179 | + } | |
180 | + | |
181 | + | |
182 | +@dataclass | |
183 | +class ComponentHealth: | |
184 | + """Tracks individual component health status and metrics""" | |
185 | + | |
186 | + # Basic information | |
187 | + component_name: str | |
188 | + status: ComponentStatus = ComponentStatus.NOT_INITIALIZED | |
189 | + is_healthy: bool = False | |
190 | + | |
191 | + # Timing information | |
192 | + last_check: datetime = field(default_factory=datetime.now) | |
193 | + initialization_time: float = 0.0 | |
194 | + last_successful_operation: Optional[datetime] = None | |
195 | + last_failed_operation: Optional[datetime] = None | |
196 | + | |
197 | + # Error tracking | |
198 | + error_message: Optional[str] = None | |
199 | + error_count: int = 0 | |
200 | + last_error_time: Optional[datetime] = None | |
201 | + error_history: List[Dict[str, Any]] = field(default_factory=list) | |
202 | + | |
203 | + # Dependency tracking | |
204 | + dependency_status: Dict[str, bool] = field(default_factory=dict) | |
205 | + required_dependencies: List[str] = field(default_factory=list) | |
206 | + optional_dependencies: List[str] = field(default_factory=list) | |
207 | + | |
208 | + # Performance metrics | |
209 | + average_response_time: float = 0.0 | |
210 | + operation_count: int = 0 | |
211 | + success_rate: float = 100.0 | |
212 | + | |
213 | + # Recovery tracking | |
214 | + recovery_attempts: int = 0 | |
215 | + last_recovery_attempt: Optional[datetime] = None | |
216 | + recovery_successful: bool = False | |
217 | + | |
218 | + def update_health_status(self) -> None: | |
219 | + """Update the health status based on current metrics""" | |
220 | + try: | |
221 | + # Check if all required dependencies are available | |
222 | + all_deps_available = all( | |
223 | + self.dependency_status.get(dep, False) | |
224 | + for dep in self.required_dependencies | |
225 | + ) | |
226 | + | |
227 | + # Determine health based on various factors | |
228 | + if self.status == ComponentStatus.FAILED: | |
229 | + self.is_healthy = False | |
230 | + elif self.status == ComponentStatus.NOT_INITIALIZED: | |
231 | + self.is_healthy = False | |
232 | + elif not all_deps_available: | |
233 | + self.is_healthy = False | |
234 | + elif self.error_count > 10: # Too many errors | |
235 | + self.is_healthy = False | |
236 | + elif self.success_rate < 50.0: # Low success rate | |
237 | + self.is_healthy = False | |
238 | + else: | |
239 | + self.is_healthy = True | |
240 | + self.status = ComponentStatus.HEALTHY | |
241 | + | |
242 | + self.last_check = datetime.now() | |
243 | + | |
244 | + except Exception as e: | |
245 | + self.is_healthy = False | |
246 | + self.error_message = f"Health check failed: {e}" | |
247 | + | |
248 | + def record_operation(self, success: bool, response_time: float = 0.0, | |
249 | + error_message: Optional[str] = None) -> None: | |
250 | + """Record the result of an operation | |
251 | + | |
252 | + Args: | |
253 | + success: Whether the operation was successful | |
254 | + response_time: Time taken for the operation | |
255 | + error_message: Error message if operation failed | |
256 | + """ | |
257 | + self.operation_count += 1 | |
258 | + | |
259 | + if success: | |
260 | + self.last_successful_operation = datetime.now() | |
261 | + | |
262 | + # Update average response time | |
263 | + if self.operation_count == 1: | |
264 | + self.average_response_time = response_time | |
265 | + else: | |
266 | + self.average_response_time = ( | |
267 | + (self.average_response_time * (self.operation_count - 1) + response_time) / | |
268 | + self.operation_count | |
269 | + ) | |
270 | + else: | |
271 | + self.error_count += 1 | |
272 | + self.last_failed_operation = datetime.now() | |
273 | + self.last_error_time = datetime.now() | |
274 | + | |
275 | + if error_message: | |
276 | + self.error_message = error_message | |
277 | + self.error_history.append({ | |
278 | + 'timestamp': datetime.now(), | |
279 | + 'error': error_message, | |
280 | + 'operation_count': self.operation_count | |
281 | + }) | |
282 | + | |
283 | + # Limit error history size | |
284 | + if len(self.error_history) > 50: | |
285 | + self.error_history = self.error_history[-50:] | |
286 | + | |
287 | + # Update success rate | |
288 | + successful_operations = self.operation_count - self.error_count | |
289 | + self.success_rate = (successful_operations / self.operation_count) * 100.0 | |
290 | + | |
291 | + # Update health status | |
292 | + self.update_health_status() | |
293 | + | |
294 | + def record_dependency_status(self, dependency: str, available: bool) -> None: | |
295 | + """Record the status of a dependency | |
296 | + | |
297 | + Args: | |
298 | + dependency: Name of the dependency | |
299 | + available: Whether the dependency is available | |
300 | + """ | |
301 | + self.dependency_status[dependency] = available | |
302 | + self.update_health_status() | |
303 | + | |
304 | + def get_dependency_health(self) -> Dict[str, Any]: | |
305 | + """Get detailed dependency health information | |
306 | + | |
307 | + Returns: | |
308 | + Dict containing dependency health information | |
309 | + """ | |
310 | + return { | |
311 | + 'required_dependencies': { | |
312 | + dep: self.dependency_status.get(dep, False) | |
313 | + for dep in self.required_dependencies | |
314 | + }, | |
315 | + 'optional_dependencies': { | |
316 | + dep: self.dependency_status.get(dep, False) | |
317 | + for dep in self.optional_dependencies | |
318 | + }, | |
319 | + 'all_required_available': all( | |
320 | + self.dependency_status.get(dep, False) | |
321 | + for dep in self.required_dependencies | |
322 | + ), | |
323 | + 'missing_required': [ | |
324 | + dep for dep in self.required_dependencies | |
325 | + if not self.dependency_status.get(dep, False) | |
326 | + ] | |
327 | + } | |
328 | + | |
329 | + def attempt_recovery(self) -> bool: | |
330 | + """Attempt to recover the component | |
331 | + | |
332 | + Returns: | |
333 | + bool: True if recovery was successful | |
334 | + """ | |
335 | + self.recovery_attempts += 1 | |
336 | + self.last_recovery_attempt = datetime.now() | |
337 | + | |
338 | + try: | |
339 | + # Basic recovery logic - reset error state | |
340 | + if self.error_count > 0: | |
341 | + self.error_count = max(0, self.error_count - 5) # Reduce error count | |
342 | + self.error_message = None | |
343 | + | |
344 | + # Update status | |
345 | + if self.error_count == 0: | |
346 | + self.status = ComponentStatus.HEALTHY | |
347 | + self.recovery_successful = True | |
348 | + return True | |
349 | + | |
350 | + return False | |
351 | + | |
352 | + except Exception as e: | |
353 | + self.error_message = f"Recovery failed: {e}" | |
354 | + self.recovery_successful = False | |
355 | + return False | |
356 | + | |
357 | + def get_performance_metrics(self) -> Dict[str, Any]: | |
358 | + """Get performance metrics for the component | |
359 | + | |
360 | + Returns: | |
361 | + Dict containing performance metrics | |
362 | + """ | |
363 | + return { | |
364 | + 'operation_count': self.operation_count, | |
365 | + 'error_count': self.error_count, | |
366 | + 'success_rate': self.success_rate, | |
367 | + 'average_response_time': self.average_response_time, | |
368 | + 'initialization_time': self.initialization_time, | |
369 | + 'last_successful_operation': self.last_successful_operation, | |
370 | + 'last_failed_operation': self.last_failed_operation, | |
371 | + 'recovery_attempts': self.recovery_attempts, | |
372 | + 'recovery_successful': self.recovery_successful | |
373 | + } | |
374 | + | |
375 | + def to_dict(self) -> Dict[str, Any]: | |
376 | + """Convert to dictionary for serialization | |
377 | + | |
378 | + Returns: | |
379 | + Dict representation of the component health | |
380 | + """ | |
381 | + return { | |
382 | + 'component_name': self.component_name, | |
383 | + 'status': self.status.value, | |
384 | + 'is_healthy': self.is_healthy, | |
385 | + 'last_check': self.last_check.isoformat(), | |
386 | + 'initialization_time': self.initialization_time, | |
387 | + 'error_message': self.error_message, | |
388 | + 'error_count': self.error_count, | |
389 | + 'dependency_health': self.get_dependency_health(), | |
390 | + 'performance_metrics': self.get_performance_metrics(), | |
391 | + 'recent_errors': self.error_history[-5:] if self.error_history else [] | |
392 | + } | |
393 | + | |
394 | + | |
395 | +@dataclass | |
396 | +class SystemHealth: | |
397 | + """Overall system health status combining all components""" | |
398 | + | |
399 | + # Overall status | |
400 | + is_healthy: bool = False | |
401 | + health_score: float = 0.0 # 0-100 score | |
402 | + last_health_check: datetime = field(default_factory=datetime.now) | |
403 | + | |
404 | + # Component tracking | |
405 | + total_components: int = 0 | |
406 | + healthy_components: int = 0 | |
407 | + unhealthy_components: int = 0 | |
408 | + failed_components: int = 0 | |
409 | + | |
410 | + # Critical systems | |
411 | + file_manager_healthy: bool = False | |
412 | + ui_system_healthy: bool = False | |
413 | + core_services_healthy: bool = False | |
414 | + | |
415 | + # Performance metrics | |
416 | + average_response_time: float = 0.0 | |
417 | + total_operations: int = 0 | |
418 | + total_errors: int = 0 | |
419 | + overall_success_rate: float = 100.0 | |
420 | + | |
421 | + def update_from_components(self, components: Dict[str, ComponentHealth]) -> None: | |
422 | + """Update system health from component health data | |
423 | + | |
424 | + Args: | |
425 | + components: Dictionary of component health objects | |
426 | + """ | |
427 | + self.total_components = len(components) | |
428 | + self.healthy_components = sum(1 for c in components.values() if c.is_healthy) | |
429 | + self.unhealthy_components = self.total_components - self.healthy_components | |
430 | + self.failed_components = sum( | |
431 | + 1 for c in components.values() | |
432 | + if c.status == ComponentStatus.FAILED | |
433 | + ) | |
434 | + | |
435 | + # Check critical systems | |
436 | + self.file_manager_healthy = components.get('file_manager', ComponentHealth('file_manager')).is_healthy | |
437 | + self.ui_system_healthy = all( | |
438 | + components.get(comp, ComponentHealth(comp)).is_healthy | |
439 | + for comp in ['theme_manager', 'panel_manager', 'media_viewer'] | |
440 | + ) | |
441 | + self.core_services_healthy = all( | |
442 | + components.get(comp, ComponentHealth(comp)).is_healthy | |
443 | + for comp in ['file_manager', 'config'] | |
444 | + ) | |
445 | + | |
446 | + # Calculate health score | |
447 | + if self.total_components > 0: | |
448 | + base_score = (self.healthy_components / self.total_components) * 100 | |
449 | + | |
450 | + # Apply penalties for critical system failures | |
451 | + if not self.file_manager_healthy: | |
452 | + base_score *= 0.5 # 50% penalty for file manager issues | |
453 | + if not self.core_services_healthy: | |
454 | + base_score *= 0.7 # 30% penalty for core service issues | |
455 | + | |
456 | + self.health_score = max(0.0, min(100.0, base_score)) | |
457 | + else: | |
458 | + self.health_score = 0.0 | |
459 | + | |
460 | + # Overall health determination | |
461 | + self.is_healthy = ( | |
462 | + self.health_score >= 80.0 and | |
463 | + self.file_manager_healthy and | |
464 | + self.core_services_healthy | |
465 | + ) | |
466 | + | |
467 | + # Update performance metrics | |
468 | + if components: | |
469 | + total_ops = sum(c.operation_count for c in components.values()) | |
470 | + total_errors = sum(c.error_count for c in components.values()) | |
471 | + | |
472 | + self.total_operations = total_ops | |
473 | + self.total_errors = total_errors | |
474 | + | |
475 | + if total_ops > 0: | |
476 | + self.overall_success_rate = ((total_ops - total_errors) / total_ops) * 100.0 | |
477 | + self.average_response_time = sum( | |
478 | + c.average_response_time * c.operation_count for c in components.values() | |
479 | + ) / total_ops | |
480 | + | |
481 | + self.last_health_check = datetime.now() | |
482 | + | |
483 | + def get_health_summary(self) -> Dict[str, Any]: | |
484 | + """Get a summary of system health | |
485 | + | |
486 | + Returns: | |
487 | + Dict containing health summary | |
488 | + """ | |
489 | + return { | |
490 | + 'is_healthy': self.is_healthy, | |
491 | + 'health_score': self.health_score, | |
492 | + 'last_check': self.last_health_check.isoformat(), | |
493 | + 'component_summary': { | |
494 | + 'total': self.total_components, | |
495 | + 'healthy': self.healthy_components, | |
496 | + 'unhealthy': self.unhealthy_components, | |
497 | + 'failed': self.failed_components | |
498 | + }, | |
499 | + 'critical_systems': { | |
500 | + 'file_manager': self.file_manager_healthy, | |
501 | + 'ui_system': self.ui_system_healthy, | |
502 | + 'core_services': self.core_services_healthy | |
503 | + }, | |
504 | + 'performance': { | |
505 | + 'total_operations': self.total_operations, | |
506 | + 'total_errors': self.total_errors, | |
507 | + 'success_rate': self.overall_success_rate, | |
508 | + 'average_response_time': self.average_response_time | |
509 | + } | |
510 | + } | |
0 | 511 | |
βΌlaxyfile/integration/component_integration.py
+8-1
| @@ -169,6 +169,13 @@ class ComponentIntegrator: | ||
169 | 169 | async def _initialize_preview_system(self) -> bool: |
170 | 170 | """Initialize preview system""" |
171 | 171 | try: |
172 | + # Ensure file manager service is available first | |
173 | + from ..core.file_manager_service import FileManagerService | |
174 | + file_manager_service = FileManagerService.get_instance() | |
175 | + | |
176 | + if not file_manager_service.ensure_initialized(): | |
177 | + self.logger.warning("File manager service not available during preview system initialization") | |
178 | + | |
172 | 179 | # Create preview config from main config |
173 | 180 | from ..preview.preview_system import PreviewConfig |
174 | 181 | preview_config = PreviewConfig() |
| @@ -189,7 +196,7 @@ class ComponentIntegrator: | ||
189 | 196 | initialized=True, |
190 | 197 | healthy=True, |
191 | 198 | last_check=time.time(), |
192 | - dependencies=['config'] | |
199 | + dependencies=['config', 'file_manager'] | |
193 | 200 | ) |
194 | 201 | return True |
195 | 202 | except Exception as e: |
196 | 203 | |
βΌlaxyfile/main.py
+186-60
| @@ -34,6 +34,9 @@ from rich.style import Style | ||
34 | 34 | |
35 | 35 | from .core.file_manager import FileManager |
36 | 36 | from .core.config import Config |
37 | +from .core.component_initializer import ComponentInitializer | |
38 | +from .core.file_manager_service import FileManagerService | |
39 | +from .core.error_handling_mixin import ErrorHandlingMixin | |
37 | 40 | from .core.performance_optimizer import PerformanceOptimizer, PerformanceConfig |
38 | 41 | from .core.startup_optimizer import StartupOptimizer, StartupConfig |
39 | 42 | from .ui.theme import ThemeManager |
| @@ -153,10 +156,13 @@ class KeyHandler: | ||
153 | 156 | pass |
154 | 157 | return None |
155 | 158 | |
156 | -class LaxyFileApp: | |
157 | - """Modern file manager with Superfile-like interface""" | |
159 | +class LaxyFileApp(ErrorHandlingMixin): | |
160 | + """Modern file manager with Superfile-like interface and robust error handling""" | |
158 | 161 | |
159 | 162 | def __init__(self): |
163 | + # Initialize error handling mixin first | |
164 | + super().__init__() | |
165 | + | |
160 | 166 | # Initialize console with optimized settings for performance |
161 | 167 | self.console = Console( |
162 | 168 | force_terminal=True, |
| @@ -170,22 +176,38 @@ class LaxyFileApp: | ||
170 | 176 | _environ=os.environ # Cache environment for performance |
171 | 177 | ) |
172 | 178 | |
173 | - # Core components - lazy initialization for faster startup | |
179 | + # Core configuration | |
174 | 180 | self.config = Config() |
175 | - self.theme_manager = None # Lazy init | |
176 | - self.file_manager = None # Lazy init | |
177 | - self.panel_manager = None # Lazy init | |
178 | - self.media_viewer = None # Lazy init | |
179 | - self.ai_assistant = None # Lazy init | |
181 | + | |
182 | + # Initialize component initializer for proper startup sequence | |
183 | + self.component_initializer = ComponentInitializer(self.config) | |
184 | + | |
185 | + # Initialize core services first | |
186 | + if not self.component_initializer.initialize_core_services(): | |
187 | + self.logger.error("Failed to initialize core services") | |
188 | + # Continue with degraded functionality | |
189 | + | |
190 | + # Core components - will be properly initialized by ComponentInitializer | |
191 | + self.theme_manager = None | |
192 | + self.file_manager_service = FileManagerService.get_instance() | |
193 | + self.panel_manager = None | |
194 | + self.media_viewer = None | |
195 | + self.ai_assistant = None | |
180 | 196 | |
181 | 197 | # Initialize navigation performance systems |
182 | 198 | self.navigation_throttle = NavigationThrottle() |
183 | 199 | self.fast_nav_manager = FastNavigationManager() |
184 | 200 | self.keyboard_debouncer = KeyboardDebouncer() |
185 | 201 | |
202 | + # Display optimization | |
203 | + self._display_dirty = False | |
204 | + self._last_navigation_time = 0 | |
205 | + self._navigation_debounce_delay = 0.05 # 50ms debounce | |
206 | + self._cached_file_lists = {} | |
207 | + self._cache_expiry = 2.0 # Cache for 2 seconds | |
208 | + | |
186 | 209 | # Initialize hotkey manager |
187 | 210 | self.hotkey_manager = HotkeyManager() |
188 | - self.logger = Logger() | |
189 | 211 | |
190 | 212 | # Performance optimizations |
191 | 213 | self._component_cache = {} |
| @@ -214,6 +236,11 @@ class LaxyFileApp: | ||
214 | 236 | if self.config.config.remember_session: |
215 | 237 | self._restore_session() |
216 | 238 | |
239 | + # Initialize UI components after core services | |
240 | + if not self.component_initializer.initialize_ui_components(): | |
241 | + self.logger.warning("Some UI components failed to initialize") | |
242 | + # Continue with available components | |
243 | + | |
217 | 244 | # Setup layout |
218 | 245 | self.layout = self.create_layout() |
219 | 246 | |
| @@ -221,6 +248,10 @@ class LaxyFileApp: | ||
221 | 248 | signal.signal(signal.SIGINT, self._signal_handler) |
222 | 249 | signal.signal(signal.SIGTERM, self._signal_handler) |
223 | 250 | |
251 | + # Mark startup as complete | |
252 | + self._startup_complete = True | |
253 | + self.logger.info("LaxyFile application initialized successfully") | |
254 | + | |
224 | 255 | def _get_theme_manager(self): |
225 | 256 | """Lazy initialization of theme manager""" |
226 | 257 | if self.theme_manager is None: |
| @@ -229,11 +260,21 @@ class LaxyFileApp: | ||
229 | 260 | return self.theme_manager |
230 | 261 | |
231 | 262 | def _get_file_manager(self): |
232 | - """Lazy initialization of file manager""" | |
233 | - if self.file_manager is None: | |
234 | - self.file_manager = FileManager() | |
235 | - self._component_cache['file_manager'] = self.file_manager | |
236 | - return self.file_manager | |
263 | + """Get file manager through the service""" | |
264 | + # Use the file manager service instead of direct instantiation | |
265 | + file_manager = self.file_manager_service.get_file_manager() | |
266 | + if file_manager is None: | |
267 | + self.logger.error("File manager not available from service") | |
268 | + # Return a fallback or handle gracefully | |
269 | + return None | |
270 | + | |
271 | + self._component_cache['file_manager'] = file_manager | |
272 | + return file_manager | |
273 | + | |
274 | + @property | |
275 | + def file_manager(self): | |
276 | + """Property to access file manager through service with error handling""" | |
277 | + return self._get_file_manager() | |
237 | 278 | |
238 | 279 | def _get_panel_manager(self): |
239 | 280 | """Lazy initialization of panel manager""" |
| @@ -299,6 +340,44 @@ class LaxyFileApp: | ||
299 | 340 | self._component_cache['animation_optimizer'] = self.animation_optimizer |
300 | 341 | return self.animation_optimizer |
301 | 342 | |
343 | + def _get_cached_file_list(self, panel_name: str) -> List[Any]: | |
344 | + """Get cached file list to prevent excessive directory reads during navigation""" | |
345 | + current_path = self.current_path[panel_name] | |
346 | + cache_key = f"{panel_name}_{current_path}_{self._get_panel_manager().show_hidden}" | |
347 | + current_time = time.time() | |
348 | + | |
349 | + # Check if we have a valid cached result | |
350 | + if cache_key in self._cached_file_lists: | |
351 | + cached_data = self._cached_file_lists[cache_key] | |
352 | + if current_time - cached_data['timestamp'] < self._cache_expiry: | |
353 | + return cached_data['files'] | |
354 | + | |
355 | + # Cache miss or expired - get fresh file list | |
356 | + files = self.safe_file_operation( | |
357 | + operation=lambda: self.file_manager_service.list_directory( | |
358 | + current_path, | |
359 | + self._get_panel_manager().show_hidden | |
360 | + ), | |
361 | + fallback_value=[], | |
362 | + cache_key=f"cached_list_{cache_key}", | |
363 | + max_retries=1 | |
364 | + ) | |
365 | + | |
366 | + # Cache the result | |
367 | + self._cached_file_lists[cache_key] = { | |
368 | + 'files': files, | |
369 | + 'timestamp': current_time | |
370 | + } | |
371 | + | |
372 | + # Limit cache size to prevent memory issues | |
373 | + if len(self._cached_file_lists) > 10: | |
374 | + # Remove oldest entries | |
375 | + oldest_key = min(self._cached_file_lists.keys(), | |
376 | + key=lambda k: self._cached_file_lists[k]['timestamp']) | |
377 | + del self._cached_file_lists[oldest_key] | |
378 | + | |
379 | + return files | |
380 | + | |
302 | 381 | def _complete_startup(self): |
303 | 382 | """Complete startup initialization after first use""" |
304 | 383 | if not self._startup_complete: |
| @@ -430,7 +509,13 @@ class LaxyFileApp: | ||
430 | 509 | def create_file_panel(self, panel_name: str, path: Path, is_active: bool) -> Panel: |
431 | 510 | """Create beautiful file panel with modern styling""" |
432 | 511 | try: |
433 | - files = self.file_manager.list_directory(path, self.panel_manager.show_hidden) | |
512 | + # Use safe file operation to get directory listing | |
513 | + files = self.safe_file_operation( | |
514 | + operation=lambda: self.file_manager_service.list_directory(path, self._get_panel_manager().show_hidden), | |
515 | + fallback_value=[], | |
516 | + cache_key=f"panel_{panel_name}_{path}", | |
517 | + max_retries=2 | |
518 | + ) | |
434 | 519 | current_selection = self.panel_manager.current_selection.get(panel_name, 0) |
435 | 520 | selected_files = self.panel_manager.selected_files.get(panel_name, set()) |
436 | 521 | |
| @@ -616,9 +701,15 @@ class LaxyFileApp: | ||
616 | 701 | def create_preview_panel(self) -> Panel: |
617 | 702 | """Create preview panel for current file""" |
618 | 703 | try: |
619 | - files = self.file_manager.list_directory( | |
620 | - self.current_path[self.active_panel], | |
621 | - self.panel_manager.show_hidden | |
704 | + # Use safe file operation to get directory listing | |
705 | + files = self.safe_file_operation( | |
706 | + operation=lambda: self.file_manager_service.list_directory( | |
707 | + self.current_path[self.active_panel], | |
708 | + self._get_panel_manager().show_hidden | |
709 | + ), | |
710 | + fallback_value=[], | |
711 | + cache_key=f"preview_{self.active_panel}_{self.current_path[self.active_panel]}", | |
712 | + max_retries=1 | |
622 | 713 | ) |
623 | 714 | current_file = self.panel_manager.get_current_file(self.active_panel, files) |
624 | 715 | |
| @@ -635,7 +726,15 @@ class LaxyFileApp: | ||
635 | 726 | |
636 | 727 | # Use media viewer for preview with even larger dimensions for better quality |
637 | 728 | self.logger.debug(f"Creating preview for: {current_file.path}") |
638 | - return self.media_viewer.create_preview_panel(current_file.path, 60, 40) | |
729 | + media_viewer = self._get_media_viewer() | |
730 | + if media_viewer is None: | |
731 | + return Panel( | |
732 | + Align.center(Text("Media viewer unavailable", style="yellow")), | |
733 | + title="β οΈ Preview Unavailable", | |
734 | + border_style="yellow", | |
735 | + box=ROUNDED | |
736 | + ) | |
737 | + return media_viewer.create_preview_panel(current_file.path, 60, 40) | |
639 | 738 | |
640 | 739 | except Exception as e: |
641 | 740 | self.logger.error(f"Preview panel creation error: {e}") |
| @@ -649,14 +748,24 @@ class LaxyFileApp: | ||
649 | 748 | def create_footer(self) -> Panel: |
650 | 749 | """Create modern footer with status and shortcuts""" |
651 | 750 | try: |
652 | - # Status information | |
653 | - left_files = self.file_manager.list_directory( | |
654 | - self.current_path["left"], | |
655 | - self.panel_manager.show_hidden | |
751 | + # Status information using safe file operations | |
752 | + left_files = self.safe_file_operation( | |
753 | + operation=lambda: self.file_manager_service.list_directory( | |
754 | + self.current_path["left"], | |
755 | + self._get_panel_manager().show_hidden | |
756 | + ), | |
757 | + fallback_value=[], | |
758 | + cache_key=f"footer_left_{self.current_path['left']}", | |
759 | + max_retries=1 | |
656 | 760 | ) |
657 | - right_files = self.file_manager.list_directory( | |
658 | - self.current_path["right"], | |
659 | - self.panel_manager.show_hidden | |
761 | + right_files = self.safe_file_operation( | |
762 | + operation=lambda: self.file_manager_service.list_directory( | |
763 | + self.current_path["right"], | |
764 | + self._get_panel_manager().show_hidden | |
765 | + ), | |
766 | + fallback_value=[], | |
767 | + cache_key=f"footer_right_{self.current_path['right']}", | |
768 | + max_retries=1 | |
660 | 769 | ) |
661 | 770 | |
662 | 771 | left_selected = len(self.panel_manager.selected_files.get("left", set())) |
| @@ -794,21 +903,18 @@ class LaxyFileApp: | ||
794 | 903 | try: |
795 | 904 | hotkeys = self.config.get_hotkeys() |
796 | 905 | |
797 | - # Ultra-fast navigation - process instantly without caching delays | |
906 | + # Optimized navigation with caching to prevent flicker | |
798 | 907 | # WASD Navigation (as requested) |
799 | 908 | if key in ['s', 'S', 'j', 'J', 'DOWN']: |
800 | - # Down navigation - instant processing | |
801 | - files = self.file_manager.list_directory( | |
802 | - self.current_path[self.active_panel], | |
803 | - self.panel_manager.show_hidden | |
804 | - ) | |
909 | + # Down navigation - use cached file list to prevent flicker | |
910 | + files = self._get_cached_file_list(self.active_panel) | |
805 | 911 | self.panel_manager.navigate_down(self.active_panel, len(files)) |
806 | - # Force immediate display update for ultra-fast response | |
807 | - | |
912 | + self._display_dirty = True | |
808 | 913 | |
809 | 914 | elif key in ['w', 'W', 'k', 'K', 'UP']: |
810 | - # Up navigation - instant processing | |
915 | + # Up navigation - instant processing without file system calls | |
811 | 916 | self.panel_manager.navigate_up(self.active_panel) |
917 | + self._display_dirty = True | |
812 | 918 | |
813 | 919 | |
814 | 920 | elif key in ['a', 'A', 'h', 'H', 'LEFT']: |
| @@ -833,8 +939,9 @@ class LaxyFileApp: | ||
833 | 939 | |
834 | 940 | |
835 | 941 | elif key in ['G']: |
836 | - files = self.file_manager.list_directory(self.current_path[self.active_panel], self.panel_manager.show_hidden) | |
942 | + files = self._get_cached_file_list(self.active_panel) | |
837 | 943 | self.panel_manager.navigate_to_bottom(self.active_panel, len(files)) |
944 | + self._display_dirty = True | |
838 | 945 | |
839 | 946 | |
840 | 947 | elif key == 'PAGE_UP': |
| @@ -843,9 +950,10 @@ class LaxyFileApp: | ||
843 | 950 | |
844 | 951 | |
845 | 952 | elif key == 'PAGE_DOWN': |
846 | - # Page down navigation - jump by 10 items | |
847 | - files = self.file_manager.list_directory(self.current_path[self.active_panel], self.panel_manager.show_hidden) | |
953 | + # Page down navigation - jump by 10 items using cached file list | |
954 | + files = self._get_cached_file_list(self.active_panel) | |
848 | 955 | self.panel_manager.navigate_page_down(self.active_panel, len(files), 10) |
956 | + self._display_dirty = True | |
849 | 957 | |
850 | 958 | # File operations - reassigned since WASD are now navigation |
851 | 959 | elif key in ['c', 'C']: |
| @@ -863,8 +971,9 @@ class LaxyFileApp: | ||
863 | 971 | elif key in ['e', 'E']: |
864 | 972 | self.handle_edit() |
865 | 973 | elif key in ['z', 'Z']: # New key for select all since 'a' is now left |
866 | - files = self.file_manager.list_directory(self.current_path[self.active_panel], self.panel_manager.show_hidden) | |
974 | + files = self._get_cached_file_list(self.active_panel) | |
867 | 975 | self.panel_manager.select_all(self.active_panel, files) |
976 | + self._display_dirty = True | |
868 | 977 | |
869 | 978 | # Quick navigation |
870 | 979 | elif key == '~': |
| @@ -1259,9 +1368,13 @@ Enter your choice (0-9):""" | ||
1259 | 1368 | |
1260 | 1369 | if current_file and current_file.is_file: |
1261 | 1370 | # Use media viewer for full-screen display |
1262 | - panel = self.media_viewer.display_file(current_file.path, 100, 40) | |
1263 | - self.console.print(panel) | |
1264 | - self.console.input("[dim cyan]Press Enter to continue...[/dim cyan]") | |
1371 | + media_viewer = self._get_media_viewer() | |
1372 | + if media_viewer is not None: | |
1373 | + panel = media_viewer.display_file(current_file.path, 100, 40) | |
1374 | + self.console.print(panel) | |
1375 | + self.console.input("[dim cyan]Press Enter to continue...[/dim cyan]") | |
1376 | + else: | |
1377 | + self.show_message("β Media viewer unavailable", "red") | |
1265 | 1378 | |
1266 | 1379 | except Exception as e: |
1267 | 1380 | self.show_message(f"β View error: {e}", "red") |
| @@ -1388,39 +1501,52 @@ Set OPENROUTER_API_KEY environment variable to enable AI features. | ||
1388 | 1501 | pass |
1389 | 1502 | |
1390 | 1503 | async def _async_run(self): |
1391 | - """Ultra-fast async main application loop optimized for navigation with stable display""" | |
1504 | + """Optimized async main application loop with anti-flicker navigation""" | |
1392 | 1505 | try: |
1393 | - # Initial display setup - populate panels before starting main loop | |
1394 | - self.update_display_immediately() | |
1395 | - | |
1396 | - | |
1506 | + # Initial display setup | |
1507 | + self.update_display() | |
1397 | 1508 | |
1398 | 1509 | with Live( |
1399 | 1510 | self.layout, |
1400 | 1511 | console=self.console, |
1401 | 1512 | screen=True, |
1402 | - refresh_per_second=60, # Reduced from 120 to prevent window jumping | |
1513 | + refresh_per_second=30, # Reduced to 30 FPS to prevent flicker | |
1403 | 1514 | auto_refresh=False |
1404 | 1515 | ) as live: |
1405 | 1516 | |
1517 | + last_key_time = 0 | |
1518 | + pending_refresh = False | |
1519 | + | |
1406 | 1520 | while self.running: |
1407 | - # Handle input first for maximum responsiveness | |
1521 | + current_time = time.time() | |
1408 | 1522 | key = self.key_handler.get_key_input() |
1523 | + | |
1409 | 1524 | if key: |
1410 | - # Process the key without showing it on screen | |
1525 | + # Process the key | |
1411 | 1526 | await self.handle_key(key) |
1412 | - | |
1413 | - # Update display content | |
1414 | - self.update_display() | |
1415 | - | |
1416 | - # Single refresh after key processing to prevent flicker | |
1527 | + last_key_time = current_time | |
1528 | + pending_refresh = True | |
1529 | + | |
1530 | + # For navigation keys, use debounced update to prevent flicker | |
1531 | + if key in ['UP', 'DOWN', 'w', 'W', 's', 'S', 'k', 'K', 'j', 'J']: | |
1532 | + # Mark display as dirty but don't refresh immediately | |
1533 | + self._display_dirty = True | |
1534 | + else: | |
1535 | + # For non-navigation keys, update immediately | |
1536 | + self.update_display() | |
1537 | + live.refresh() | |
1538 | + pending_refresh = False | |
1539 | + | |
1540 | + # Debounced refresh for navigation keys to prevent flicker | |
1541 | + if pending_refresh and (current_time - last_key_time) > 0.05: # 50ms debounce | |
1542 | + if self._display_dirty: | |
1543 | + self.update_display() | |
1544 | + self._display_dirty = False | |
1417 | 1545 | live.refresh() |
1546 | + pending_refresh = False | |
1418 | 1547 | |
1419 | - # Slightly longer delay to prevent excessive refreshing | |
1420 | - await asyncio.sleep(0.016) # ~60 FPS to match refresh rate | |
1421 | - else: | |
1422 | - # No input - minimal delay without refresh to prevent jumping | |
1423 | - await asyncio.sleep(0.016) | |
1548 | + # Minimal sleep to prevent CPU spinning | |
1549 | + await asyncio.sleep(0.01) # 10ms sleep | |
1424 | 1550 | |
1425 | 1551 | except KeyboardInterrupt: |
1426 | 1552 | self.quit() |
1427 | 1553 | |
βΌlaxyfile/preview/preview_system.py
+61-3
| @@ -18,6 +18,8 @@ import json | ||
18 | 18 | |
19 | 19 | from ..core.types import OperationResult |
20 | 20 | from ..core.exceptions import PreviewError |
21 | +from ..core.file_manager_service import FileManagerService | |
22 | +from ..core.error_handling_mixin import ErrorHandlingMixin | |
21 | 23 | from ..utils.logger import Logger |
22 | 24 | from .terminal_media import terminal_image_renderer, terminal_video_renderer |
23 | 25 | |
| @@ -384,14 +386,18 @@ class BinaryPreviewRenderer(BasePreviewRenderer): | ||
384 | 386 | return self._create_error_result(file_path, str(e)) |
385 | 387 | |
386 | 388 | |
387 | -class AdvancedPreviewSystem: | |
389 | +class AdvancedPreviewSystem(ErrorHandlingMixin): | |
388 | 390 | """Advanced file preview system with multiple format support""" |
389 | 391 | |
390 | 392 | def __init__(self, config: Optional[PreviewConfig] = None): |
393 | + super().__init__() | |
391 | 394 | self.config = config or PreviewConfig() |
392 | 395 | self.logger = Logger() |
393 | 396 | self.cache = PreviewCache() if self.config.cache_enabled else None |
394 | 397 | |
398 | + # Get file manager service | |
399 | + self.file_manager_service = FileManagerService.get_instance() | |
400 | + | |
395 | 401 | # Initialize renderers |
396 | 402 | self.renderers: List[BasePreviewRenderer] = [] |
397 | 403 | self._initialize_renderers() |
| @@ -423,6 +429,57 @@ class AdvancedPreviewSystem: | ||
423 | 429 | self.logger.error(f"Failed to initialize AdvancedPreviewSystem: {e}") |
424 | 430 | raise |
425 | 431 | |
432 | + def _safe_file_exists(self, file_path: Path) -> bool: | |
433 | + """Safely check if file exists using file manager service""" | |
434 | + try: | |
435 | + # Ensure file manager service is available | |
436 | + if not self.file_manager_service.ensure_initialized(): | |
437 | + # Fallback to direct file system check | |
438 | + return file_path.exists() | |
439 | + | |
440 | + # For file existence, we can use direct path check | |
441 | + # but we could also use file manager service for consistency | |
442 | + return file_path.exists() and file_path.is_file() | |
443 | + | |
444 | + except Exception as e: | |
445 | + self.handle_file_manager_error(e, "_safe_file_exists") | |
446 | + return False | |
447 | + | |
448 | + def _safe_get_file_info(self, file_path: Path) -> Optional[Dict[str, Any]]: | |
449 | + """Safely get file information using file manager service""" | |
450 | + try: | |
451 | + # Ensure file manager service is available | |
452 | + if not self.file_manager_service.ensure_initialized(): | |
453 | + # Fallback to direct file system access | |
454 | + try: | |
455 | + stat = file_path.stat() | |
456 | + return { | |
457 | + 'size': stat.st_size, | |
458 | + 'modified': stat.st_mtime, | |
459 | + 'is_file': file_path.is_file(), | |
460 | + 'is_dir': file_path.is_dir() | |
461 | + } | |
462 | + except Exception: | |
463 | + return None | |
464 | + | |
465 | + # Use file manager service to get file info | |
466 | + # For now, fallback to direct access since file manager service | |
467 | + # doesn't have a specific get_file_info method | |
468 | + try: | |
469 | + stat = file_path.stat() | |
470 | + return { | |
471 | + 'size': stat.st_size, | |
472 | + 'modified': stat.st_mtime, | |
473 | + 'is_file': file_path.is_file(), | |
474 | + 'is_dir': file_path.is_dir() | |
475 | + } | |
476 | + except Exception: | |
477 | + return None | |
478 | + | |
479 | + except Exception as e: | |
480 | + self.handle_file_manager_error(e, "_safe_get_file_info") | |
481 | + return None | |
482 | + | |
426 | 483 | def _initialize_renderers(self): |
427 | 484 | """Initialize preview renderers in priority order""" |
428 | 485 | # Import here to avoid circular imports |
| @@ -443,13 +500,14 @@ class AdvancedPreviewSystem: | ||
443 | 500 | force_refresh: bool = False) -> PreviewResult: |
444 | 501 | """Generate preview for a file""" |
445 | 502 | try: |
446 | - if not file_path.exists(): | |
503 | + # Use safe file operation to check if file exists | |
504 | + if not self._safe_file_exists(file_path): | |
447 | 505 | return PreviewResult( |
448 | 506 | file_path=file_path, |
449 | 507 | preview_type=PreviewType.UNKNOWN, |
450 | 508 | content="", |
451 | 509 | success=False, |
452 | - error_message="File does not exist" | |
510 | + error_message="File does not exist or is not accessible" | |
453 | 511 | ) |
454 | 512 | |
455 | 513 | # Check cache first |
456 | 514 | |
βΌlaxyfile/ui/enhanced_panels.py
+17-2
| @@ -20,18 +20,24 @@ from rich.console import Console | ||
20 | 20 | |
21 | 21 | from ..core.types import PanelData, EnhancedFileInfo, SortType |
22 | 22 | from ..core.interfaces import UIManagerInterface |
23 | +from ..core.file_manager_service import FileManagerService | |
24 | +from ..core.error_handling_mixin import ErrorHandlingMixin | |
23 | 25 | from .theme import ThemeManager |
24 | 26 | from ..utils.logger import Logger |
25 | 27 | |
26 | 28 | |
27 | -class EnhancedPanelRenderer: | |
29 | +class EnhancedPanelRenderer(ErrorHandlingMixin): | |
28 | 30 | """Enhanced panel rendering with advanced visual features""" |
29 | 31 | |
30 | 32 | def __init__(self, theme_manager: ThemeManager, console: Console): |
33 | + super().__init__() | |
31 | 34 | self.theme_manager = theme_manager |
32 | 35 | self.console = console |
33 | 36 | self.logger = Logger() |
34 | 37 | |
38 | + # Get file manager service | |
39 | + self.file_manager_service = FileManagerService.get_instance() | |
40 | + | |
35 | 41 | # Visual settings |
36 | 42 | self.show_file_icons = True |
37 | 43 | self.show_file_sizes = True |
| @@ -431,7 +437,7 @@ class EnhancedPanelRenderer: | ||
431 | 437 | |
432 | 438 | def render_preview_panel(self, file_path: Optional[Path], content: str = "", |
433 | 439 | preview_type: str = "none") -> Panel: |
434 | - """Render preview panel with file content""" | |
440 | + """Render preview panel with file content using safe file operations""" | |
435 | 441 | try: |
436 | 442 | theme = self.theme_manager.get_current_theme() |
437 | 443 | |
| @@ -443,6 +449,15 @@ class EnhancedPanelRenderer: | ||
443 | 449 | box=ROUNDED |
444 | 450 | ) |
445 | 451 | |
452 | + # Check if file manager service is available | |
453 | + if not self.file_manager_service.ensure_initialized(): | |
454 | + return Panel( | |
455 | + Align.center(Text("Preview unavailable - File manager loading...", style="yellow")), | |
456 | + title="β οΈ Loading Preview", | |
457 | + border_style="yellow", | |
458 | + box=ROUNDED | |
459 | + ) | |
460 | + | |
446 | 461 | # Create preview content based on type |
447 | 462 | if preview_type == "text": |
448 | 463 | preview_content = Text(content[:1000]) # Limit content |
449 | 464 | |
βΌlaxyfile/ui/panels.py
+91-26
| @@ -10,32 +10,56 @@ from rich.text import Text | ||
10 | 10 | from rich.align import Align |
11 | 11 | |
12 | 12 | from ..core.file_manager import FileManager, FileInfo |
13 | +from ..core.file_manager_service import FileManagerService | |
14 | +from ..core.error_handling_mixin import ErrorHandlingMixin | |
13 | 15 | from ..ui.theme import ThemeManager |
14 | 16 | |
15 | -class PanelManager: | |
16 | - """Manages file browser panels""" | |
17 | +class PanelManager(ErrorHandlingMixin): | |
18 | + """Manages file browser panels with robust error handling""" | |
17 | 19 | |
18 | 20 | def __init__(self, theme_manager: ThemeManager): |
21 | + super().__init__() | |
19 | 22 | self.theme_manager = theme_manager |
20 | - self.file_manager = FileManager() | |
23 | + self.file_manager_service = FileManagerService.get_instance() | |
21 | 24 | self.current_selection: Dict[str, int] = {"left": 0, "right": 0} |
22 | 25 | self.selected_files: Dict[str, Set[Path]] = {"left": set(), "right": set()} |
23 | 26 | self.show_hidden = False |
24 | 27 | self.current_paths: Dict[str, Path] = {} |
25 | 28 | |
29 | + # Ensure file manager is initialized | |
30 | + if not self.file_manager_service.ensure_initialized(): | |
31 | + self.logger.warning("File manager service not available during PanelManager initialization") | |
32 | + | |
26 | 33 | def render_panel(self, path: Path, panel_name: str, is_active: bool = False) -> Panel: |
27 | - """Render a file browser panel""" | |
34 | + """Render a file browser panel with robust error handling""" | |
28 | 35 | try: |
29 | - files = self.file_manager.list_directory(path, self.show_hidden) | |
36 | + # Use safe file operation to get directory listing | |
37 | + files = self.safe_file_operation( | |
38 | + operation=lambda: self.file_manager_service.list_directory(path, self.show_hidden), | |
39 | + fallback_value=[], | |
40 | + cache_key=f"list_dir_{path}_{self.show_hidden}", | |
41 | + max_retries=2 | |
42 | + ) | |
30 | 43 | |
44 | + # Handle empty directory or error case | |
31 | 45 | if not files: |
32 | - empty_content = Align.center( | |
33 | - Text("Empty directory", style=self.theme_manager.get_style("info")) | |
34 | - ) | |
35 | - border_style = "bright_blue" if is_active else self.theme_manager.get_style("border") | |
46 | + # Check if this is an error case or truly empty directory | |
47 | + if not self.file_manager_service.is_healthy(): | |
48 | + error_content = Align.center( | |
49 | + Text("File manager unavailable - Loading...", style="yellow") | |
50 | + ) | |
51 | + border_style = "yellow" | |
52 | + title = "β οΈ Loading..." | |
53 | + else: | |
54 | + error_content = Align.center( | |
55 | + Text("Empty directory", style="dim cyan") | |
56 | + ) | |
57 | + border_style = "bright_blue" if is_active else self.theme_manager.get_style("border") | |
58 | + title = f"π {path.name or str(path)}" | |
59 | + | |
36 | 60 | return Panel( |
37 | - empty_content, | |
38 | - title=f"π {path.name or str(path)}", | |
61 | + error_content, | |
62 | + title=title, | |
39 | 63 | border_style=border_style |
40 | 64 | ) |
41 | 65 | |
| @@ -58,6 +82,24 @@ class PanelManager: | ||
58 | 82 | self.current_selection[panel_name] = current_selection |
59 | 83 | |
60 | 84 | for i, file_info in enumerate(files): |
85 | + # Handle different file info formats (dict vs FileInfo object) | |
86 | + if isinstance(file_info, dict): | |
87 | + # Fallback format from error handling | |
88 | + name = file_info.get('name', 'Unknown') | |
89 | + is_dir = file_info.get('is_dir', False) | |
90 | + size = file_info.get('size', 0) | |
91 | + modified = file_info.get('modified', 0) | |
92 | + file_path = file_info.get('path', path / name) | |
93 | + file_type = file_info.get('file_type', 'unknown') | |
94 | + else: | |
95 | + # Normal FileInfo object | |
96 | + name = file_info.name | |
97 | + is_dir = file_info.is_dir | |
98 | + size = file_info.size | |
99 | + modified = file_info.modified | |
100 | + file_path = file_info.path | |
101 | + file_type = getattr(file_info, 'file_type', 'unknown') | |
102 | + | |
61 | 103 | # Selection indicator |
62 | 104 | if i == current_selection and is_active: |
63 | 105 | selector = "βΆ" |
| @@ -65,33 +107,55 @@ class PanelManager: | ||
65 | 107 | elif i == current_selection: |
66 | 108 | selector = "βΊ" |
67 | 109 | name_style = "bold white" |
68 | - elif file_info.path in self.selected_files.get(panel_name, set()): | |
110 | + elif file_path in self.selected_files.get(panel_name, set()): | |
69 | 111 | selector = "β" |
70 | 112 | name_style = "bold yellow" |
71 | 113 | else: |
72 | 114 | selector = " " |
73 | - name_style = self.theme_manager.get_file_style(file_info.file_type) | |
115 | + name_style = self.theme_manager.get_file_style(file_type, False, False) if hasattr(self.theme_manager, 'get_file_style') else "white" | |
74 | 116 | |
75 | 117 | # Icon |
76 | - icon = self.theme_manager.get_file_icon( | |
77 | - file_info.file_type, | |
78 | - file_info.is_dir | |
79 | - ) | |
118 | + if hasattr(self.theme_manager, 'get_file_icon'): | |
119 | + icon = self.theme_manager.get_file_icon(file_type, name, is_dir, False) | |
120 | + else: | |
121 | + icon = "π" if is_dir else "π" | |
80 | 122 | |
81 | 123 | # Name with style |
82 | - name_text = Text(file_info.name, style=name_style) | |
124 | + name_text = Text(name, style=name_style) | |
83 | 125 | |
84 | 126 | # Size |
85 | - if file_info.is_dir: | |
127 | + if is_dir: | |
86 | 128 | size_text = Text("<DIR>", style="dim cyan") |
87 | 129 | else: |
88 | - size_text = Text(file_info.get_size_formatted(), style="dim") | |
130 | + # Format size | |
131 | + if hasattr(file_info, 'get_size_formatted') and not isinstance(file_info, dict): | |
132 | + size_text = Text(file_info.get_size_formatted(), style="dim") | |
133 | + else: | |
134 | + # Manual size formatting | |
135 | + if size < 1024: | |
136 | + size_str = f"{size}B" | |
137 | + elif size < 1024 * 1024: | |
138 | + size_str = f"{size/1024:.1f}K" | |
139 | + elif size < 1024 * 1024 * 1024: | |
140 | + size_str = f"{size/(1024*1024):.1f}M" | |
141 | + else: | |
142 | + size_str = f"{size/(1024*1024*1024):.1f}G" | |
143 | + size_text = Text(size_str, style="dim") | |
89 | 144 | |
90 | 145 | # Modified time |
91 | - modified_text = Text( | |
92 | - file_info.get_modified_formatted(), | |
93 | - style="dim" | |
94 | - ) | |
146 | + if hasattr(file_info, 'get_modified_formatted') and not isinstance(file_info, dict): | |
147 | + modified_text = Text(file_info.get_modified_formatted(), style="dim") | |
148 | + else: | |
149 | + # Manual time formatting | |
150 | + try: | |
151 | + import time as time_module | |
152 | + if isinstance(modified, (int, float)): | |
153 | + modified_str = time_module.strftime("%m/%d %H:%M", time_module.localtime(modified)) | |
154 | + else: | |
155 | + modified_str = str(modified)[:16] if modified else "??/??" | |
156 | + except: | |
157 | + modified_str = "??/??" | |
158 | + modified_text = Text(modified_str, style="dim") | |
95 | 159 | |
96 | 160 | table.add_row( |
97 | 161 | selector, |
| @@ -119,13 +183,14 @@ class PanelManager: | ||
119 | 183 | ) |
120 | 184 | |
121 | 185 | except Exception as e: |
186 | + self.handle_file_manager_error(e, "render_panel") | |
122 | 187 | error_content = Align.center( |
123 | - Text(f"Error: {str(e)}", style=self.theme_manager.get_style("error")) | |
188 | + Text(f"Panel Error: {str(e)}", style="red") | |
124 | 189 | ) |
125 | 190 | return Panel( |
126 | 191 | error_content, |
127 | 192 | title=f"β Error", |
128 | - border_style=self.theme_manager.get_style("error") | |
193 | + border_style="red" | |
129 | 194 | ) |
130 | 195 | |
131 | 196 | def navigate_up(self, panel_name: str): |
132 | 197 | |
βΌpytest.ini
+15-1
| @@ -36,7 +36,13 @@ omit = | ||
36 | 36 | */__pycache__/* |
37 | 37 | */venv/* |
38 | 38 | */env/* |
39 | + */build/* | |
40 | + */dist/* | |
39 | 41 | setup.py |
42 | + laxyfile/__main__.py | |
43 | + | |
44 | +branch = True | |
45 | +parallel = True | |
40 | 46 | |
41 | 47 | [coverage:report] |
42 | 48 | exclude_lines = |
| @@ -50,9 +56,17 @@ exclude_lines = | ||
50 | 56 | if __name__ == .__main__.: |
51 | 57 | class .*\bProtocol\): |
52 | 58 | @(abc\.)?abstractmethod |
59 | + # Type checking imports | |
60 | + if TYPE_CHECKING: | |
53 | 61 | |
54 | 62 | show_missing = True |
55 | 63 | precision = 2 |
64 | +skip_covered = False | |
65 | +skip_empty = True | |
56 | 66 | |
57 | 67 | [coverage:html] |
58 | -directory = htmlcov | |
68 | +directory = htmlcov | |
69 | +title = LaxyFile Coverage Report | |
70 | + | |
71 | +[coverage:xml] | |
72 | +output = coverage.xml | |
59 | 73 | |
βΌrequirements-dev.txt
+1-1
| @@ -64,7 +64,7 @@ memory-profiler>=0.61.0 | ||
64 | 64 | line-profiler>=4.1.0 |
65 | 65 | |
66 | 66 | # Debugging |
67 | -pdb++>=0.10.3 | |
67 | +# pdbpp>=0.10.3 # Optional: enhanced Python debugger (pip install pdbpp) | |
68 | 68 | ipdb>=0.13.13 |
69 | 69 | pudb>=2022.1.3 |
70 | 70 | |
71 | 71 | |
βΌscripts/run_coverage.py
+105-0
| @@ -0,0 +1,105 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Coverage Testing Script | |
4 | + | |
5 | +Run comprehensive coverage tests locally and generate reports. | |
6 | +""" | |
7 | + | |
8 | +import os | |
9 | +import sys | |
10 | +import subprocess | |
11 | +from pathlib import Path | |
12 | + | |
13 | + | |
14 | +def run_command(cmd, description): | |
15 | + """Run a command and handle errors""" | |
16 | + print(f"π {description}...") | |
17 | + try: | |
18 | + result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True) | |
19 | + print(f"β
{description} completed") | |
20 | + return result | |
21 | + except subprocess.CalledProcessError as e: | |
22 | + print(f"β {description} failed:") | |
23 | + print(f" Command: {cmd}") | |
24 | + print(f" Error: {e.stderr}") | |
25 | + return None | |
26 | + | |
27 | + | |
28 | +def main(): | |
29 | + """Main coverage testing function""" | |
30 | + project_root = Path(__file__).parent.parent | |
31 | + os.chdir(project_root) | |
32 | + | |
33 | + print("π§ͺ LaxyFile Coverage Testing") | |
34 | + print("=" * 50) | |
35 | + | |
36 | + # Clean previous coverage data | |
37 | + run_command("coverage erase", "Cleaning previous coverage data") | |
38 | + | |
39 | + # Run unit tests with coverage | |
40 | + result = run_command( | |
41 | + "python -m pytest tests/unit/ -v --cov=laxyfile --cov-append --cov-report=term-missing", | |
42 | + "Running unit tests with coverage" | |
43 | + ) | |
44 | + if not result: | |
45 | + return 1 | |
46 | + | |
47 | + # Run integration tests with coverage | |
48 | + result = run_command( | |
49 | + "python -m pytest tests/integration/ -v --cov=laxyfile --cov-append --cov-report=term-missing", | |
50 | + "Running integration tests with coverage" | |
51 | + ) | |
52 | + if not result: | |
53 | + return 1 | |
54 | + | |
55 | + # Run e2e tests with coverage | |
56 | + result = run_command( | |
57 | + "python -m pytest tests/e2e/ -v --cov=laxyfile --cov-append --cov-report=term-missing", | |
58 | + "Running e2e tests with coverage" | |
59 | + ) | |
60 | + if not result: | |
61 | + return 1 | |
62 | + | |
63 | + # Generate HTML report | |
64 | + run_command("coverage html", "Generating HTML coverage report") | |
65 | + | |
66 | + # Generate XML report for Codecov | |
67 | + run_command("coverage xml", "Generating XML coverage report") | |
68 | + | |
69 | + # Show coverage summary | |
70 | + result = run_command("coverage report", "Generating coverage summary") | |
71 | + | |
72 | + print("\n" + "=" * 50) | |
73 | + print("π Coverage Report Generated!") | |
74 | + print("=" * 50) | |
75 | + print("π HTML Report: htmlcov/index.html") | |
76 | + print("π XML Report: coverage.xml") | |
77 | + print("π Open HTML report: file://" + str(project_root / "htmlcov" / "index.html")) | |
78 | + | |
79 | + # Check if coverage meets minimum threshold | |
80 | + if result and result.stdout: | |
81 | + lines = result.stdout.split('\n') | |
82 | + for line in lines: | |
83 | + if 'TOTAL' in line and '%' in line: | |
84 | + # Extract percentage | |
85 | + parts = line.split() | |
86 | + for part in parts: | |
87 | + if part.endswith('%'): | |
88 | + coverage_pct = int(part[:-1]) | |
89 | + print(f"\nπ Total Coverage: {coverage_pct}%") | |
90 | + | |
91 | + if coverage_pct >= 80: | |
92 | + print("β
Coverage meets target (80%+)") | |
93 | + return 0 | |
94 | + elif coverage_pct >= 70: | |
95 | + print("β οΈ Coverage below target but acceptable (70-79%)") | |
96 | + return 0 | |
97 | + else: | |
98 | + print("β Coverage below minimum threshold (70%)") | |
99 | + return 1 | |
100 | + | |
101 | + return 0 | |
102 | + | |
103 | + | |
104 | +if __name__ == "__main__": | |
105 | + sys.exit(main()) | |
0 | 106 | |
βΌtest_error_fixes.py
+290-0
| @@ -0,0 +1,290 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Test script to verify the LaxyFile error fixes are working correctly. | |
4 | + | |
5 | +This script tests the FileManagerService, ComponentInitializer, and ErrorHandlingMixin | |
6 | +to ensure they properly handle the NoneType errors that were occurring. | |
7 | +""" | |
8 | + | |
9 | +import sys | |
10 | +import os | |
11 | +from pathlib import Path | |
12 | +import time | |
13 | +import threading | |
14 | + | |
15 | +# Add the project root to Python path | |
16 | +project_root = Path(__file__).parent | |
17 | +sys.path.insert(0, str(project_root)) | |
18 | + | |
19 | +def test_file_manager_service(): | |
20 | + """Test FileManagerService singleton and initialization""" | |
21 | + print("π§ͺ Testing FileManagerService...") | |
22 | + | |
23 | + try: | |
24 | + from laxyfile.core.file_manager_service import FileManagerService | |
25 | + | |
26 | + # Test singleton behavior | |
27 | + service1 = FileManagerService.get_instance() | |
28 | + service2 = FileManagerService.get_instance() | |
29 | + | |
30 | + assert service1 is service2, "FileManagerService should be a singleton" | |
31 | + print("β
Singleton behavior working correctly") | |
32 | + | |
33 | + # Test initialization | |
34 | + success = service1.ensure_initialized() | |
35 | + print(f"β
Initialization: {'Success' if success else 'Failed'}") | |
36 | + | |
37 | + # Test health check | |
38 | + is_healthy = service1.is_healthy() | |
39 | + print(f"β
Health check: {'Healthy' if is_healthy else 'Unhealthy'}") | |
40 | + | |
41 | + # Test directory listing | |
42 | + current_dir = Path.cwd() | |
43 | + files = service1.list_directory(current_dir) | |
44 | + print(f"β
Directory listing: Found {len(files)} items in {current_dir}") | |
45 | + | |
46 | + # Test status | |
47 | + status = service1.get_status() | |
48 | + print(f"β
Service status: {status}") | |
49 | + | |
50 | + return True | |
51 | + | |
52 | + except Exception as e: | |
53 | + print(f"β FileManagerService test failed: {e}") | |
54 | + return False | |
55 | + | |
56 | +def test_component_initializer(): | |
57 | + """Test ComponentInitializer proper initialization order""" | |
58 | + print("\nπ§ͺ Testing ComponentInitializer...") | |
59 | + | |
60 | + try: | |
61 | + from laxyfile.core.component_initializer import ComponentInitializer | |
62 | + from laxyfile.core.config import Config | |
63 | + | |
64 | + config = Config() | |
65 | + initializer = ComponentInitializer(config) | |
66 | + | |
67 | + # Test core services initialization | |
68 | + core_success = initializer.initialize_core_services() | |
69 | + print(f"β
Core services initization: {'Success' if core_success else 'Failed'}") | |
70 | + | |
71 | + # Test UI components initialization | |
72 | + ui_success = initializer.initialize_ui_components() | |
73 | + print(f"β
UI components initialization: {'Success' if ui_success else 'Failed'}") | |
74 | + | |
75 | + # Test dependency verification | |
76 | + missing_deps = initializer.verify_dependencies() | |
77 | + print(f"β
Dependency verification: {len(missing_deps)} missing dependencies") | |
78 | + if missing_deps: | |
79 | + print(f" Missing: {missing_deps}") | |
80 | + | |
81 | + # Test initialization status | |
82 | + status = initializer.get_initialization_status() | |
83 | + print(f"β
Initialization status: {status}") | |
84 | + | |
85 | + # Test health check | |
86 | + health = initializer.health_check() | |
87 | + print(f"β
Health check: {'Healthy' if health else 'Unhealthy'}") | |
88 | + | |
89 | + return True | |
90 | + | |
91 | + except Exception as e: | |
92 | + print(f"β ComponentInitializer test failed: {e}") | |
93 | + return False | |
94 | + | |
95 | +def test_error_handling_mixin(): | |
96 | + """Test ErrorHandlingMixin error handling and recovery""" | |
97 | + print("\nπ§ͺ Testing ErrorHandlingMixin...") | |
98 | + | |
99 | + try: | |
100 | + from laxyfile.core.error_handling_mixin import ErrorHandlingMixin | |
101 | + | |
102 | + class TestComponent(ErrorHandlingMixin): | |
103 | + def __init__(self): | |
104 | + super().__init__() | |
105 | + self.call_count = 0 | |
106 | + | |
107 | + def test_operation(self): | |
108 | + self.call_count += 1 | |
109 | + if self.call_count < 3: | |
110 | + raise Exception("Simulated error") | |
111 | + return "Success" | |
112 | + | |
113 | + def failing_operation(self): | |
114 | + raise Exception("Always fails") | |
115 | + | |
116 | + component = TestComponent() | |
117 | + | |
118 | + # Test safe operation with retry | |
119 | + result = component.safe_file_operation( | |
120 | + operation=component.test_operation, | |
121 | + fallback_value="Fallback", | |
122 | + max_retries=3 | |
123 | + ) | |
124 | + print(f"β
Safe operation with retry: {result}") | |
125 | + | |
126 | + # Test safe operation with fallback | |
127 | + result = component.safe_file_operation( | |
128 | + operation=component.failing_operation, | |
129 | + fallback_value="Fallback used", | |
130 | + max_retries=1 | |
131 | + ) | |
132 | + print(f"β
Safe operation with fallback: {result}") | |
133 | + | |
134 | + # Test error statistics | |
135 | + stats = component.get_error_statistics() | |
136 | + print(f"β
Error statistics: {stats}") | |
137 | + | |
138 | + # Test file manager availability check | |
139 | + available = component.is_file_manager_available() | |
140 | + print(f"β
File manager availability: {'Available' if available else 'Unavailable'}") | |
141 | + | |
142 | + return True | |
143 | + | |
144 | + except Exception as e: | |
145 | + print(f"β ErrorHandlingMixin test failed: {e}") | |
146 | + return False | |
147 | + | |
148 | +def test_panel_manager_integration(): | |
149 | + """Test PanelManager integration with FileManagerService""" | |
150 | + print("\nπ§ͺ Testing PanelManager integration...") | |
151 | + | |
152 | + try: | |
153 | + from laxyfile.ui.panels import PanelManager | |
154 | + from laxyfile.ui.theme import ThemeManager | |
155 | + from laxyfile.core.config import Config | |
156 | + | |
157 | + config = Config() | |
158 | + theme_manager = ThemeManager(config) | |
159 | + panel_manager = PanelManager(theme_manager) | |
160 | + | |
161 | + # Test panel rendering | |
162 | + current_dir = Path.cwd() | |
163 | + panel = panel_manager.render_panel(current_dir, "test", is_active=True) | |
164 | + | |
165 | + print(f"β
Panel rendering: Successfully created panel for {current_dir}") | |
166 | + print(f" Panel type: {type(panel)}") | |
167 | + | |
168 | + # Test error handling in panel manager | |
169 | + non_existent_path = Path("/non/existent/path") | |
170 | + error_panel = panel_manager.render_panel(non_existent_path, "error_test", is_active=False) | |
171 | + | |
172 | + print(f"β
Error handling: Successfully handled non-existent path") | |
173 | + print(f" Error panel type: {type(error_panel)}") | |
174 | + | |
175 | + return True | |
176 | + | |
177 | + except Exception as e: | |
178 | + print(f"β PanelManager integration test failed: {e}") | |
179 | + return False | |
180 | + | |
181 | +def test_thread_safety(): | |
182 | + """Test thread safety of FileManagerService""" | |
183 | + print("\nπ§ͺ Testing thread safety...") | |
184 | + | |
185 | + try: | |
186 | + from laxyfile.core.file_manager_service import FileManagerService | |
187 | + | |
188 | + results = [] | |
189 | + errors = [] | |
190 | + | |
191 | + def worker_thread(thread_id): | |
192 | + try: | |
193 | + service = FileManagerService.get_instance() | |
194 | + current_dir = Path.cwd() | |
195 | + files = service.list_directory(current_dir) | |
196 | + results.append((thread_id, len(files))) | |
197 | + except Exception as e: | |
198 | + errors.append((thread_id, str(e))) | |
199 | + | |
200 | + # Create multiple threads | |
201 | + threads = [] | |
202 | + for i in range(5): | |
203 | + thread = threading.Thread(target=worker_thread, args=(i,)) | |
204 | + threads.append(thread) | |
205 | + thread.start() | |
206 | + | |
207 | + # Wait for all threads to complete | |
208 | + for thread in threads: | |
209 | + thread.join() | |
210 | + | |
211 | + print(f"β
Thread safety: {len(results)} successful operations, {len(errors)} errors") | |
212 | + if errors: | |
213 | + print(f" Errors: {errors}") | |
214 | + | |
215 | + return len(errors) == 0 | |
216 | + | |
217 | + except Exception as e: | |
218 | + print(f"β Thread safety test failed: {e}") | |
219 | + return False | |
220 | + | |
221 | +def test_recovery_mechanisms(): | |
222 | + """Test automatic recovery mechanisms""" | |
223 | + print("\nπ§ͺ Testing recovery mechanisms...") | |
224 | + | |
225 | + try: | |
226 | + from laxyfile.core.file_manager_service import FileManagerService | |
227 | + | |
228 | + service = FileManagerService.get_instance() | |
229 | + | |
230 | + # Test reinitialization | |
231 | + reinit_success = service.reinitialize(force=True) | |
232 | + print(f"β
Reinitialization: {'Success' if reinit_success else 'Failed'}") | |
233 | + | |
234 | + # Test health after reinitialization | |
235 | + is_healthy = service.is_healthy() | |
236 | + print(f"β
Health after reinit: {'Healthy' if is_healthy else 'Unhealthy'}") | |
237 | + | |
238 | + # Test service reset and recovery | |
239 | + FileManagerService.reset() | |
240 | + new_service = FileManagerService.get_instance() | |
241 | + recovery_success = new_service.ensure_initialized() | |
242 | + print(f"β
Service recovery: {'Success' if recovery_success else 'Failed'}") | |
243 | + | |
244 | + return True | |
245 | + | |
246 | + except Exception as e: | |
247 | + print(f"β Recovery mechanisms test failed: {e}") | |
248 | + return False | |
249 | + | |
250 | +def main(): | |
251 | + """Run all tests""" | |
252 | + print("π Starting LaxyFile Error Fixes Test Suite") | |
253 | + print("=" * 50) | |
254 | + | |
255 | + tests = [ | |
256 | + test_file_manager_service, | |
257 | + test_component_initializer, | |
258 | + test_error_handling_mixin, | |
259 | + test_panel_manager_integration, | |
260 | + test_thread_safety, | |
261 | + test_recovery_mechanisms | |
262 | + ] | |
263 | + | |
264 | + passed = 0 | |
265 | + failed = 0 | |
266 | + | |
267 | + for test in tests: | |
268 | + try: | |
269 | + if test(): | |
270 | + passed += 1 | |
271 | + else: | |
272 | + failed += 1 | |
273 | + except Exception as e: | |
274 | + print(f"β Test {test.__name__} crashed: {e}") | |
275 | + failed += 1 | |
276 | + | |
277 | + time.sleep(0.1) # Brief pause between tests | |
278 | + | |
279 | + print("\n" + "=" * 50) | |
280 | + print(f"π Test Results: {passed} passed, {failed} failed") | |
281 | + | |
282 | + if failed == 0: | |
283 | + print("π All tests passed! Error fixes are working correctly.") | |
284 | + return 0 | |
285 | + else: | |
286 | + print("β οΈ Some tests failed. Please check the error messages above.") | |
287 | + return 1 | |
288 | + | |
289 | +if __name__ == "__main__": | |
290 | + sys.exit(main()) | |
0 | 291 | |
βΌtest_flicker_fix.py
+178-0
| @@ -0,0 +1,178 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Test script to verify the flicker fix for LaxyFile navigation | |
4 | +""" | |
5 | + | |
6 | +import sys | |
7 | +import time | |
8 | +from pathlib import Path | |
9 | + | |
10 | +# Add the laxyfile package to the path | |
11 | +sys.path.insert(0, str(Path(__file__).parent)) | |
12 | + | |
13 | +def test_navigation_performance(): | |
14 | + """Test that navigation doesn't cause excessive file system calls""" | |
15 | + print("π§ͺ Testing Navigation Performance and Anti-Flicker...") | |
16 | + | |
17 | + try: | |
18 | + from laxyfile.main import LaxyFileApp | |
19 | + | |
20 | + # Create LaxyFile app | |
21 | + app = LaxyFileApp() | |
22 | + print(" β
LaxyFile app created successfully") | |
23 | + | |
24 | + # Test cached file list functionality | |
25 | + print(" π Testing cached file list functionality...") | |
26 | + | |
27 | + # First call should populate cache | |
28 | + start_time = time.time() | |
29 | + files1 = app._get_cached_file_list("left") | |
30 | + first_call_time = time.time() - start_time | |
31 | + | |
32 | + # Second call should use cache (much faster) | |
33 | + start_time = time.time() | |
34 | + files2 = app._get_cached_file_list("left") | |
35 | + second_call_time = time.time() - start_time | |
36 | + | |
37 | + print(f" π First call: {first_call_time:.4f}s, Second call: {second_call_time:.4f}s") | |
38 | + | |
39 | + if second_call_time < first_call_time: | |
40 | + print(" β
Caching is working - second call is faster") | |
41 | + else: | |
42 | + print(" β οΈ Caching may not be working optimally") | |
43 | + | |
44 | + # Test that both calls return the same data | |
45 | + if len(files1) == len(files2): | |
46 | + print(f" β
Cache consistency verified - both calls returned {len(files1)} files") | |
47 | + else: | |
48 | + print(f" β Cache inconsistency - first: {len(files1)}, second: {len(files2)}") | |
49 | + return False | |
50 | + | |
51 | + # Test display dirty flag functionality | |
52 | + print(" π Testing display dirty flag...") | |
53 | + | |
54 | + app._display_dirty = False | |
55 | + print(f" π Initial display dirty state: {app._display_dirty}") | |
56 | + | |
57 | + # Simulate navigation that should set dirty flag | |
58 | + app._display_dirty = True | |
59 | + print(f" π After navigation simulation: {app._display_dirty}") | |
60 | + | |
61 | + if app._display_dirty: | |
62 | + print(" β
Display dirty flag working correctly") | |
63 | + else: | |
64 | + print(" β Display dirty flag not working") | |
65 | + return False | |
66 | + | |
67 | + # Test debounce settings | |
68 | + print(" π Testing debounce configuration...") | |
69 | + | |
70 | + debounce_delay = app._navigation_debounce_delay | |
71 | + cache_expiry = app._cache_expiry | |
72 | + | |
73 | + print(f" π Navigation debounce delay: {debounce_delay}s") | |
74 | + print(f" π Cache expiry time: {cache_expiry}s") | |
75 | + | |
76 | + if debounce_delay > 0 and cache_expiry > 0: | |
77 | + print(" β
Anti-flicker settings configured correctly") | |
78 | + else: | |
79 | + print(" β Anti-flicker settings not configured properly") | |
80 | + return False | |
81 | + | |
82 | + print(" β
Navigation performance test completed successfully") | |
83 | + return True | |
84 | + | |
85 | + except Exception as e: | |
86 | + print(f" β Navigation performance test failed: {e}") | |
87 | + import traceback | |
88 | + traceback.print_exc() | |
89 | + return False | |
90 | + | |
91 | +def test_display_optimization(): | |
92 | + """Test display optimization features""" | |
93 | + print("\nπ§ͺ Testing Display Optimization...") | |
94 | + | |
95 | + try: | |
96 | + from laxyfile.main import LaxyFileApp | |
97 | + | |
98 | + # Create LaxyFile app | |
99 | + app = LaxyFileApp() | |
100 | + print(" β
LaxyFile app created successfully") | |
101 | + | |
102 | + # Test that cached file lists are properly managed | |
103 | + print(" π Testing cache management...") | |
104 | + | |
105 | + # Fill cache with multiple entries | |
106 | + for i in range(5): | |
107 | + cache_key = f"test_panel_{i}" | |
108 | + app.current_path["left"] = Path.cwd() # Use current directory | |
109 | + files = app._get_cached_file_list("left") | |
110 | + print(f" π Cache entry {i}: {len(files)} files") | |
111 | + | |
112 | + cache_size = len(app._cached_file_lists) | |
113 | + print(f" π Total cache entries: {cache_size}") | |
114 | + | |
115 | + if cache_size > 0: | |
116 | + print(" β
File list caching is working") | |
117 | + else: | |
118 | + print(" β File list caching not working") | |
119 | + return False | |
120 | + | |
121 | + # Test cache expiry mechanism | |
122 | + print(" π Testing cache expiry...") | |
123 | + | |
124 | + # Manually expire a cache entry | |
125 | + if app._cached_file_lists: | |
126 | + first_key = list(app._cached_file_lists.keys())[0] | |
127 | + app._cached_file_lists[first_key]['timestamp'] = time.time() - app._cache_expiry - 1 | |
128 | + print(" π Manually expired one cache entry") | |
129 | + | |
130 | + # Request files again - should refresh expired entry | |
131 | + files = app._get_cached_file_list("left") | |
132 | + print(f" π Files after cache expiry test: {len(files)}") | |
133 | + | |
134 | + print(" β
Display optimization test completed successfully") | |
135 | + return True | |
136 | + | |
137 | + except Exception as e: | |
138 | + print(f" β Display optimization test failed: {e}") | |
139 | + import traceback | |
140 | + traceback.print_exc() | |
141 | + return False | |
142 | + | |
143 | +def main(): | |
144 | + """Run all anti-flicker tests""" | |
145 | + print("π Starting LaxyFile Anti-Flicker Tests\n") | |
146 | + | |
147 | + test_results = [] | |
148 | + | |
149 | + # Test navigation performance | |
150 | + result1 = test_navigation_performance() | |
151 | + test_results.append(result1) | |
152 | + | |
153 | + # Test display optimization | |
154 | + result2 = test_display_optimization() | |
155 | + test_results.append(result2) | |
156 | + | |
157 | + # Summary | |
158 | + print(f"\nπ Test Results:") | |
159 | + print(f" β
Passed: {sum(test_results)}") | |
160 | + print(f" β Failed: {len(test_results) - sum(test_results)}") | |
161 | + | |
162 | + if all(test_results): | |
163 | + print("\nπ All tests passed! Anti-flicker optimizations are working correctly.") | |
164 | + print("\nπ Flicker Fixes Applied:") | |
165 | + print(" β’ Implemented navigation debouncing (50ms delay)") | |
166 | + print(" β’ Added file list caching to prevent excessive directory reads") | |
167 | + print(" β’ Reduced refresh rate from 60 FPS to 30 FPS") | |
168 | + print(" β’ Added display dirty flag for optimized updates") | |
169 | + print(" β’ Cached file operations for navigation keys") | |
170 | + print(" β’ Optimized async main loop with proper timing") | |
171 | + return 0 | |
172 | + else: | |
173 | + print("\nπ₯ Some tests failed. Please check the error messages above.") | |
174 | + return 1 | |
175 | + | |
176 | +if __name__ == "__main__": | |
177 | + exit_code = main() | |
178 | + sys.exit(exit_code) | |
0 | 179 | |
βΌtest_media_viewer_fix.py
+145-0
| @@ -0,0 +1,145 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Test script to verify the media viewer NoneType error fix | |
4 | +""" | |
5 | + | |
6 | +import sys | |
7 | +from pathlib import Path | |
8 | + | |
9 | +# Add the laxyfile package to the path | |
10 | +sys.path.insert(0, str(Path(__file__).parent)) | |
11 | + | |
12 | +def test_media_viewer_error_handling(): | |
13 | + """Test that media viewer handles initialization errors gracefully""" | |
14 | + print("π§ͺ Testing Media Viewer Error Handling...") | |
15 | + | |
16 | + try: | |
17 | + from laxyfile.main import LaxyFileApp | |
18 | + | |
19 | + # Create LaxyFile app | |
20 | + app = LaxyFileApp() | |
21 | + print(" β
LaxyFile app created successfully") | |
22 | + | |
23 | + # Test create_preview_panel method - this should not crash even if media viewer is None | |
24 | + print(" π Testing create_preview_panel method...") | |
25 | + | |
26 | + try: | |
27 | + preview_panel = app.create_preview_panel() | |
28 | + print(" β
Preview panel created successfully without crashing") | |
29 | + | |
30 | + # Check if the panel has content (even if it's an error message) | |
31 | + if hasattr(preview_panel, 'renderable'): | |
32 | + print(" β
Preview panel has renderable content") | |
33 | + else: | |
34 | + print(" β
Preview panel created (may be fallback content)") | |
35 | + | |
36 | + except Exception as e: | |
37 | + print(f" β Preview panel creation failed: {e}") | |
38 | + return False | |
39 | + | |
40 | + # Test that media viewer lazy initialization works | |
41 | + print(" π Testing media viewer lazy initialization...") | |
42 | + | |
43 | + try: | |
44 | + media_viewer = app._get_media_viewer() | |
45 | + if media_viewer is not None: | |
46 | + print(" β
Media viewer initialized successfully") | |
47 | + else: | |
48 | + print(" β οΈ Media viewer is None (expected if dependencies missing)") | |
49 | + | |
50 | + except Exception as e: | |
51 | + print(f" β Media viewer initialization failed: {e}") | |
52 | + return False | |
53 | + | |
54 | + print(" β
Media viewer error handling test completed successfully") | |
55 | + return True | |
56 | + | |
57 | + except Exception as e: | |
58 | + print(f" β Media viewer test failed: {e}") | |
59 | + import traceback | |
60 | + traceback.print_exc() | |
61 | + return False | |
62 | + | |
63 | +def test_file_manager_service_integration(): | |
64 | + """Test that file manager service integration works correctly""" | |
65 | + print("\nπ§ͺ Testing File Manager Service Integration...") | |
66 | + | |
67 | + try: | |
68 | + from laxyfile.main import LaxyFileApp | |
69 | + from laxyfile.core.file_manager_service import FileManagerService | |
70 | + | |
71 | + # Reset file manager service to test initialization | |
72 | + FileManagerService.reset() | |
73 | + | |
74 | + # Create LaxyFile app | |
75 | + app = LaxyFileApp() | |
76 | + print(" β
LaxyFile app created successfully") | |
77 | + | |
78 | + # Test file manager property access | |
79 | + print(" π Testing file manager property access...") | |
80 | + | |
81 | + try: | |
82 | + file_manager = app.file_manager | |
83 | + if file_manager is not None: | |
84 | + print(" β
File manager accessed successfully through property") | |
85 | + else: | |
86 | + print(" β οΈ File manager is None (may need initialization)") | |
87 | + | |
88 | + except Exception as e: | |
89 | + print(f" β File manager property access failed: {e}") | |
90 | + return False | |
91 | + | |
92 | + # Test safe file operations | |
93 | + print(" π Testing safe file operations...") | |
94 | + | |
95 | + try: | |
96 | + # This should not crash even if file manager is not available | |
97 | + test_result = app.safe_file_operation( | |
98 | + operation=lambda: [], | |
99 | + fallback_value=[], | |
100 | + cache_key="test_operation" | |
101 | + ) | |
102 | + print(" β
Safe file operation completed successfully") | |
103 | + | |
104 | + except Exception as e: | |
105 | + print(f" β Safe file operation failed: {e}") | |
106 | + return False | |
107 | + | |
108 | + print(" β
File manager service integration test completed successfully") | |
109 | + return True | |
110 | + | |
111 | + except Exception as e: | |
112 | + print(f" β File manager service integration test failed: {e}") | |
113 | + import traceback | |
114 | + traceback.print_exc() | |
115 | + return False | |
116 | + | |
117 | +def main(): | |
118 | + """Run all tests""" | |
119 | + print("π Starting LaxyFile Media Viewer Error Fix Tests\n") | |
120 | + | |
121 | + test_results = [] | |
122 | + | |
123 | + # Test media viewer error handling | |
124 | + result1 = test_media_viewer_error_handling() | |
125 | + test_results.append(result1) | |
126 | + | |
127 | + # Test file manager service integration | |
128 | + result2 = test_file_manager_service_integration() | |
129 | + test_results.append(result2) | |
130 | + | |
131 | + # Summary | |
132 | + print(f"\nπ Test Results:") | |
133 | + print(f" β
Passed: {sum(test_results)}") | |
134 | + print(f" β Failed: {len(test_results) - sum(test_results)}") | |
135 | + | |
136 | + if all(test_results): | |
137 | + print("\nπ All tests passed! Media viewer and file manager error handling is working correctly.") | |
138 | + return 0 | |
139 | + else: | |
140 | + print("\nπ₯ Some tests failed. Please check the error messages above.") | |
141 | + return 1 | |
142 | + | |
143 | +if __name__ == "__main__": | |
144 | + exit_code = main() | |
145 | + sys.exit(exit_code) | |
0 | 146 | |
βΌtest_nonetype_fix.py
+176-0
| @@ -0,0 +1,176 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Specific test to verify the NoneType error fix. | |
4 | + | |
5 | +This test simulates the original error condition and verifies it's been resolved. | |
6 | +""" | |
7 | + | |
8 | +import sys | |
9 | +from pathlib import Path | |
10 | + | |
11 | +# Add the project root to Python path | |
12 | +project_root = Path(__file__).parent | |
13 | +sys.path.insert(0, str(project_root)) | |
14 | + | |
15 | +def test_original_nonetype_error(): | |
16 | + """Test that the original 'NoneType' object has no attribute 'list_directory' error is fixed""" | |
17 | + print("π§ͺ Testing original NoneType error fix...") | |
18 | + | |
19 | + try: | |
20 | + # This simulates the original error scenario where file_manager was None | |
21 | + from laxyfile.ui.panels import PanelManager | |
22 | + from laxyfile.ui.theme import ThemeManager | |
23 | + from laxyfile.core.config import Config | |
24 | + | |
25 | + # Initialize components | |
26 | + config = Config() | |
27 | + theme_manager = ThemeManager(config) | |
28 | + panel_manager = PanelManager(theme_manager) | |
29 | + | |
30 | + # This would previously cause: 'NoneType' object has no attribute 'list_directory' | |
31 | + current_dir = Path.cwd() | |
32 | + panel = panel_manager.render_panel(current_dir, "test", is_active=True) | |
33 | + | |
34 | + print("β
Successfully created panel without NoneType error") | |
35 | + print(f" Panel created for: {current_dir}") | |
36 | + print(f" Panel type: {type(panel)}") | |
37 | + | |
38 | + # Test with multiple rapid calls (stress test) | |
39 | + for i in range(5): | |
40 | + panel = panel_manager.render_panel(current_dir, f"test_{i}", is_active=(i % 2 == 0)) | |
41 | + | |
42 | + print("β
Multiple rapid panel creations successful") | |
43 | + | |
44 | + return True | |
45 | + | |
46 | + except AttributeError as e: | |
47 | + if "'NoneType' object has no attribute" in str(e): | |
48 | + print(f"β Original NoneType error still occurs: {e}") | |
49 | + return False | |
50 | + else: | |
51 | + print(f"β Different AttributeError: {e}") | |
52 | + return False | |
53 | + except Exception as e: | |
54 | + print(f"β Unexpected error: {e}") | |
55 | + return False | |
56 | + | |
57 | +def test_file_manager_service_robustness(): | |
58 | + """Test FileManagerService robustness under various conditions""" | |
59 | + print("\nπ§ͺ Testing FileManagerService robustness...") | |
60 | + | |
61 | + try: | |
62 | + from laxyfile.core.file_manager_service import FileManagerService | |
63 | + | |
64 | + # Test getting service multiple times | |
65 | + services = [FileManagerService.get_instance() for _ in range(10)] | |
66 | + | |
67 | + # Verify all are the same instance | |
68 | + assert all(service is services[0] for service in services), "All instances should be the same" | |
69 | + print("β
Singleton consistency verified") | |
70 | + | |
71 | + # Test directory listing with various paths | |
72 | + service = services[0] | |
73 | + | |
74 | + # Test current directory | |
75 | + current_files = service.list_directory(Path.cwd()) | |
76 | + print(f"β
Current directory: {len(current_files)} files") | |
77 | + | |
78 | + # Test parent directory | |
79 | + parent_files = service.list_directory(Path.cwd().parent) | |
80 | + print(f"β
Parent directory: {len(parent_files)} files") | |
81 | + | |
82 | + # Test non-existent directory (should return empty list, not crash) | |
83 | + non_existent_files = service.list_directory(Path("/non/existent/path")) | |
84 | + print(f"β
Non-existent directory handled: {len(non_existent_files)} files") | |
85 | + | |
86 | + # Test health status | |
87 | + is_healthy = service.is_healthy() | |
88 | + print(f"β
Service health: {'Healthy' if is_healthy else 'Unhealthy'}") | |
89 | + | |
90 | + return True | |
91 | + | |
92 | + except Exception as e: | |
93 | + print(f"β FileManagerService robustness test failed: {e}") | |
94 | + return False | |
95 | + | |
96 | +def test_error_recovery(): | |
97 | + """Test error recovery mechanisms""" | |
98 | + print("\nπ§ͺ Testing error recovery...") | |
99 | + | |
100 | + try: | |
101 | + from laxyfile.core.error_handling_mixin import ErrorHandlingMixin | |
102 | + from laxyfile.core.file_manager_service import FileManagerService | |
103 | + | |
104 | + class TestComponent(ErrorHandlingMixin): | |
105 | + def __init__(self): | |
106 | + super().__init__() | |
107 | + | |
108 | + def test_file_operation(self): | |
109 | + # This should use the FileManagerService safely | |
110 | + service = FileManagerService.get_instance() | |
111 | + return service.list_directory(Path.cwd()) | |
112 | + | |
113 | + component = TestComponent() | |
114 | + | |
115 | + # Test safe file operation | |
116 | + result = component.safe_file_operation( | |
117 | + operation=component.test_file_operation, | |
118 | + fallback_value=[], | |
119 | + max_retries=2 | |
120 | + ) | |
121 | + | |
122 | + print(f"β
Safe file operation: {len(result)} files retrieved") | |
123 | + | |
124 | + # Test file manager availability | |
125 | + available = component.is_file_manager_available() | |
126 | + print(f"β
File manager availability: {'Available' if available else 'Unavailable'}") | |
127 | + | |
128 | + # Test recovery attempt | |
129 | + recovery_success = component.ensure_file_manager_available() | |
130 | + print(f"β
Recovery mechanism: {'Success' if recovery_success else 'Failed'}") | |
131 | + | |
132 | + return True | |
133 | + | |
134 | + except Exception as e: | |
135 | + print(f"β Error recovery test failed: {e}") | |
136 | + return False | |
137 | + | |
138 | +def main(): | |
139 | + """Run the NoneType error fix verification tests""" | |
140 | + print("π LaxyFile NoneType Error Fix Verification") | |
141 | + print("=" * 50) | |
142 | + | |
143 | + tests = [ | |
144 | + test_original_nonetype_error, | |
145 | + test_file_manager_service_robustness, | |
146 | + test_error_recovery | |
147 | + ] | |
148 | + | |
149 | + passed = 0 | |
150 | + failed = 0 | |
151 | + | |
152 | + for test in tests: | |
153 | + try: | |
154 | + if test(): | |
155 | + passed += 1 | |
156 | + else: | |
157 | + failed += 1 | |
158 | + except Exception as e: | |
159 | + print(f"β Test {test.__name__} crashed: {e}") | |
160 | + failed += 1 | |
161 | + | |
162 | + print("\n" + "=" * 50) | |
163 | + print(f"π Test Results: {passed} passed, {failed} failed") | |
164 | + | |
165 | + if failed == 0: | |
166 | + print("π SUCCESS: The original NoneType error has been fixed!") | |
167 | + print(" - FileManagerService provides robust singleton access") | |
168 | + print(" - ErrorHandlingMixin provides fallback behavior") | |
169 | + print(" - PanelManager safely handles file manager operations") | |
170 | + return 0 | |
171 | + else: | |
172 | + print("β οΈ Some tests failed. The fix may not be complete.") | |
173 | + return 1 | |
174 | + | |
175 | +if __name__ == "__main__": | |
176 | + sys.exit(main()) | |
0 | 177 | |
βΌtest_preview_fix.py
+128-0
| @@ -0,0 +1,128 @@ | ||
1 | +#!/usr/bin/env python3 | |
2 | +""" | |
3 | +Test script to verify the preview system NoneType error fix | |
4 | +""" | |
5 | + | |
6 | +import asyncio | |
7 | +import sys | |
8 | +from pathlib import Path | |
9 | + | |
10 | +# Add the laxyfile package to the path | |
11 | +sys.path.insert(0, str(Path(__file__).parent)) | |
12 | + | |
13 | +async def test_preview_system_error_handling(): | |
14 | + """Test that preview system handles file manager errors gracefully""" | |
15 | + print("π§ͺ Testing Preview System Error Handling...") | |
16 | + | |
17 | + try: | |
18 | + from laxyfile.preview.preview_system import AdvancedPreviewSystem, PreviewConfig | |
19 | + from laxyfile.core.file_manager_service import FileManagerService | |
20 | + | |
21 | + # Reset file manager service to simulate uninitialized state | |
22 | + FileManagerService.reset() | |
23 | + | |
24 | + # Create preview system | |
25 | + config = PreviewConfig() | |
26 | + preview_system = AdvancedPreviewSystem(config) | |
27 | + | |
28 | + # Initialize preview system | |
29 | + await preview_system.initialize() | |
30 | + print(" β
Preview system initialized successfully") | |
31 | + | |
32 | + # Test preview generation with a file that exists | |
33 | + test_file = Path(__file__) | |
34 | + print(f" π Testing preview generation for: {test_file.name}") | |
35 | + | |
36 | + # This should not crash even if file manager is not properly initialized | |
37 | + preview_result = await preview_system.generate_preview(test_file) | |
38 | + | |
39 | + if preview_result.success: | |
40 | + print(f" β
Preview generated successfully: {len(preview_result.content)} characters") | |
41 | + else: | |
42 | + print(f" β οΈ Preview generation failed (expected): {preview_result.error_message}") | |
43 | + | |
44 | + # Test with non-existent file | |
45 | + fake_file = Path("non_existent_file.txt") | |
46 | + print(f" π Testing preview generation for non-existent file: {fake_file.name}") | |
47 | + | |
48 | + preview_result = await preview_system.generate_preview(fake_file) | |
49 | + | |
50 | + if not preview_result.success: | |
51 | + print(f" β
Non-existent file handled gracefully: {preview_result.error_message}") | |
52 | + else: | |
53 | + print(f" β οΈ Unexpected success for non-existent file") | |
54 | + | |
55 | + print(" β
Preview system error handling test completed successfully") | |
56 | + return True | |
57 | + | |
58 | + except Exception as e: | |
59 | + print(f" β Preview system test failed: {e}") | |
60 | + import traceback | |
61 | + traceback.print_exc() | |
62 | + return False | |
63 | + | |
64 | +async def test_enhanced_panels_error_handling(): | |
65 | + """Test that enhanced panels handle file manager errors gracefully""" | |
66 | + print("\nπ§ͺ Testing Enhanced Panels Error Handling...") | |
67 | + | |
68 | + try: | |
69 | + from laxyfile.ui.enhanced_panels import EnhancedPanelRenderer | |
70 | + from laxyfile.ui.theme import ThemeManager | |
71 | + from laxyfile.core.config import Config | |
72 | + from rich.console import Console | |
73 | + | |
74 | + # Create components | |
75 | + console = Console() | |
76 | + config = Config() | |
77 | + theme_manager = ThemeManager(config) | |
78 | + panel_renderer = EnhancedPanelRenderer(theme_manager, console) | |
79 | + | |
80 | + print(" β
Enhanced panel renderer created successfully") | |
81 | + | |
82 | + # Test preview panel rendering with no file | |
83 | + preview_panel = panel_renderer.render_preview_panel(None) | |
84 | + print(" β
Preview panel rendered successfully with no file") | |
85 | + | |
86 | + # Test preview panel rendering with a file | |
87 | + test_file = Path(__file__) | |
88 | + preview_panel = panel_renderer.render_preview_panel(test_file, "Test content", "text") | |
89 | + print(" β
Preview panel rendered successfully with file") | |
90 | + | |
91 | + print(" β
Enhanced panels error handling test completed successfully") | |
92 | + return True | |
93 | + | |
94 | + except Exception as e: | |
95 | + print(f" β Enhanced panels test failed: {e}") | |
96 | + import traceback | |
97 | + traceback.print_exc() | |
98 | + return False | |
99 | + | |
100 | +async def main(): | |
101 | + """Run all tests""" | |
102 | + print("π Starting LaxyFile Preview System Error Fix Tests\n") | |
103 | + | |
104 | + test_results = [] | |
105 | + | |
106 | + # Test preview system | |
107 | + result1 = await test_preview_system_error_handling() | |
108 | + test_results.append(result1) | |
109 | + | |
110 | + # Test enhanced panels | |
111 | + result2 = await test_enhanced_panels_error_handling() | |
112 | + test_results.append(result2) | |
113 | + | |
114 | + # Summary | |
115 | + print(f"\nπ Test Results:") | |
116 | + print(f" β
Passed: {sum(test_results)}") | |
117 | + print(f" β Failed: {len(test_results) - sum(test_results)}") | |
118 | + | |
119 | + if all(test_results): | |
120 | + print("\nπ All tests passed! Preview system error handling is working correctly.") | |
121 | + return 0 | |
122 | + else: | |
123 | + print("\nπ₯ Some tests failed. Please check the error messages above.") | |
124 | + return 1 | |
125 | + | |
126 | +if __name__ == "__main__": | |
127 | + exit_code = asyncio.run(main()) | |
128 | + sys.exit(exit_code) | |
0 | 129 | |