Cleanup: KI-spezifische, veraltete und experimentelle Dateien entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c867305f1c
commit
6268ea6693
8 changed files with 0 additions and 1684 deletions
90
.github/workflows/coverage.yml
vendored
90
.github/workflows/coverage.yml
vendored
|
|
@ -1,90 +0,0 @@
|
|||
name: Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install coverage
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
run: |
|
||||
coverage run -m unittest discover -s tests -p "test_*.py"
|
||||
coverage report -m
|
||||
coverage html
|
||||
|
||||
- name: Generate Coverage Badge
|
||||
run: |
|
||||
COVERAGE=$(coverage report | grep TOTAL | awk '{print $4}' | sed 's/%//')
|
||||
echo "Coverage: $COVERAGE%"
|
||||
|
||||
# Create badge color
|
||||
if (( $(echo "$COVERAGE >= 90" | bc -l) )); then
|
||||
COLOR="brightgreen"
|
||||
elif (( $(echo "$COVERAGE >= 75" | bc -l) )); then
|
||||
COLOR="green"
|
||||
elif (( $(echo "$COVERAGE >= 60" | bc -l) )); then
|
||||
COLOR="yellow"
|
||||
else
|
||||
COLOR="red"
|
||||
fi
|
||||
|
||||
echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV
|
||||
echo "COLOR=$COLOR" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Coverage Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: htmlcov/
|
||||
retention-days: 30
|
||||
|
||||
- name: Coverage Summary
|
||||
run: |
|
||||
echo "# Coverage Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Detailed Coverage" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
coverage report >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Full HTML report available as artifact." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Comment Coverage on PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const coverage = process.env.COVERAGE;
|
||||
const color = process.env.COLOR;
|
||||
const body = `## 📊 Coverage Report\n\n\n\nDetailed report available in artifacts.`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: body
|
||||
});
|
||||
116
.github/workflows/tests.yml
vendored
116
.github/workflows/tests.yml
vendored
|
|
@ -1,116 +0,0 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
python -m unittest tests.test_lesson_service tests.test_statistics_service tests.test_settings_service -v
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
python -m unittest tests.test_integration_progress tests.test_integration_statistics -v
|
||||
|
||||
- name: Run All Tests
|
||||
run: |
|
||||
python -m unittest discover -s tests -p "test_*.py" -v
|
||||
|
||||
test-summary:
|
||||
name: Test Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Generate Test Summary
|
||||
run: |
|
||||
echo "# Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Test Execution" >> $GITHUB_STEP_SUMMARY
|
||||
python -m unittest discover -s tests -p "test_*.py" -v 2>&1 | tee test_output.txt
|
||||
|
||||
# Parse test results
|
||||
TESTS_RUN=$(grep -o "Ran [0-9]* test" test_output.txt | grep -o "[0-9]*" || echo "0")
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tests Run:** $TESTS_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if grep -q "OK" test_output.txt; then
|
||||
echo "**Status:** ✅ All tests passed!" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Status:** ❌ Some tests failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Test Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
tail -20 test_output.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
lint:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install flake8
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# Stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# Exit-zero treats all errors as warnings
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
continue-on-error: true
|
||||
287
CLAUDE.md
287
CLAUDE.md
|
|
@ -1,287 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Typewriter Tutor (Tipptrainer) is a web-based typing tutor application built with Flask that helps users learn touch typing. The application features lesson-based training, real-time statistics, progress tracking, and an adaptive metronome system to guide typing speed.
|
||||
|
||||
**Important Context**: This is a minimal practice project created to learn Aider (an AI coding tool). The codebase is in German, and this is intentional.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: Python 3 with Flask
|
||||
- **ORM**: SQLAlchemy with SQLite database
|
||||
- **Frontend**: HTML, Bootstrap 5, JavaScript
|
||||
- **Charts**: Chart.js for statistics visualization
|
||||
- **Icons**: Lucide Icons
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Running the Application
|
||||
|
||||
```bash
|
||||
# Start the Flask development server
|
||||
python app.py
|
||||
```
|
||||
|
||||
The application runs on `http://localhost:5000` by default.
|
||||
|
||||
### Database Setup
|
||||
|
||||
**WICHTIG**: Die Anwendung verwendet Alembic für Datenbank-Migrationen.
|
||||
|
||||
**Erste Installation**:
|
||||
```bash
|
||||
# Setup-Script ausführen (empfohlen)
|
||||
python setup_database.py
|
||||
|
||||
# ODER manuell:
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
**Bei Schema-Änderungen**:
|
||||
```bash
|
||||
# Neue Migration erstellen
|
||||
alembic revision --autogenerate -m "Beschreibung der Änderung"
|
||||
|
||||
# Migration anwenden
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Siehe `MIGRATIONS.md` für detaillierte Informationen.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Install required packages:
|
||||
|
||||
```bash
|
||||
pip install flask flask-sqlalchemy
|
||||
```
|
||||
|
||||
For production or development environment:
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
venv\Scripts\activate # Windows
|
||||
pip install flask flask-sqlalchemy
|
||||
```
|
||||
|
||||
### Testing Endpoints
|
||||
|
||||
Debug endpoint for checking lesson loading and configuration:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/debug
|
||||
```
|
||||
|
||||
### Creating Lessons from TXT Files
|
||||
|
||||
The project includes a CLI tool to convert TXT files into lessons.json format:
|
||||
|
||||
```bash
|
||||
# Convert TXT files (order matters!)
|
||||
python3 convert_txt_files_to_lessons_json.py 01.1_*.txt 01.2_*.txt 01.3_*.txt
|
||||
|
||||
# With custom output file
|
||||
python3 convert_txt_files_to_lessons_json.py -o data/lessons.json *.txt
|
||||
|
||||
# Show help
|
||||
python3 convert_txt_files_to_lessons_json.py --help
|
||||
```
|
||||
|
||||
**TXT File Format:**
|
||||
```
|
||||
Titel: 01.1 Erarbeitung der Grundstellung
|
||||
Untertitel: Optional subtitle (ignored)
|
||||
Anweisung: Task/instruction for the user
|
||||
|
||||
ffff jjjj ffff jjjj
|
||||
# Comments starting with # are ignored
|
||||
...
|
||||
```
|
||||
|
||||
**Filename Format:** `XX.Y_Description.txt` (e.g., `01.1_Grundstellung.txt`)
|
||||
- Number becomes `lesson` as string (e.g., `01.1` → `"1.1"`)
|
||||
- Parameter order determines JSON order
|
||||
- Supports both decimal (1.1) and integer (2) lesson numbers
|
||||
|
||||
**Output Format:**
|
||||
```json
|
||||
{
|
||||
"lesson": "1.1",
|
||||
"title": "01.1 Erarbeitung der Grundstellung",
|
||||
"text": "The practice text...",
|
||||
"task": "Instruction for the user"
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Request Flow
|
||||
|
||||
1. **Browser → Flask Routes** (`app.py`) → **Templates** (`templates/`) with server-side rendered data
|
||||
2. **JavaScript** (`static/js/script.js`) handles user input, cursor management, and real-time statistics
|
||||
3. **AJAX calls** to Flask endpoints for progress updates and lesson changes
|
||||
4. **SQLAlchemy ORM** (`models.py`) manages database operations
|
||||
|
||||
### Key Components
|
||||
|
||||
**Session State Management**: The application uses a hybrid approach:
|
||||
- **Flask Session**: Stores temporary state like metronome settings, elapsed time, keystroke count
|
||||
- **SQLite Database**: Persists progress (current lesson, cursor position, user input) and statistics
|
||||
|
||||
**Lesson Loading System**:
|
||||
- Lessons are loaded from `data/lessons.json` at startup
|
||||
- Includes automatic JSON repair functionality if the file is corrupted
|
||||
- Falls back to hardcoded `FALLBACK_LESSONS` if JSON loading fails
|
||||
- JSON repair endpoint available at `/repair_json` (POST)
|
||||
|
||||
**Direct Text Input**: The application uses a unique approach where users type directly in the template text field rather than a separate input field. The cursor position is tracked and visualized with a blinking underscore.
|
||||
|
||||
### Database Models (`models.py`)
|
||||
|
||||
**Progress**: Tracks the current typing session state
|
||||
- `current_text_index`: Which lesson is currently active
|
||||
- `current_position`: Cursor position within the text
|
||||
- `last_text`: User's typed input (allows resuming sessions)
|
||||
|
||||
**Statistic**: General typing statistics (currently not heavily used)
|
||||
|
||||
**LessonStatistic**: Historical performance data per lesson
|
||||
- `lesson_index`: References the lesson in `lessons.json`
|
||||
- `chars_per_minute`: Typing speed (ZPM in German)
|
||||
- `error_rate`: Error percentage
|
||||
- `wpm`: Words per minute (characters/5 per minute)
|
||||
- `created_at`: Timestamp for tracking progress over time
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
**Modular JavaScript (ES6 Modules)**: Das Frontend ist in separate Module aufgeteilt für bessere Wartbarkeit:
|
||||
|
||||
- **`theme.js`**: Theme Management (Light/Dark Mode)
|
||||
- **`metronome.js`**: Adaptive Metronome-Klasse und Audio-Player
|
||||
- **`statistics.js`**: Statistik-Berechnungen und Anzeige
|
||||
- **`textDisplay.js`**: Text-Rendering und Highlighting
|
||||
- **`keyboard.js`**: Keyboard-Event-Handling
|
||||
- **`session.js`**: Session Management und Fortschritt-Speicherung
|
||||
- **`ui.js`**: UI-Interaktionen (Buttons, Modals, Toggles)
|
||||
- **`main.js`**: Hauptorchestration und Initialisierung
|
||||
|
||||
**Theme System**: Light/Dark mode toggle using Bootstrap's `data-bs-theme` attribute, stored in localStorage.
|
||||
|
||||
**Adaptive Metronome**:
|
||||
- Adjusts typing speed guidance based on user accuracy
|
||||
- Target accuracy: 96% (configurable)
|
||||
- Tracks last 50 keystrokes to calculate rolling accuracy
|
||||
- Speed increases when accuracy is high, decreases when low
|
||||
- Maximum speed configurable (default: 100 BPM) to prevent overwhelming users
|
||||
- Runs independently with precise timing (Web Audio API, not setTimeout)
|
||||
- Implementiert in `metronome.js` mit zwei Klassen:
|
||||
- `AdaptiveMetronome`: Geschwindigkeitsanpassungslogik mit `maxSpeed` option
|
||||
- `MetronomePlayer`: Audio-Wiedergabe und präzises Timing
|
||||
|
||||
**Visual Feedback**:
|
||||
- Green highlighting for correct characters
|
||||
- Red highlighting with orange overlay for incorrect characters
|
||||
- Blinking cursor shows current typing position
|
||||
|
||||
**UI Design**:
|
||||
- Minimalistic statistics display without colorful badges
|
||||
- Right-aligned values with consistent spacing
|
||||
- Tooltips on all icons for better user guidance (v0.4.5)
|
||||
- Color-coded whitespace characters (v0.4.5)
|
||||
- Precision display (100 - error_rate) instead of error rate for positive feedback
|
||||
- Uniform button colors: Blue (primary actions), Green (continue), Red (end)
|
||||
|
||||
### Data Storage
|
||||
|
||||
**Lessons**: `data/lessons.json`
|
||||
- Each lesson has: `lesson` (number), `title`, `text`
|
||||
- Text can contain newlines for structured practice
|
||||
- Maximum 5000 characters per lesson (enforced by recent commit)
|
||||
|
||||
**Database**: `instance/typewriter.db`
|
||||
- Auto-created on first run
|
||||
- Includes migration logic for schema updates (see `chars_per_second` → `chars_per_minute` migration in `app.py:144-172`)
|
||||
|
||||
## Important Routes
|
||||
|
||||
### Main Routes
|
||||
- `/` - Main typing interface
|
||||
- `/statistics` - Statistics page with charts
|
||||
- `/settings` - Settings page for metronome configuration
|
||||
|
||||
### API Endpoints
|
||||
- `POST /update_progress` - Updates typing progress and returns real-time statistics
|
||||
- `POST /next_text` - Loads next lesson
|
||||
- `POST /set_lesson` - Switches to a specific lesson by index
|
||||
- `POST /save_lesson_statistics` - Saves completed lesson statistics
|
||||
- `GET /get_lesson_statistics/<lesson_index>` - Returns historical stats for a lesson
|
||||
- `POST /end_session` - Ends current typing session
|
||||
- `POST /save_settings` - Saves metronome and display settings to session
|
||||
- `POST /set_metronome` - Updates metronome configuration
|
||||
|
||||
### Debug/Utility Routes
|
||||
- `/debug` - Shows lesson loading status and configuration
|
||||
- `POST /repair_json` - Attempts to repair corrupted lessons.json
|
||||
- `POST /recreate_lessons` - Creates sample lessons file
|
||||
|
||||
## Statistics Calculation
|
||||
|
||||
The application calculates several metrics based on real-time typing:
|
||||
|
||||
- **Zeichen pro Minute (ZPM)**: `(total_chars / elapsed_time_seconds) * 60`
|
||||
- **Wörter pro Minute (WPM)**: `(correct_chars / 5) / (elapsed_time_seconds / 60)`
|
||||
- **Fehlerrate**: `(incorrect_chars / total_chars) * 100`
|
||||
- **Genauigkeit (Accuracy)**: `(correct_chars / total_chars) * 100`
|
||||
|
||||
Time measurement starts with the first keystroke and runs continuously. All keystrokes are counted including backspace and arrow keys.
|
||||
|
||||
## Configuration & Settings
|
||||
|
||||
**Metronome Settings** (stored persistently in UserSettings database table):
|
||||
- `metronome_enabled`: Boolean for on/off
|
||||
- `metronome_bpm`: Beats per minute
|
||||
- `metronome_sound`: Sound type ('beep')
|
||||
- `metronome_mode`: 'automatic' or manual
|
||||
- `metronome_speed`: Base speed setting
|
||||
- `target_error_rate`: Target error percentage (default: 5%)
|
||||
- `max_bpm_speed`: Maximum speed for adaptive metronome (default: 100 BPM) - prevents overwhelming users
|
||||
- `speed_display`: 'zpm' or 'wpm' for display preference
|
||||
|
||||
**Version Display**: Version number from VERSION file is displayed in the header using `APP_VERSION` template variable.
|
||||
|
||||
**Secret Key**: Update `app.secret_key` in `app.py:10` for production use.
|
||||
|
||||
## Database Schema Changes
|
||||
|
||||
The application includes automatic migration logic. When adding new columns:
|
||||
|
||||
1. Update the model in `models.py`
|
||||
2. Add migration logic in `app.py` within the `with app.app_context():` block
|
||||
3. Use SQLAlchemy's `inspect` to check existing schema
|
||||
4. Create temporary table, migrate data, drop old table, rename new table
|
||||
|
||||
See example migration at `app.py:144-172` for converting `chars_per_second` to `chars_per_minute`.
|
||||
|
||||
## Anti-Cheat Features
|
||||
|
||||
The application includes measures to prevent cheating:
|
||||
- Copy-paste is disabled
|
||||
- Direct keyboard input only
|
||||
- All keystrokes are counted including corrections
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Lessons not loading**: Check `data/lessons.json` exists and is valid JSON. Use `/debug` endpoint to diagnose.
|
||||
2. **Session state lost**: Flask sessions require a valid `secret_key` and cookies enabled.
|
||||
3. **Database schema issues**: The app includes automatic migration for some schema changes, but manual intervention may be needed for complex changes.
|
||||
4. **Character encoding**: Lessons use UTF-8 encoding for German characters (ä, ö, ü, ß). Ensure files are saved with UTF-8.
|
||||
|
||||
## File Structure Notes
|
||||
|
||||
- Loose files in root directory (e.g., `Metronome_app.py`, `adative_speed.py`, `*.js` files) appear to be experimental/prototype code not integrated into the main application
|
||||
- The active codebase is: `app.py`, `models.py`, `data/`, `static/`, `templates/`
|
||||
269
CLEANUP_GUIDE.md
269
CLEANUP_GUIDE.md
|
|
@ -1,269 +0,0 @@
|
|||
# 🧹 Aufräum-Guide für Typewriter Tutor
|
||||
|
||||
## Schnell-Übersicht
|
||||
|
||||
```bash
|
||||
./cleanup.sh # Interaktives Aufräumen (empfohlen)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Was kann aufgeräumt werden?
|
||||
|
||||
### 🔴 Immer sicher zu löschen
|
||||
|
||||
| Was | Wo | Warum | Wie |
|
||||
|-----|-----|-------|-----|
|
||||
| Python-Cache | `__pycache__/` | Wird automatisch neu erstellt | `find . -name "__pycache__" -type d -exec rm -rf {} +` |
|
||||
| .pyc Dateien | `*.pyc` | Kompilierte Python-Dateien | `find . -name "*.pyc" -delete` |
|
||||
| Temporäre Dateien | `*.tmp`, `*.temp` | Nicht benötigt | `find . -name "*.tmp" -delete` |
|
||||
| Backup-Dateien | `*.bak`, `*~` | Alte Backups | `find . -name "*.bak" -delete` |
|
||||
|
||||
### 🟡 Optional löschbar
|
||||
|
||||
| Was | Wo | Beachten | Wie |
|
||||
|-----|-----|----------|-----|
|
||||
| Aider-History | `.aider.*` | Nur wenn Sie Aider nicht mehr verwenden | `rm -rf .aider.*` |
|
||||
| Claude Code Cache | `.claude/` | Cache wird neu erstellt | `rm -rf .claude/` |
|
||||
| Log-Dateien | `*.log` | Falls nicht für Debugging benötigt | `find . -name "*.log" -delete` |
|
||||
|
||||
### 🟠 Vorsicht beim Löschen
|
||||
|
||||
| Was | Wo | Beachten | Wiederherstellung |
|
||||
|-----|-----|----------|-------------------|
|
||||
| Datenbank | `instance/typewriter.db` | **Alle Fortschritte gehen verloren!** | `alembic upgrade head` |
|
||||
| Virtual Environment | `venv/` | Muss neu installiert werden | `./setup.sh` oder `python -m venv venv` |
|
||||
| .env Datei | `.env` | **SECRET_KEY geht verloren!** | Neu generieren lassen |
|
||||
|
||||
### ⛔ NIE löschen
|
||||
|
||||
| Was | Warum |
|
||||
|-----|-------|
|
||||
| `data/lessons.json` | Enthält alle Tipp-Lektionen |
|
||||
| `alembic/versions/` | Datenbank-Migrations-Historie |
|
||||
| `requirements.txt` | Dependencies-Liste |
|
||||
| Alle `.py`, `.js`, `.html`, `.css` Dateien | Kern-Anwendung |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Aufräum-Szenarien
|
||||
|
||||
### Szenario 1: Leichtes Aufräumen
|
||||
**Ziel**: Cache und temporäre Dateien entfernen
|
||||
|
||||
```bash
|
||||
# Automatisch (empfohlen)
|
||||
./cleanup.sh
|
||||
# Bei allen Fragen "n" eingeben außer bei Cache
|
||||
|
||||
# Oder manuell
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type f -name "*.tmp" -delete
|
||||
git gc
|
||||
```
|
||||
|
||||
**Ergebnis**: ~5-50 MB gespart, keine Neuinstallation nötig
|
||||
|
||||
---
|
||||
|
||||
### Szenario 2: Tiefes Aufräumen
|
||||
**Ziel**: Alles außer Code und Daten zurücksetzen
|
||||
|
||||
```bash
|
||||
./cleanup.sh
|
||||
# Bei allen Fragen "j" eingeben
|
||||
|
||||
# Oder manuell
|
||||
rm -rf __pycache__/ venv/ instance/ .aider.* .claude/
|
||||
find . -name "*.pyc" -delete
|
||||
```
|
||||
|
||||
**Dann neu aufsetzen**:
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
**Ergebnis**: ~100-500 MB gespart, ~2 Minuten Neuinstallation
|
||||
|
||||
---
|
||||
|
||||
### Szenario 3: Kompletter Reset
|
||||
**Ziel**: Projekt auf Auslieferungszustand zurücksetzen
|
||||
|
||||
```bash
|
||||
# VORSICHT: Alle Daten gehen verloren!
|
||||
|
||||
# 1. Aufräumen
|
||||
./cleanup.sh # Alle Fragen mit "j" beantworten
|
||||
|
||||
# 2. Git-Status prüfen
|
||||
git status
|
||||
|
||||
# 3. Falls gewünscht: Uncommitted Changes löschen
|
||||
git checkout .
|
||||
git clean -fd
|
||||
|
||||
# 4. Neustart
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
**Ergebnis**: Wie frisch geklont, alle Benutzer-Daten verloren
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Empfohlene Routine
|
||||
|
||||
### Täglich
|
||||
```bash
|
||||
# Nicht nötig! Git-Ignore kümmert sich darum
|
||||
```
|
||||
|
||||
### Wöchentlich
|
||||
```bash
|
||||
# Cache aufräumen
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null
|
||||
```
|
||||
|
||||
### Monatlich
|
||||
```bash
|
||||
# Komplettes Aufräumen
|
||||
./cleanup.sh
|
||||
```
|
||||
|
||||
### Bei Problemen
|
||||
```bash
|
||||
# Reset und Neuinstallation
|
||||
rm -rf venv/ instance/
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Speicherplatz-Analyse
|
||||
|
||||
### Typische Größen
|
||||
|
||||
```bash
|
||||
# Gesamtprojekt (frisch): ~10 MB
|
||||
# Mit venv: ~60 MB
|
||||
# Mit Cache: ~70 MB
|
||||
# Mit Datenbank: ~71 MB
|
||||
# Mit Aider-History: ~75+ MB
|
||||
```
|
||||
|
||||
### Größte Verzeichnisse prüfen
|
||||
```bash
|
||||
du -h --max-depth=1 | sort -hr
|
||||
```
|
||||
|
||||
**Typisches Ergebnis**:
|
||||
```
|
||||
75M .
|
||||
50M ./venv
|
||||
10M ./.aider.tags.cache.v4
|
||||
3M ./.aider.chat.history.md
|
||||
5M ./static
|
||||
1M ./alembic
|
||||
500K ./templates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Nützliche Befehle
|
||||
|
||||
### Alle Cache-Dateien finden
|
||||
```bash
|
||||
find . -type d -name "__pycache__"
|
||||
find . -type f -name "*.pyc"
|
||||
```
|
||||
|
||||
### Große Dateien finden
|
||||
```bash
|
||||
find . -type f -size +1M -exec ls -lh {} \;
|
||||
```
|
||||
|
||||
### Alte Dateien finden (>30 Tage)
|
||||
```bash
|
||||
find . -type f -mtime +30
|
||||
```
|
||||
|
||||
### Git-Größe optimieren
|
||||
```bash
|
||||
git gc --aggressive --prune=now
|
||||
```
|
||||
|
||||
### Verzeichnis-Größen
|
||||
```bash
|
||||
du -sh */ | sort -hr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checkliste vor Aufräumen
|
||||
|
||||
- [ ] **Wichtige Daten gesichert?**
|
||||
- [ ] Datenbank-Backup erstellt (`cp instance/typewriter.db backup.db`)
|
||||
- [ ] Aktuelle Änderungen committed (`git status`)
|
||||
|
||||
- [ ] **Was wird gelöscht verstanden?**
|
||||
- [ ] Liste oben durchgelesen
|
||||
- [ ] Bei Unsicherheit: Nur Cache löschen
|
||||
|
||||
- [ ] **Neuinstallation vorbereitet?**
|
||||
- [ ] `./setup.sh` verfügbar
|
||||
- [ ] Internet-Verbindung vorhanden (für pip)
|
||||
|
||||
- [ ] **Zeit eingeplant?**
|
||||
- [ ] Leichtes Aufräumen: 10 Sekunden
|
||||
- [ ] Tiefes Aufräumen: 2-5 Minuten
|
||||
- [ ] Kompletter Reset: 5-10 Minuten
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Probleme und Lösungen
|
||||
|
||||
### "Permission denied" beim Löschen
|
||||
```bash
|
||||
sudo rm -rf <datei> # Nicht empfohlen!
|
||||
# Besser: Ownership prüfen
|
||||
ls -la <datei>
|
||||
```
|
||||
|
||||
### Virtual Environment lässt sich nicht löschen
|
||||
```bash
|
||||
# Erst deaktivieren
|
||||
deactivate
|
||||
# Dann löschen
|
||||
rm -rf venv/
|
||||
```
|
||||
|
||||
### Datenbank lässt sich nicht löschen
|
||||
```bash
|
||||
# App läuft möglicherweise noch
|
||||
pkill -f "python app.py"
|
||||
# Oder
|
||||
lsof instance/typewriter.db # Zeigt, wer die Datei verwendet
|
||||
```
|
||||
|
||||
### "Command not found: ./cleanup.sh"
|
||||
```bash
|
||||
# Ausführbar machen
|
||||
chmod +x cleanup.sh setup.sh start.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Weiterführende Links
|
||||
|
||||
- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) - Projekt-Übersicht
|
||||
- [MIGRATIONS.md](MIGRATIONS.md) - Datenbank-Migrationen
|
||||
- [CLAUDE.md](CLAUDE.md) - Entwickler-Guide
|
||||
|
||||
---
|
||||
|
||||
**Tipp**: Starten Sie mit dem leichten Aufräumen und arbeiten Sie sich bei Bedarf hoch!
|
||||
|
||||
```bash
|
||||
./cleanup.sh # Interaktiv und sicher
|
||||
```
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
# Scroll-Verbesserungen
|
||||
|
||||
## Implementierte Verbesserungen
|
||||
|
||||
### 1. Cursor immer in der ersten sichtbaren Zeile
|
||||
|
||||
Der Eingabe-Cursor steht nun immer in der ersten sichtbaren Zeile des Vorlagen-Fensters. Dies verbessert die Lesbarkeit und reduziert Augenbelastung.
|
||||
|
||||
**Implementierung**:
|
||||
- Neue Methode `scrollToCursorLine()` in `textDisplay.js`
|
||||
- Berechnet Scroll-Position so, dass Cursor-Zeile = erste sichtbare Zeile
|
||||
|
||||
### 2. Automatisches Zeilenweise Scrollen
|
||||
|
||||
Wenn der Benutzer alle Zeichen einer Zeile eingetippt hat, scrollt das Programm automatisch um eine Zeile nach unten, sodass der Cursor wieder in der ersten sichtbaren Zeile steht.
|
||||
|
||||
**Mechanismus**:
|
||||
```javascript
|
||||
scrollToCursorLine(currentCharIndex) {
|
||||
const currentLineIndex = this.getCurrentLineIndex(currentCharIndex);
|
||||
const lineHeight = this.getLineHeight();
|
||||
|
||||
// Cursor-Zeile soll erste sichtbare Zeile sein
|
||||
let desiredScrollTop = (currentLineIndex * lineHeight) - this.topPadding;
|
||||
|
||||
// Mit Grenzen-Prüfung
|
||||
desiredScrollTop = Math.max(0, Math.min(desiredScrollTop, maxScrollTop));
|
||||
this.targetElement.scrollTop = desiredScrollTop;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Genug Abstand für Overlay-Zeichen
|
||||
|
||||
Es gibt nun ausreichend Abstand (2.5em ≈ 48px) am oberen Rand, damit die falsch eingegebenen Zeichen (orange Overlay) vollständig lesbar angezeigt werden.
|
||||
|
||||
**CSS**:
|
||||
```css
|
||||
#target-text {
|
||||
padding-top: 2.5em; /* Vergrößerter Abstand am oberen Rand */
|
||||
}
|
||||
```
|
||||
|
||||
**JavaScript**:
|
||||
```javascript
|
||||
this.topPadding = 48; // Entspricht dem CSS padding-top: 2.5em
|
||||
```
|
||||
|
||||
### 4. Automatische Rückkehr nach manuellem Scrollen
|
||||
|
||||
Wenn der Benutzer mit dem Scrollbalken manuell scrollt, wird bei der nächsten Tastatureingabe sofort wieder zur Cursor-Position gescrollt.
|
||||
|
||||
**Implementierung**:
|
||||
```javascript
|
||||
updateDisplay(userInput, currentCharIndex, isKeystroke = false) {
|
||||
// Reset user scroll flag on keystroke
|
||||
if (isKeystroke && this.userScrolled) {
|
||||
this.userScrolled = false;
|
||||
}
|
||||
|
||||
// ... rendering ...
|
||||
|
||||
// Auto-scroll nur wenn nicht manuell gescrollt
|
||||
if (!this.userScrolled) {
|
||||
this.scrollToCursorLine(currentCharIndex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
### 1. `static/js/textDisplay.js`
|
||||
- Neue Eigenschaft: `topPadding` (48px)
|
||||
- Neue Methode: `getLineHeight()`
|
||||
- Neue Methode: `scrollToCursorLine(currentCharIndex)`
|
||||
- Parameter hinzugefügt: `isKeystroke` zu `updateDisplay()`
|
||||
- Automatisches Zurücksetzen von `userScrolled` bei Tastendruck
|
||||
|
||||
### 2. `static/js/main.js`
|
||||
- Parameter `isKeystroke = true` bei allen Tastendruck-Aufrufen:
|
||||
- `processKeystroke()`
|
||||
- `processBackspace()`
|
||||
- `processArrowLeft()`
|
||||
- `processArrowRight()`
|
||||
- `updateDisplay(isKeystroke)` Methode angepasst
|
||||
|
||||
## Benutzer-Erlebnis
|
||||
|
||||
### Vorher:
|
||||
- Cursor konnte irgendwo im Fenster sein
|
||||
- Benutzer musste oft nach oben/unten schauen
|
||||
- Manuelles Scrollen wurde beibehalten
|
||||
|
||||
### Nachher:
|
||||
- ✅ Cursor immer in der ersten Zeile sichtbar
|
||||
- ✅ Automatisches Scrollen Zeile für Zeile
|
||||
- ✅ Genug Platz für Fehler-Anzeige oben
|
||||
- ✅ Bei Tastendruck sofortige Rückkehr zur Cursor-Position
|
||||
- ✅ Optimale Lesbarkeit und Ergonomie
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Scroll-Berechnung:
|
||||
```
|
||||
desiredScrollTop = (currentLineIndex × lineHeight) - topPadding
|
||||
```
|
||||
|
||||
- `currentLineIndex`: Zeile, in der sich der Cursor befindet
|
||||
- `lineHeight`: Höhe einer Textzeile in Pixel
|
||||
- `topPadding`: 48px Abstand oben für Overlay-Zeichen
|
||||
|
||||
### Grenzen:
|
||||
```javascript
|
||||
desiredScrollTop = Math.max(0, Math.min(desiredScrollTop, maxScrollTop));
|
||||
```
|
||||
|
||||
- Minimum: 0 (nicht über den Anfang hinaus)
|
||||
- Maximum: `scrollHeight - containerHeight` (nicht über das Ende hinaus)
|
||||
|
||||
## Testing
|
||||
|
||||
Manuell testen:
|
||||
1. Starte die Anwendung
|
||||
2. Beginne zu tippen
|
||||
3. Beobachte, dass der Cursor immer in der ersten Zeile bleibt
|
||||
4. Scrolle manuell mit der Maus
|
||||
5. Tippe ein Zeichen → Automatische Rückkehr zur Cursor-Position
|
||||
6. Mache absichtlich Fehler → Orange Overlay sollte vollständig sichtbar sein
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
- ✅ Funktioniert in allen modernen Browsern
|
||||
- ✅ Responsive Design unverändert
|
||||
- ✅ Dark/Light Mode kompatibel
|
||||
- ✅ Keine Breaking Changes
|
||||
|
||||
## Performance
|
||||
|
||||
- Scroll-Berechnung: O(1)
|
||||
- Timeout verwendet für DOM-Rendering (0ms)
|
||||
- Sehr effizient, keine merkbare Verzögerung
|
||||
|
||||
---
|
||||
|
||||
**Datum**: 2025-10-27
|
||||
**Version**: 1.1.0
|
||||
**Autor**: Claude Code
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Bitte arbeite nur mit den Dateien app.py und README.md. Verzichte auf das Laden des gesamten Repos.
|
||||
Zeige alle Änderungen nur als diff im unified diff-Format, keine ganzen Dateien!
|
||||
Batch-Edits zusammenfassen, unnötige Kontextdaten vermeiden.
|
||||
Verwende vorhandene Informationen aus README.md und CLAUDE.md, lade keine Fremddateien.
|
||||
Nutze /clear nach jedem Task und /compact für die History.
|
||||
235
test_app.py
235
test_app.py
|
|
@ -1,235 +0,0 @@
|
|||
"""
|
||||
Unit Tests für die Typewriter Tutor Anwendung
|
||||
"""
|
||||
import unittest
|
||||
import json
|
||||
from app import app, db, calculate_statistics, repair_json
|
||||
from models import Progress, LessonStatistic
|
||||
|
||||
|
||||
class TypewriterTestCase(unittest.TestCase):
|
||||
"""Basis-Testklasse für Typewriter Tutor"""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup für jeden Test"""
|
||||
app.config['TESTING'] = True
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||
app.config['WTF_CSRF_ENABLED'] = False # CSRF für Tests deaktivieren
|
||||
self.client = app.test_client()
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
def tearDown(self):
|
||||
"""Cleanup nach jedem Test"""
|
||||
with app.app_context():
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
|
||||
|
||||
class StatisticsCalculationTests(TypewriterTestCase):
|
||||
"""Tests für Statistik-Berechnungen"""
|
||||
|
||||
def test_calculate_statistics_perfect_input(self):
|
||||
"""Test: Perfekte Eingabe ohne Fehler"""
|
||||
user_input = "hello world"
|
||||
current_text = "hello world"
|
||||
elapsed_time = 10.0 # 10 Sekunden
|
||||
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
self.assertEqual(stats['correct_chars'], 11)
|
||||
self.assertEqual(stats['incorrect_chars'], 0)
|
||||
self.assertEqual(stats['total_chars'], 11)
|
||||
self.assertEqual(stats['error_rate'], 0.0)
|
||||
self.assertEqual(stats['accuracy'], 100.0)
|
||||
self.assertGreater(stats['typing_speed'], 0)
|
||||
|
||||
def test_calculate_statistics_with_errors(self):
|
||||
"""Test: Eingabe mit Fehlern"""
|
||||
user_input = "hallo world" # 'a' statt 'e'
|
||||
current_text = "hello world"
|
||||
elapsed_time = 10.0
|
||||
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
self.assertEqual(stats['correct_chars'], 10) # alle außer 'a'
|
||||
self.assertEqual(stats['incorrect_chars'], 1)
|
||||
self.assertEqual(stats['total_chars'], 11)
|
||||
self.assertGreater(stats['error_rate'], 0)
|
||||
self.assertLess(stats['accuracy'], 100.0)
|
||||
|
||||
def test_calculate_statistics_zero_time(self):
|
||||
"""Test: Division durch Null vermeiden"""
|
||||
user_input = "test"
|
||||
current_text = "test"
|
||||
elapsed_time = 0.0
|
||||
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
self.assertEqual(stats['typing_speed'], 0)
|
||||
self.assertEqual(stats['words_per_minute'], 0)
|
||||
|
||||
def test_calculate_statistics_wpm_calculation(self):
|
||||
"""Test: WPM-Berechnung (5 Zeichen = 1 Wort)"""
|
||||
user_input = "12345" # genau 5 Zeichen = 1 Wort
|
||||
current_text = "12345"
|
||||
elapsed_time = 60.0 # 60 Sekunden = 1 Minute
|
||||
|
||||
stats = calculate_statistics(user_input, current_text, elapsed_time)
|
||||
|
||||
self.assertEqual(stats['words_per_minute'], 1.0)
|
||||
|
||||
|
||||
class JSONRepairTests(TypewriterTestCase):
|
||||
"""Tests für JSON-Reparatur"""
|
||||
|
||||
def test_repair_json_missing_comma(self):
|
||||
"""Test: Fehlende Kommas zwischen Objekten"""
|
||||
broken_json = '{"a": 1}\n{"b": 2}'
|
||||
repaired = repair_json(broken_json)
|
||||
|
||||
# Sollte Komma hinzufügen
|
||||
self.assertIn(',', repaired)
|
||||
|
||||
def test_repair_json_valid_json(self):
|
||||
"""Test: Gültiges JSON bleibt unverändert (abgesehen von Regex-Änderungen)"""
|
||||
valid_json = '[{"a": 1}, {"b": 2}]'
|
||||
repaired = repair_json(valid_json)
|
||||
|
||||
# Sollte parsebar sein
|
||||
try:
|
||||
json.loads(repaired)
|
||||
parsed = True
|
||||
except json.JSONDecodeError:
|
||||
parsed = False
|
||||
|
||||
self.assertTrue(parsed)
|
||||
|
||||
|
||||
class APIEndpointTests(TypewriterTestCase):
|
||||
"""Tests für API-Endpunkte"""
|
||||
|
||||
def test_index_page_loads(self):
|
||||
"""Test: Hauptseite lädt erfolgreich"""
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'Typewriter Trainer', response.data)
|
||||
|
||||
def test_statistics_page_loads(self):
|
||||
"""Test: Statistik-Seite lädt"""
|
||||
response = self.client.get('/statistics')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_settings_page_loads(self):
|
||||
"""Test: Einstellungsseite lädt"""
|
||||
response = self.client.get('/settings')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_update_progress_requires_data(self):
|
||||
"""Test: update_progress benötigt Daten"""
|
||||
response = self.client.post('/update_progress',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_save_lesson_statistics_validation(self):
|
||||
"""Test: Input-Validierung bei save_lesson_statistics"""
|
||||
# Negative Werte sollten abgelehnt werden
|
||||
response = self.client.post('/save_lesson_statistics',
|
||||
data=json.dumps({
|
||||
'lesson_index': 0,
|
||||
'chars_per_minute': -10,
|
||||
'error_rate': 5,
|
||||
'wpm': 2
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_save_lesson_statistics_error_rate_validation(self):
|
||||
"""Test: Fehlerrate über 100% wird abgelehnt"""
|
||||
response = self.client.post('/save_lesson_statistics',
|
||||
data=json.dumps({
|
||||
'lesson_index': 0,
|
||||
'chars_per_minute': 100,
|
||||
'error_rate': 150, # über 100%
|
||||
'wpm': 20
|
||||
}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_debug_endpoint_disabled_in_production(self):
|
||||
"""Test: Debug-Endpoint ist in Produktion deaktiviert"""
|
||||
app.config['DEBUG'] = False
|
||||
response = self.client.get('/debug')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
app.config['DEBUG'] = True # Zurücksetzen
|
||||
|
||||
|
||||
class DatabaseTests(TypewriterTestCase):
|
||||
"""Tests für Datenbank-Operationen"""
|
||||
|
||||
def test_create_lesson_statistic(self):
|
||||
"""Test: LessonStatistic kann erstellt werden"""
|
||||
with app.app_context():
|
||||
stat = LessonStatistic(
|
||||
lesson_index=0,
|
||||
chars_per_minute=100.0,
|
||||
error_rate=5.0,
|
||||
wpm=20.0
|
||||
)
|
||||
db.session.add(stat)
|
||||
db.session.commit()
|
||||
|
||||
# Abrufen und prüfen
|
||||
retrieved = LessonStatistic.query.first()
|
||||
self.assertIsNotNone(retrieved)
|
||||
self.assertEqual(retrieved.lesson_index, 0)
|
||||
self.assertEqual(retrieved.chars_per_minute, 100.0)
|
||||
|
||||
def test_lesson_statistic_index_exists(self):
|
||||
"""Test: Index auf lesson_index existiert"""
|
||||
with app.app_context():
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(db.engine)
|
||||
indexes = inspector.get_indexes('lesson_statistic')
|
||||
|
||||
# Prüfe ob ein Index auf lesson_index existiert
|
||||
has_index = any('lesson_index' in idx['column_names'] for idx in indexes)
|
||||
self.assertTrue(has_index, "Index auf lesson_index sollte existieren")
|
||||
|
||||
|
||||
class ExportTests(TypewriterTestCase):
|
||||
"""Tests für Export-Funktionalität"""
|
||||
|
||||
def test_csv_export_empty_database(self):
|
||||
"""Test: CSV-Export funktioniert auch mit leerer Datenbank"""
|
||||
response = self.client.get('/export/statistics/csv')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers['Content-Type'], 'text/csv; charset=utf-8')
|
||||
|
||||
def test_json_export_empty_database(self):
|
||||
"""Test: JSON-Export funktioniert auch mit leerer Datenbank"""
|
||||
response = self.client.get('/export/statistics/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers['Content-Type'], 'application/json; charset=utf-8')
|
||||
|
||||
def test_csv_export_with_data(self):
|
||||
"""Test: CSV-Export enthält Daten"""
|
||||
with app.app_context():
|
||||
stat = LessonStatistic(
|
||||
lesson_index=0,
|
||||
chars_per_minute=100.0,
|
||||
error_rate=5.0,
|
||||
wpm=20.0
|
||||
)
|
||||
db.session.add(stat)
|
||||
db.session.commit()
|
||||
|
||||
response = self.client.get('/export/statistics/csv')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'100.0', response.data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -1,536 +0,0 @@
|
|||
"""
|
||||
Umfangreiche Tests für die adaptive Metronom-Geschwindigkeitsanpassung.
|
||||
|
||||
Dieses Modul testet die JavaScript-Logik der AdaptiveMetronome-Klasse
|
||||
durch eine Python-Reimplementierung und umfangreiche Testszenarien.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from typing import List
|
||||
|
||||
|
||||
class AdaptiveMetronome:
|
||||
"""
|
||||
Python-Reimplementierung der JavaScript AdaptiveMetronome-Klasse.
|
||||
Muss identisch zur JS-Version in static/js/script.js:81-137 sein.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.speed = 60.0 # Startgeschwindigkeit in BPM
|
||||
self.accuracy_history: List[float] = [] # Letzte 50 Ergebnisse (1.0=korrekt, 0.0=falsch)
|
||||
self.consecutive_correct = 0 # Aufeinanderfolgende korrekte Zeichen
|
||||
self.target_accuracy = 0.96 # Zielgenauigkeit (96%)
|
||||
|
||||
def process_keystroke(self, is_correct: bool, current_difficulty: float = 1.0) -> float:
|
||||
"""
|
||||
Verarbeitet einen Tastendruck und passt die Geschwindigkeit an.
|
||||
|
||||
Args:
|
||||
is_correct: Ob das eingegebene Zeichen korrekt war
|
||||
current_difficulty: Schwierigkeitsfaktor (1.0 = normal)
|
||||
|
||||
Returns:
|
||||
Neue BPM-Geschwindigkeit
|
||||
"""
|
||||
# 1. History aktualisieren
|
||||
self.accuracy_history.append(1.0 if is_correct else 0.0)
|
||||
if len(self.accuracy_history) > 50:
|
||||
self.accuracy_history.pop(0)
|
||||
|
||||
# 2. Konsekutive korrekte Zeichen zählen
|
||||
if is_correct:
|
||||
self.consecutive_correct += 1
|
||||
else:
|
||||
self.consecutive_correct = 0
|
||||
|
||||
# 3. Geschwindigkeit anpassen
|
||||
return self._adjust_speed(current_difficulty)
|
||||
|
||||
def _adjust_speed(self, difficulty: float) -> float:
|
||||
"""
|
||||
Passt die Geschwindigkeit basierend auf der aktuellen Genauigkeit an.
|
||||
|
||||
Args:
|
||||
difficulty: Schwierigkeitsfaktor
|
||||
|
||||
Returns:
|
||||
Neue BPM-Geschwindigkeit
|
||||
"""
|
||||
if len(self.accuracy_history) == 0:
|
||||
return self.speed
|
||||
|
||||
# Berechne aktuelle Genauigkeit
|
||||
current_accuracy = sum(self.accuracy_history) / len(self.accuracy_history)
|
||||
adjusted_target = self.target_accuracy - (difficulty * 0.05)
|
||||
|
||||
# Mehrere Anpassungsstrategien kombinieren:
|
||||
if self.consecutive_correct >= 15:
|
||||
# Belohnung für längere Fehlerfreiheit
|
||||
self.speed *= 1.03
|
||||
elif current_accuracy > adjusted_target + 0.04:
|
||||
# Zu einfach - stärker beschleunigen
|
||||
self.speed *= 1.02
|
||||
elif current_accuracy < adjusted_target - 0.06:
|
||||
# Zu schwierig - stärker bremsen
|
||||
self.speed *= 0.92
|
||||
elif current_accuracy < adjusted_target:
|
||||
# Leicht unter Ziel - sanft bremsen
|
||||
self.speed *= 0.97
|
||||
else:
|
||||
# Im Zielbereich - sanft beschleunigen
|
||||
self.speed *= 1.01
|
||||
|
||||
# Geschwindigkeitsgrenzen
|
||||
self.speed = max(40.0, min(200.0, self.speed))
|
||||
return self.speed
|
||||
|
||||
|
||||
class TestAdaptiveMetronomeBasics(unittest.TestCase):
|
||||
"""Basistests für die AdaptiveMetronome-Klasse"""
|
||||
|
||||
def setUp(self):
|
||||
"""Wird vor jedem Test ausgeführt"""
|
||||
self.metronome = AdaptiveMetronome()
|
||||
|
||||
def test_initial_values(self):
|
||||
"""Test: Initialwerte sind korrekt gesetzt"""
|
||||
self.assertEqual(self.metronome.speed, 60.0)
|
||||
self.assertEqual(len(self.metronome.accuracy_history), 0)
|
||||
self.assertEqual(self.metronome.consecutive_correct, 0)
|
||||
self.assertEqual(self.metronome.target_accuracy, 0.96)
|
||||
|
||||
def test_single_correct_keystroke(self):
|
||||
"""Test: Ein einzelner korrekter Tastendruck"""
|
||||
speed = self.metronome.process_keystroke(True)
|
||||
|
||||
self.assertEqual(len(self.metronome.accuracy_history), 1)
|
||||
self.assertEqual(self.metronome.accuracy_history[0], 1.0)
|
||||
self.assertEqual(self.metronome.consecutive_correct, 1)
|
||||
# Bei 100% Genauigkeit sollte beschleunigt werden (Regel #2: +2%)
|
||||
self.assertAlmostEqual(speed, 61.2, places=1)
|
||||
|
||||
def test_single_incorrect_keystroke(self):
|
||||
"""Test: Ein einzelner falscher Tastendruck"""
|
||||
speed = self.metronome.process_keystroke(False)
|
||||
|
||||
self.assertEqual(len(self.metronome.accuracy_history), 1)
|
||||
self.assertEqual(self.metronome.accuracy_history[0], 0.0)
|
||||
self.assertEqual(self.metronome.consecutive_correct, 0)
|
||||
# Bei 0% Genauigkeit sollte stark gebremst werden (Regel #3: -8%)
|
||||
self.assertAlmostEqual(speed, 55.2, places=1)
|
||||
|
||||
def test_history_limit_50(self):
|
||||
"""Test: History wird auf 50 Einträge begrenzt"""
|
||||
# Gebe 60 korrekte Zeichen ein
|
||||
for _ in range(60):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
self.assertEqual(len(self.metronome.accuracy_history), 50)
|
||||
|
||||
def test_consecutive_correct_reset_on_error(self):
|
||||
"""Test: Consecutive-Counter wird bei Fehler zurückgesetzt"""
|
||||
# 5 korrekte Zeichen
|
||||
for _ in range(5):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
self.assertEqual(self.metronome.consecutive_correct, 5)
|
||||
|
||||
# Ein Fehler
|
||||
self.metronome.process_keystroke(False)
|
||||
self.assertEqual(self.metronome.consecutive_correct, 0)
|
||||
|
||||
|
||||
class TestAdaptiveMetronomeSpeedAdjustment(unittest.TestCase):
|
||||
"""Tests für die Geschwindigkeitsanpassung"""
|
||||
|
||||
def setUp(self):
|
||||
"""Wird vor jedem Test ausgeführt"""
|
||||
self.metronome = AdaptiveMetronome()
|
||||
|
||||
def test_rule_1_streak_bonus(self):
|
||||
"""Test Regel #1: Streak-Bonus bei 15+ korrekten Zeichen"""
|
||||
# 15 korrekte Zeichen eingeben
|
||||
for i in range(15):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
# 16. korrektes Zeichen sollte Streak-Bonus auslösen (+3%)
|
||||
speed = self.metronome.process_keystroke(True)
|
||||
|
||||
# Überprüfe dass Regel #1 angewendet wurde
|
||||
expected_speed = initial_speed * 1.03
|
||||
self.assertAlmostEqual(speed, expected_speed, places=1)
|
||||
self.assertGreater(speed, initial_speed)
|
||||
|
||||
def test_rule_2_too_easy(self):
|
||||
"""Test Regel #2: Zu einfach - starke Beschleunigung"""
|
||||
# Erstelle 10 korrekte Eingaben (100% Genauigkeit)
|
||||
for _ in range(10):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Bei 100% Genauigkeit (> 95%) sollte Regel #2 greifen (+2%)
|
||||
# Aber nicht Regel #1, da consecutive_correct nur 10 ist
|
||||
self.assertEqual(self.metronome.consecutive_correct, 10)
|
||||
|
||||
# Letzter gespeicherter Speed vor nächstem Keystroke
|
||||
speeds = [self.metronome.speed]
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Überprüfe Beschleunigung um ~2%
|
||||
speed_increase = self.metronome.speed / speeds[0]
|
||||
self.assertAlmostEqual(speed_increase, 1.02, places=2)
|
||||
|
||||
def test_rule_3_too_difficult(self):
|
||||
"""Test Regel #3: Zu schwer - starkes Bremsen"""
|
||||
# Erstelle Scenario: 2 korrekt, 8 falsch = 20% Genauigkeit
|
||||
for _ in range(2):
|
||||
self.metronome.process_keystroke(True)
|
||||
for _ in range(8):
|
||||
self.metronome.process_keystroke(False)
|
||||
|
||||
# 20% < (91% - 6%) → Regel #3 sollte greifen (-8%)
|
||||
current_accuracy = sum(self.metronome.accuracy_history) / len(self.metronome.accuracy_history)
|
||||
self.assertLess(current_accuracy, 0.85)
|
||||
|
||||
# Aber: Speed ist bereits auf Minimum (40 BPM) - kann nicht weiter sinken
|
||||
# Daher testen wir mit höherem Ausgangspoint
|
||||
self.metronome.speed = 100.0
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
# Ein weiterer Fehler sollte Regel #3 auslösen
|
||||
self.metronome.process_keystroke(False)
|
||||
|
||||
# Überprüfe starkes Bremsen um ~8%
|
||||
speed_decrease = self.metronome.speed / initial_speed
|
||||
self.assertAlmostEqual(speed_decrease, 0.92, places=2)
|
||||
|
||||
def test_rule_4_slightly_below_target(self):
|
||||
"""Test Regel #4: Leicht unter Ziel - sanftes Bremsen"""
|
||||
# Erstelle Scenario: 9 korrekt, 1 falsch = 90% Genauigkeit
|
||||
# 90% liegt zwischen 85% und 91% → Regel #4 (-3%)
|
||||
for _ in range(9):
|
||||
self.metronome.process_keystroke(True)
|
||||
self.metronome.process_keystroke(False)
|
||||
|
||||
initial_speed = self.metronome.speed
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Überprüfe sanftes Bremsen um ~3%
|
||||
speed_decrease = self.metronome.speed / initial_speed
|
||||
self.assertAlmostEqual(speed_decrease, 0.97, places=2)
|
||||
|
||||
def test_rule_5_in_target_range(self):
|
||||
"""Test Regel #5: Im Zielbereich - sanfte Beschleunigung"""
|
||||
# Problem: Nach vielen korrekten Eingaben erreicht Speed das Maximum (200 BPM)
|
||||
# Daher: Starte mit niedrigerer Speed und baue Genauigkeit auf
|
||||
|
||||
# Setze Speed manuell niedriger
|
||||
self.metronome.speed = 80.0
|
||||
|
||||
# Erstelle Scenario: 92% Genauigkeit (zwischen 91% und 95%)
|
||||
# 46 korrekt, 4 falsch = 92%
|
||||
for i in range(50):
|
||||
is_correct = i < 46
|
||||
self.metronome.process_keystroke(is_correct)
|
||||
# Verhindere dass Speed zu hoch wird
|
||||
if self.metronome.speed > 100:
|
||||
self.metronome.speed = 85.0
|
||||
|
||||
# Setze finale Testbedingungen
|
||||
self.metronome.speed = 85.0
|
||||
self.metronome.consecutive_correct = 0 # Kein Streak-Bonus
|
||||
|
||||
# Accuracy sollte im Zielbereich sein
|
||||
current_accuracy = sum(self.metronome.accuracy_history) / len(self.metronome.accuracy_history)
|
||||
self.assertGreater(current_accuracy, 0.91)
|
||||
self.assertLess(current_accuracy, 0.95)
|
||||
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
# Füge ein korrektes Zeichen hinzu (sollte Regel #5 auslösen)
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Überprüfe sanfte Beschleunigung um ~1%
|
||||
speed_increase = self.metronome.speed / initial_speed
|
||||
self.assertAlmostEqual(speed_increase, 1.01, places=2)
|
||||
|
||||
def test_speed_limits_minimum(self):
|
||||
"""Test: Geschwindigkeit wird nicht unter 40 BPM fallen"""
|
||||
# Setze Speed manuell nahe an Minimum
|
||||
self.metronome.speed = 41.0
|
||||
|
||||
# Erzeuge sehr schlechte Performance
|
||||
for _ in range(50):
|
||||
self.metronome.process_keystroke(False)
|
||||
|
||||
# Speed sollte nicht unter 40 fallen
|
||||
self.assertGreaterEqual(self.metronome.speed, 40.0)
|
||||
|
||||
def test_speed_limits_maximum(self):
|
||||
"""Test: Geschwindigkeit wird nicht über 200 BPM steigen"""
|
||||
# Setze Speed manuell nahe an Maximum
|
||||
self.metronome.speed = 195.0
|
||||
|
||||
# Erzeuge perfekte Performance mit Streak
|
||||
for _ in range(20):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Speed sollte nicht über 200 steigen
|
||||
self.assertLessEqual(self.metronome.speed, 200.0)
|
||||
|
||||
|
||||
class TestAdaptiveMetronomeScenarios(unittest.TestCase):
|
||||
"""Realistische Szenarien und Edge Cases"""
|
||||
|
||||
def setUp(self):
|
||||
"""Wird vor jedem Test ausgeführt"""
|
||||
self.metronome = AdaptiveMetronome()
|
||||
|
||||
def test_scenario_beginner(self):
|
||||
"""Scenario: Anfänger mit 70% Genauigkeit"""
|
||||
# Simuliere 100 Tasteneingaben mit 70% Genauigkeit
|
||||
correct_count = 0
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
pattern = [True] * 7 + [False] * 3 # 70% korrekt
|
||||
for i in range(10): # 100 Eingaben
|
||||
for keystroke in pattern:
|
||||
self.metronome.process_keystroke(keystroke)
|
||||
if keystroke:
|
||||
correct_count += 1
|
||||
|
||||
# Bei 70% Genauigkeit sollte das Metronom deutlich langsamer werden
|
||||
self.assertLess(self.metronome.speed, initial_speed)
|
||||
self.assertGreaterEqual(self.metronome.speed, 40.0)
|
||||
|
||||
print(f"Beginner: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
|
||||
|
||||
def test_scenario_intermediate(self):
|
||||
"""Scenario: Fortgeschrittener mit 92% Genauigkeit"""
|
||||
# Simuliere 100 Tasteneingaben mit 92% Genauigkeit
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
pattern = [True] * 23 + [False] * 2 # 92% korrekt
|
||||
for i in range(4): # 100 Eingaben
|
||||
for keystroke in pattern:
|
||||
self.metronome.process_keystroke(keystroke)
|
||||
|
||||
# Bei 92% Genauigkeit (im Zielbereich) sollte sanft beschleunigt werden
|
||||
self.assertGreater(self.metronome.speed, initial_speed)
|
||||
|
||||
print(f"Intermediate: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
|
||||
|
||||
def test_scenario_expert(self):
|
||||
"""Scenario: Experte mit 98% Genauigkeit"""
|
||||
# Simuliere 100 Tasteneingaben mit 98% Genauigkeit
|
||||
initial_speed = self.metronome.speed
|
||||
|
||||
pattern = [True] * 49 + [False] * 1 # 98% korrekt
|
||||
for i in range(2): # 100 Eingaben
|
||||
for keystroke in pattern:
|
||||
self.metronome.process_keystroke(keystroke)
|
||||
|
||||
# Bei 98% Genauigkeit sollte deutlich beschleunigt werden
|
||||
self.assertGreater(self.metronome.speed, initial_speed * 1.5)
|
||||
|
||||
print(f"Expert: {initial_speed:.1f} BPM → {self.metronome.speed:.1f} BPM")
|
||||
|
||||
def test_scenario_recovery_after_errors(self):
|
||||
"""Scenario: Erholung nach Fehlerphase"""
|
||||
# Phase 1: Viele Fehler
|
||||
for _ in range(20):
|
||||
self.metronome.process_keystroke(False)
|
||||
|
||||
speed_after_errors = self.metronome.speed
|
||||
self.assertLess(speed_after_errors, 60.0)
|
||||
|
||||
# Phase 2: Perfekte Eingaben
|
||||
for _ in range(50):
|
||||
self.metronome.process_keystroke(True)
|
||||
|
||||
# Speed sollte sich wieder erholen
|
||||
self.assertGreater(self.metronome.speed, speed_after_errors)
|
||||
|
||||
print(f"Recovery: {speed_after_errors:.1f} BPM → {self.metronome.speed:.1f} BPM")
|
||||
|
||||
def test_scenario_alternating_performance(self):
|
||||
"""Scenario: Wechselnde Performance"""
|
||||
speeds = [60.0] # Startgeschwindigkeit
|
||||
|
||||
# 5 Zyklen: 10 korrekt, dann 5 falsch
|
||||
for cycle in range(5):
|
||||
for _ in range(10):
|
||||
self.metronome.process_keystroke(True)
|
||||
speeds.append(self.metronome.speed)
|
||||
|
||||
for _ in range(5):
|
||||
self.metronome.process_keystroke(False)
|
||||
speeds.append(self.metronome.speed)
|
||||
|
||||
print(f"Alternating: Start={speeds[0]:.1f}, End={speeds[-1]:.1f}")
|
||||
print(f" Speed changes: {[f'{s:.1f}' for s in speeds]}")
|
||||
|
||||
# Nach mehreren Zyklen sollte sich ein Gleichgewicht einstellen
|
||||
self.assertGreaterEqual(self.metronome.speed, 40.0)
|
||||
self.assertLessEqual(self.metronome.speed, 200.0)
|
||||
|
||||
def test_difficulty_parameter_effect(self):
|
||||
"""Test: Difficulty-Parameter beeinflusst Zielgenauigkeit"""
|
||||
# Teste mit normalem difficulty (1.0)
|
||||
metronome1 = AdaptiveMetronome()
|
||||
for _ in range(50):
|
||||
metronome1.process_keystroke(True, current_difficulty=1.0)
|
||||
speed_normal = metronome1.speed
|
||||
|
||||
# Teste mit höherem difficulty (2.0)
|
||||
metronome2 = AdaptiveMetronome()
|
||||
for _ in range(50):
|
||||
metronome2.process_keystroke(True, current_difficulty=2.0)
|
||||
speed_difficult = metronome2.speed
|
||||
|
||||
# Bei höherer Schwierigkeit sollte adjusted_target niedriger sein
|
||||
# → Gleiche Performance wird als "besser" bewertet → schneller
|
||||
print(f"Difficulty effect: normal={speed_normal:.1f}, difficult={speed_difficult:.1f}")
|
||||
|
||||
# Beide sollten schneller sein als Start, aber möglicherweise unterschiedlich
|
||||
self.assertGreater(speed_normal, 60.0)
|
||||
self.assertGreater(speed_difficult, 60.0)
|
||||
|
||||
|
||||
class TestAdaptiveMetronomeEdgeCases(unittest.TestCase):
|
||||
"""Edge Cases und Grenzfälle"""
|
||||
|
||||
def test_empty_history(self):
|
||||
"""Test: Verhalten bei leerer History"""
|
||||
metronome = AdaptiveMetronome()
|
||||
# Ohne Eingaben sollte Speed unverändert bleiben
|
||||
speed = metronome._adjust_speed(1.0)
|
||||
self.assertEqual(speed, 60.0)
|
||||
|
||||
def test_exactly_at_boundaries(self):
|
||||
"""Test: Verhalten exakt an den Regel-Grenzen"""
|
||||
metronome = AdaptiveMetronome()
|
||||
|
||||
# Erstelle exakt 95% Genauigkeit (Grenze zwischen Regel #2 und #5)
|
||||
for _ in range(19):
|
||||
metronome.process_keystroke(True)
|
||||
metronome.process_keystroke(False)
|
||||
|
||||
accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history)
|
||||
self.assertEqual(accuracy, 0.95)
|
||||
|
||||
# Bei 95% Genauigkeit mit adjusted_target=0.91:
|
||||
# 95% ist NICHT > (0.91 + 0.04) = 0.95, daher nicht Regel #2
|
||||
# 95% ist >= 0.91, daher Regel #5 (+1%)
|
||||
# ABER: consecutive_correct ist 19, daher KEINE Regel #1 (braucht >= 15)
|
||||
# Warte... consecutive_correct wird bei Fehler auf 0 gesetzt!
|
||||
# Also nach dem Fehler ist consecutive_correct = 0
|
||||
|
||||
initial_speed = metronome.speed
|
||||
# Nächstes korrektes Zeichen: Genauigkeit 95.2%, consecutive=1
|
||||
# 95.2% > 95% → Regel #2 sollte greifen (+2%)
|
||||
metronome.process_keystroke(True)
|
||||
self.assertAlmostEqual(metronome.speed / initial_speed, 1.02, places=2)
|
||||
|
||||
def test_streak_exactly_15(self):
|
||||
"""Test: Streak-Bonus exakt bei 15"""
|
||||
metronome = AdaptiveMetronome()
|
||||
|
||||
# 14 korrekte
|
||||
for _ in range(14):
|
||||
metronome.process_keystroke(True)
|
||||
|
||||
self.assertEqual(metronome.consecutive_correct, 14)
|
||||
initial_speed = metronome.speed
|
||||
|
||||
# 15. korrekt - sollte noch nicht Streak-Bonus auslösen
|
||||
metronome.process_keystroke(True)
|
||||
# Streak ist jetzt 15, aber wird erst beim NÄCHSTEN Keystroke belohnt
|
||||
|
||||
speed_at_15 = metronome.speed
|
||||
|
||||
# 16. korrekt - JETZT sollte Streak-Bonus aktiv sein
|
||||
metronome.process_keystroke(True)
|
||||
|
||||
# Jetzt sollte Regel #1 aktiv sein
|
||||
self.assertGreater(metronome.speed / speed_at_15, 1.02)
|
||||
|
||||
def test_rapid_speed_changes(self):
|
||||
"""Test: Schnelle Wechsel zwischen Erfolg und Misserfolg"""
|
||||
metronome = AdaptiveMetronome()
|
||||
speeds = []
|
||||
|
||||
# Alterniere zwischen korrekt und falsch
|
||||
for i in range(20):
|
||||
is_correct = (i % 2 == 0)
|
||||
metronome.process_keystroke(is_correct)
|
||||
speeds.append(metronome.speed)
|
||||
|
||||
# Speed sollte trotz Chaos in gültigen Grenzen bleiben
|
||||
self.assertGreaterEqual(min(speeds), 40.0)
|
||||
self.assertLessEqual(max(speeds), 200.0)
|
||||
|
||||
# Genauigkeit sollte bei ~50% liegen
|
||||
accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history)
|
||||
self.assertAlmostEqual(accuracy, 0.5, places=1)
|
||||
|
||||
|
||||
def run_detailed_simulation():
|
||||
"""
|
||||
Führt eine detaillierte Simulation durch und gibt die Ergebnisse aus.
|
||||
Dies ist keine Unit-Test, sondern eine visuelle Demonstration.
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DETAILLIERTE SIMULATION: Adaptive Metronom-Geschwindigkeit")
|
||||
print("=" * 80)
|
||||
|
||||
metronome = AdaptiveMetronome()
|
||||
|
||||
# Simuliere verschiedene Phasen
|
||||
phases = [
|
||||
("Anfangsphase (70% korrekt)", [True]*7 + [False]*3, 5),
|
||||
("Verbesserung (85% korrekt)", [True]*17 + [False]*3, 5),
|
||||
("Zielbereich (93% korrekt)", [True]*28 + [False]*2, 5),
|
||||
("Exzellent (98% korrekt)", [True]*49 + [False]*1, 2),
|
||||
("Fehlerphase (50% korrekt)", [True, False], 10),
|
||||
("Erholung (95% korrekt)", [True]*19 + [False]*1, 5),
|
||||
]
|
||||
|
||||
print(f"\nStartgeschwindigkeit: {metronome.speed:.1f} BPM\n")
|
||||
|
||||
total_keystrokes = 0
|
||||
for phase_name, pattern, repetitions in phases:
|
||||
phase_start_speed = metronome.speed
|
||||
phase_keystrokes = 0
|
||||
|
||||
for _ in range(repetitions):
|
||||
for is_correct in pattern:
|
||||
metronome.process_keystroke(is_correct)
|
||||
phase_keystrokes += 1
|
||||
total_keystrokes += 1
|
||||
|
||||
accuracy = sum(metronome.accuracy_history[-phase_keystrokes:]) / phase_keystrokes * 100
|
||||
|
||||
print(f"{phase_name}:")
|
||||
print(f" Eingaben: {phase_keystrokes} Tasten, Genauigkeit: {accuracy:.1f}%")
|
||||
print(f" Speed: {phase_start_speed:.1f} → {metronome.speed:.1f} BPM "
|
||||
f"({metronome.speed - phase_start_speed:+.1f})")
|
||||
print(f" Streak: {metronome.consecutive_correct} korrekte Zeichen")
|
||||
print()
|
||||
|
||||
print(f"Gesamtstatistik:")
|
||||
print(f" Gesamte Eingaben: {total_keystrokes}")
|
||||
print(f" Endgeschwindigkeit: {metronome.speed:.1f} BPM")
|
||||
overall_accuracy = sum(metronome.accuracy_history) / len(metronome.accuracy_history) * 100
|
||||
print(f" Aktuelle Genauigkeit (letzte 50): {overall_accuracy:.1f}%")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Führe zuerst die Simulation aus
|
||||
run_detailed_simulation()
|
||||
|
||||
# Dann die Unit-Tests
|
||||
print("\nFühre Unit-Tests aus...\n")
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue