mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	working
This commit is contained in:
		
							
								
								
									
										182
									
								
								tests/components/web_server/test_multipart_ota.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										182
									
								
								tests/components/web_server/test_multipart_ota.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Test script for ESP-IDF web server multipart OTA upload functionality. | ||||
| This script can be run manually to test OTA uploads to a running device. | ||||
| """ | ||||
|  | ||||
| import argparse | ||||
| from pathlib import Path | ||||
| import sys | ||||
| import time | ||||
|  | ||||
| import requests | ||||
|  | ||||
|  | ||||
| def test_multipart_ota_upload(host, port, firmware_path): | ||||
|     """Test OTA firmware upload using multipart/form-data""" | ||||
|     base_url = f"http://{host}:{port}" | ||||
|  | ||||
|     print(f"Testing OTA upload to {base_url}") | ||||
|  | ||||
|     # First check if server is reachable | ||||
|     try: | ||||
|         resp = requests.get(f"{base_url}/", timeout=5) | ||||
|         if resp.status_code != 200: | ||||
|             print(f"Error: Server returned status {resp.status_code}") | ||||
|             return False | ||||
|         print("✓ Server is reachable") | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         print(f"Error: Cannot reach server - {e}") | ||||
|         return False | ||||
|  | ||||
|     # Check if firmware file exists | ||||
|     if not Path(firmware_path).exists(): | ||||
|         print(f"Error: Firmware file not found: {firmware_path}") | ||||
|         return False | ||||
|  | ||||
|     # Prepare multipart upload | ||||
|     print(f"Uploading firmware: {firmware_path}") | ||||
|     print(f"File size: {Path(firmware_path).stat().st_size} bytes") | ||||
|  | ||||
|     try: | ||||
|         with open(firmware_path, "rb") as f: | ||||
|             files = {"firmware": ("firmware.bin", f, "application/octet-stream")} | ||||
|  | ||||
|             # Send OTA update request | ||||
|             resp = requests.post(f"{base_url}/ota/upload", files=files, timeout=60) | ||||
|  | ||||
|             if resp.status_code in [200, 201, 204]: | ||||
|                 print(f"✓ OTA upload successful (status: {resp.status_code})") | ||||
|                 if resp.text: | ||||
|                     print(f"Response: {resp.text}") | ||||
|                 return True | ||||
|             else: | ||||
|                 print(f"✗ OTA upload failed with status {resp.status_code}") | ||||
|                 print(f"Response: {resp.text}") | ||||
|                 return False | ||||
|  | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         print(f"Error during upload: {e}") | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def test_ota_with_wrong_content_type(host, port): | ||||
|     """Test that OTA upload fails gracefully with wrong content type""" | ||||
|     base_url = f"http://{host}:{port}" | ||||
|  | ||||
|     print("\nTesting OTA with wrong content type...") | ||||
|  | ||||
|     try: | ||||
|         # Send plain text instead of multipart | ||||
|         headers = {"Content-Type": "text/plain"} | ||||
|         resp = requests.post( | ||||
|             f"{base_url}/ota/upload", | ||||
|             data="This is not a firmware file", | ||||
|             headers=headers, | ||||
|             timeout=10, | ||||
|         ) | ||||
|  | ||||
|         if resp.status_code >= 400: | ||||
|             print( | ||||
|                 f"✓ Server correctly rejected wrong content type (status: {resp.status_code})" | ||||
|             ) | ||||
|             return True | ||||
|         else: | ||||
|             print(f"✗ Server accepted wrong content type (status: {resp.status_code})") | ||||
|             return False | ||||
|  | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         print(f"Error: {e}") | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def test_ota_with_empty_file(host, port): | ||||
|     """Test that OTA upload fails gracefully with empty file""" | ||||
|     base_url = f"http://{host}:{port}" | ||||
|  | ||||
|     print("\nTesting OTA with empty file...") | ||||
|  | ||||
|     try: | ||||
|         # Send empty file | ||||
|         files = {"firmware": ("empty.bin", b"", "application/octet-stream")} | ||||
|         resp = requests.post(f"{base_url}/ota/upload", files=files, timeout=10) | ||||
|  | ||||
|         if resp.status_code >= 400: | ||||
|             print( | ||||
|                 f"✓ Server correctly rejected empty file (status: {resp.status_code})" | ||||
|             ) | ||||
|             return True | ||||
|         else: | ||||
|             print(f"✗ Server accepted empty file (status: {resp.status_code})") | ||||
|             return False | ||||
|  | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         print(f"Error: {e}") | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def create_test_firmware(size_kb=10): | ||||
|     """Create a dummy firmware file for testing""" | ||||
|     import tempfile | ||||
|  | ||||
|     with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: | ||||
|         # ESP32 firmware magic bytes | ||||
|         f.write(b"\xe9\x08\x02\x20") | ||||
|         # Add padding | ||||
|         f.write(b"\x00" * (size_kb * 1024 - 4)) | ||||
|         return f.name | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     parser = argparse.ArgumentParser( | ||||
|         description="Test ESP-IDF web server OTA functionality" | ||||
|     ) | ||||
|     parser.add_argument("--host", default="localhost", help="Device hostname or IP") | ||||
|     parser.add_argument("--port", type=int, default=8080, help="Web server port") | ||||
|     parser.add_argument( | ||||
|         "--firmware", help="Path to firmware file (if not specified, creates test file)" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--skip-error-tests", action="store_true", help="Skip error condition tests" | ||||
|     ) | ||||
|  | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # Create test firmware if not specified | ||||
|     firmware_path = args.firmware | ||||
|     if not firmware_path: | ||||
|         print("Creating test firmware file...") | ||||
|         firmware_path = create_test_firmware(100)  # 100KB test file | ||||
|         print(f"Created test firmware: {firmware_path}") | ||||
|  | ||||
|     all_passed = True | ||||
|  | ||||
|     # Test successful OTA upload | ||||
|     if not test_multipart_ota_upload(args.host, args.port, firmware_path): | ||||
|         all_passed = False | ||||
|  | ||||
|     # Test error conditions | ||||
|     if not args.skip_error_tests: | ||||
|         time.sleep(1)  # Small delay between tests | ||||
|  | ||||
|         if not test_ota_with_wrong_content_type(args.host, args.port): | ||||
|             all_passed = False | ||||
|  | ||||
|         time.sleep(1) | ||||
|  | ||||
|         if not test_ota_with_empty_file(args.host, args.port): | ||||
|             all_passed = False | ||||
|  | ||||
|     # Clean up test firmware if we created it | ||||
|     if not args.firmware: | ||||
|         import os | ||||
|  | ||||
|         os.unlink(firmware_path) | ||||
|         print("\nCleaned up test firmware") | ||||
|  | ||||
|     print(f"\n{'All tests passed!' if all_passed else 'Some tests failed!'}") | ||||
|     return 0 if all_passed else 1 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     sys.exit(main()) | ||||
| @@ -1,12 +1,33 @@ | ||||
| # Test configuration for ESP-IDF web server with OTA enabled | ||||
| esphome: | ||||
|   name: test-web-server-ota-idf | ||||
|  | ||||
| # Force ESP-IDF framework | ||||
| esp32: | ||||
|   board: esp32dev | ||||
|   framework: | ||||
|     type: esp-idf | ||||
|  | ||||
| packages: | ||||
|   device_base: !include common.yaml | ||||
|  | ||||
| # Enable OTA for this test | ||||
| # Enable OTA for multipart upload testing | ||||
| ota: | ||||
|   - platform: esphome | ||||
|     safe_mode: true | ||||
|     password: "test_ota_password" | ||||
|  | ||||
| # Web server with OTA enabled | ||||
| web_server: | ||||
|   port: 8080 | ||||
|   version: 2 | ||||
|   ota: true | ||||
|   include_internal: true | ||||
|  | ||||
| # Enable debug logging for OTA | ||||
| logger: | ||||
|   level: DEBUG | ||||
|   logs: | ||||
|     web_server: VERBOSE | ||||
|     web_server_idf: VERBOSE | ||||
|  | ||||
|   | ||||
							
								
								
									
										70
									
								
								tests/components/web_server/test_ota_readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								tests/components/web_server/test_ota_readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| # Testing ESP-IDF Web Server OTA Functionality | ||||
|  | ||||
| This directory contains tests for the ESP-IDF web server OTA (Over-The-Air) update functionality using multipart form uploads. | ||||
|  | ||||
| ## Test Files | ||||
|  | ||||
| - `test_ota.esp32-idf.yaml` - ESPHome configuration with OTA enabled for ESP-IDF | ||||
| - `test_no_ota.esp32-idf.yaml` - ESPHome configuration with OTA disabled | ||||
| - `test_ota_disabled.esp32-idf.yaml` - ESPHome configuration with web_server ota: false | ||||
| - `test_multipart_ota.py` - Manual test script for OTA functionality | ||||
| - `test_esp_idf_ota.py` - Automated pytest for OTA functionality | ||||
|  | ||||
| ## Running the Tests | ||||
|  | ||||
| ### 1. Compile and Flash Test Device | ||||
|  | ||||
| ```bash | ||||
| # Compile the OTA-enabled configuration | ||||
| esphome compile tests/components/web_server/test_ota.esp32-idf.yaml | ||||
|  | ||||
| # Flash to device | ||||
| esphome upload tests/components/web_server/test_ota.esp32-idf.yaml | ||||
| ``` | ||||
|  | ||||
| ### 2. Run Manual Tests | ||||
|  | ||||
| Once the device is running, you can test OTA functionality: | ||||
|  | ||||
| ```bash | ||||
| # Test with default settings (creates test firmware) | ||||
| python tests/components/web_server/test_multipart_ota.py --host <device-ip> | ||||
|  | ||||
| # Test with real firmware file | ||||
| python tests/components/web_server/test_multipart_ota.py --host <device-ip> --firmware <path-to-firmware.bin> | ||||
|  | ||||
| # Skip error condition tests (useful for production devices) | ||||
| python tests/components/web_server/test_multipart_ota.py --host <device-ip> --skip-error-tests | ||||
| ``` | ||||
|  | ||||
| ### 3. Run Automated Tests | ||||
|  | ||||
| ```bash | ||||
| # Run pytest suite | ||||
| pytest tests/component_tests/web_server/test_esp_idf_ota.py | ||||
| ``` | ||||
|  | ||||
| ## What's Being Tested | ||||
|  | ||||
| 1. **Multipart Upload**: Tests that firmware can be uploaded using multipart/form-data | ||||
| 2. **Error Handling**:  | ||||
|    - Wrong content type rejection | ||||
|    - Empty file rejection | ||||
|    - Concurrent upload handling | ||||
| 3. **Large Files**: Tests chunked processing of larger firmware files | ||||
| 4. **Boundary Parsing**: Tests various multipart boundary formats | ||||
|  | ||||
| ## Implementation Details | ||||
|  | ||||
| The ESP-IDF web server uses the `multipart-parser` library to handle multipart uploads. Key components: | ||||
|  | ||||
| - `MultipartReader` class for parsing multipart data | ||||
| - Chunked processing to handle large files without excessive memory use | ||||
| - Integration with ESPHome's OTA component for actual firmware updates | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| 1. **Connection Refused**: Make sure the device is on the network and the IP is correct | ||||
| 2. **404 Not Found**: Ensure OTA is enabled in the configuration (`ota: true` in web_server) | ||||
| 3. **Upload Fails**: Check device logs for detailed error messages | ||||
| 4. **Timeout**: Large firmware files may take time, increase timeout if needed | ||||
		Reference in New Issue
	
	Block a user