From 1781a052696938cd96c022e6d3a745dd2a4b5c07 Mon Sep 17 00:00:00 2001 From: HuangMason320 Date: Thu, 21 Aug 2025 00:31:45 +0800 Subject: [PATCH] feat: Add multi-series configuration testing and debugging tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive test scripts for multi-series dongle configuration - Add debugging tools for deployment and flow testing - Add configuration verification and guide utilities - Fix stdout/stderr handling in deployment dialog for PyInstaller builds - Includes port ID configuration tests and multi-series config validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- check_multi_series_config.py | 110 +++++++++++++++++++ debug_deployment.py | 58 ++++++++++ debug_multi_series_flow.py | 90 ++++++++++++++++ simple_test.py | 37 +++++++ test_multi_series_fix.py | 134 +++++++++++++++++++++++ test_multidongle_start.py | 46 ++++++++ test_port_id_config.py | 201 +++++++++++++++++++++++++++++++++++ ui/dialogs/deployment.py | 10 +- verify_properties.py | 41 +++++++ 9 files changed, 724 insertions(+), 3 deletions(-) create mode 100644 check_multi_series_config.py create mode 100644 debug_deployment.py create mode 100644 debug_multi_series_flow.py create mode 100644 simple_test.py create mode 100644 test_multi_series_fix.py create mode 100644 test_multidongle_start.py create mode 100644 test_port_id_config.py create mode 100644 verify_properties.py diff --git a/check_multi_series_config.py b/check_multi_series_config.py new file mode 100644 index 0000000..2d8d3a7 --- /dev/null +++ b/check_multi_series_config.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Check current multi-series configuration in saved .mflow files +""" + +import json +import os +import glob + +def check_mflow_files(): + """Check .mflow files for multi-series configuration""" + + # Look for .mflow files in common locations + search_paths = [ + "*.mflow", + "flows/*.mflow", + "examples/*.mflow", + "../*.mflow" + ] + + mflow_files = [] + for pattern in search_paths: + mflow_files.extend(glob.glob(pattern)) + + if not mflow_files: + print("No .mflow files found in current directory") + return + + print(f"Found {len(mflow_files)} .mflow file(s):") + + for mflow_file in mflow_files: + print(f"\n=== Checking {mflow_file} ===") + + try: + with open(mflow_file, 'r') as f: + data = json.load(f) + + # Look for nodes with type "Model" or "ExactModelNode" + nodes = data.get('nodes', []) + model_nodes = [node for node in nodes if node.get('type') in ['Model', 'ExactModelNode']] + + if not model_nodes: + print(" No Model nodes found") + continue + + for i, node in enumerate(model_nodes): + print(f"\n Model Node {i+1}:") + print(f" Name: {node.get('name', 'Unnamed')}") + + # Check both custom_properties and properties for multi-series config + custom_properties = node.get('custom_properties', {}) + properties = node.get('properties', {}) + + # Multi-series config is typically in custom_properties + config_props = custom_properties if custom_properties else properties + + # Check multi-series configuration + multi_series_mode = config_props.get('multi_series_mode', False) + enabled_series = config_props.get('enabled_series', []) + + print(f" multi_series_mode: {multi_series_mode}") + print(f" enabled_series: {enabled_series}") + + if multi_series_mode: + print(" Multi-series port configurations:") + for series in ['520', '720', '630', '730', '540']: + port_ids = config_props.get(f'kl{series}_port_ids', '') + if port_ids: + print(f" kl{series}_port_ids: '{port_ids}'") + + assets_folder = config_props.get('assets_folder', '') + if assets_folder: + print(f" assets_folder: '{assets_folder}'") + else: + print(" assets_folder: (not set)") + else: + print(" Multi-series mode is DISABLED") + print(" Current single-series configuration:") + port_ids = properties.get('port_ids', []) + model_path = properties.get('model_path', '') + print(f" port_ids: {port_ids}") + print(f" model_path: '{model_path}'") + + except Exception as e: + print(f" Error reading file: {e}") + +def print_configuration_guide(): + """Print guide for setting up multi-series configuration""" + print("\n" + "="*60) + print("MULTI-SERIES CONFIGURATION GUIDE") + print("="*60) + print() + print("To enable multi-series inference, set these properties in your Model Node:") + print() + print("1. multi_series_mode = True") + print("2. enabled_series = ['520', '720']") + print("3. kl520_port_ids = '28,32'") + print("4. kl720_port_ids = '4'") + print("5. assets_folder = (optional, for auto model/firmware detection)") + print() + print("Expected devices found:") + print(" KL520 devices on ports: 28, 32") + print(" KL720 device on port: 4") + print() + print("If multi_series_mode is False or not set, the system will use") + print("single-series mode with only the first available device.") + +if __name__ == "__main__": + check_mflow_files() + print_configuration_guide() \ No newline at end of file diff --git a/debug_deployment.py b/debug_deployment.py new file mode 100644 index 0000000..d211fd4 --- /dev/null +++ b/debug_deployment.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Debug deployment error +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def simulate_deployment(): + """Simulate the deployment process to find the Optional error""" + try: + print("Testing export_pipeline_data equivalent...") + + # Simulate creating a node and getting properties + from core.nodes.exact_nodes import ExactModelNode + + # This would be similar to what dashboard does + node = ExactModelNode() + print("Node created") + + # Check if node has get_business_properties + if hasattr(node, 'get_business_properties'): + print("Node has get_business_properties") + try: + props = node.get_business_properties() + print(f"Properties extracted: {type(props)}") + except Exception as e: + print(f"Error in get_business_properties: {e}") + import traceback + traceback.print_exc() + + # Test the mflow converter directly + print("\nTesting MFlowConverter...") + from core.functions.mflow_converter import MFlowConverter + converter = MFlowConverter(default_fw_path='.') + print("MFlowConverter created successfully") + + # Test multi-series config building + test_props = { + 'multi_series_mode': True, + 'enabled_series': ['520', '720'], + 'kl520_port_ids': '28,32', + 'kl720_port_ids': '4' + } + + config = converter._build_multi_series_config_from_properties(test_props) + print(f"Multi-series config: {config}") + + print("All tests passed!") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + simulate_deployment() \ No newline at end of file diff --git a/debug_multi_series_flow.py b/debug_multi_series_flow.py new file mode 100644 index 0000000..8ab3ff3 --- /dev/null +++ b/debug_multi_series_flow.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Debug the multi-series configuration flow +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def test_full_flow(): + """Test the complete multi-series configuration flow""" + print("=== Testing Multi-Series Configuration Flow ===") + + # Simulate node properties as they would appear in the UI + mock_node_properties = { + 'multi_series_mode': True, + 'enabled_series': ['520', '720'], + 'kl520_port_ids': '28,32', + 'kl720_port_ids': '4', + 'assets_folder': '', + 'max_queue_size': 100 + } + + print(f"1. Mock node properties: {mock_node_properties}") + + # Test the mflow converter building multi-series config + try: + from core.functions.mflow_converter import MFlowConverter + converter = MFlowConverter(default_fw_path='.') + + config = converter._build_multi_series_config_from_properties(mock_node_properties) + print(f"2. Multi-series config built: {config}") + + if config: + print(" [OK] Multi-series config successfully built") + + # Test StageConfig creation + from core.functions.InferencePipeline import StageConfig + + stage_config = StageConfig( + stage_id="test_stage", + port_ids=[], # Not used in multi-series + scpu_fw_path='', + ncpu_fw_path='', + model_path='', + upload_fw=False, + multi_series_mode=True, + multi_series_config=config + ) + + print(f"3. StageConfig created with multi_series_mode: {stage_config.multi_series_mode}") + print(f" Multi-series config: {stage_config.multi_series_config}") + + # Test what would happen in PipelineStage initialization + print("4. Testing PipelineStage initialization logic:") + if stage_config.multi_series_mode and stage_config.multi_series_config: + print(" [OK] Would initialize MultiDongle with multi_series_config") + print(f" MultiDongle(multi_series_config={stage_config.multi_series_config})") + else: + print(" [ERROR] Would fall back to single-series mode") + + else: + print(" [ERROR] Multi-series config is None - this is the problem!") + + except Exception as e: + print(f"Error in flow test: {e}") + import traceback + traceback.print_exc() + +def test_node_direct(): + """Test creating a node directly and getting its inference config""" + print("\n=== Testing Node Direct Configuration ===") + + try: + from core.nodes.exact_nodes import ExactModelNode + + # This won't work without NodeGraphQt, but let's see what happens + node = ExactModelNode() + print("Node created (mock mode)") + + # Test the get_business_properties method that would be called during export + props = node.get_business_properties() + print(f"Business properties: {props}") + + except Exception as e: + print(f"Error in node test: {e}") + +if __name__ == "__main__": + test_full_flow() + test_node_direct() \ No newline at end of file diff --git a/simple_test.py b/simple_test.py new file mode 100644 index 0000000..dacded5 --- /dev/null +++ b/simple_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +Simple test for port ID configuration +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from core.nodes.exact_nodes import ExactModelNode + +def main(): + print("Creating ExactModelNode...") + node = ExactModelNode() + + print("Testing property options...") + if hasattr(node, '_property_options'): + port_props = [k for k in node._property_options.keys() if 'port_ids' in k] + print(f"Found port ID properties: {port_props}") + else: + print("No _property_options found") + + print("Testing _build_multi_series_config method...") + if hasattr(node, '_build_multi_series_config'): + print("Method exists") + try: + config = node._build_multi_series_config() + print(f"Config result: {config}") + except Exception as e: + print(f"Error calling method: {e}") + else: + print("Method does not exist") + + print("Test completed!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_multi_series_fix.py b/test_multi_series_fix.py new file mode 100644 index 0000000..a227d5d --- /dev/null +++ b/test_multi_series_fix.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +Test script to verify multi-series configuration fix +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# Test the mflow_converter functionality +def test_multi_series_config_building(): + """Test building multi-series config from properties""" + print("Testing multi-series config building...") + + from core.functions.mflow_converter import MFlowConverter + + # Create converter instance + converter = MFlowConverter(default_fw_path='.') + + # Mock properties data that would come from a node + test_properties = { + 'multi_series_mode': True, + 'enabled_series': ['520', '720'], + 'kl520_port_ids': '28,32', + 'kl720_port_ids': '4', + 'assets_folder': '', # Empty for this test + 'max_queue_size': 100 + } + + # Test building config + config = converter._build_multi_series_config_from_properties(test_properties) + + print(f"Generated config: {config}") + + if config: + # Verify structure + assert 'KL520' in config, "KL520 should be in config" + assert 'KL720' in config, "KL720 should be in config" + + # Check KL520 config + kl520_config = config['KL520'] + assert 'port_ids' in kl520_config, "KL520 should have port_ids" + assert kl520_config['port_ids'] == [28, 32], f"KL520 port_ids should be [28, 32], got {kl520_config['port_ids']}" + + # Check KL720 config + kl720_config = config['KL720'] + assert 'port_ids' in kl720_config, "KL720 should have port_ids" + assert kl720_config['port_ids'] == [4], f"KL720 port_ids should be [4], got {kl720_config['port_ids']}" + + print("[OK] Multi-series config structure is correct") + else: + print("[ERROR] Config building returned None") + return False + + # Test with invalid port IDs + invalid_properties = { + 'multi_series_mode': True, + 'enabled_series': ['520'], + 'kl520_port_ids': 'invalid,port,ids', + 'assets_folder': '' + } + + invalid_config = converter._build_multi_series_config_from_properties(invalid_properties) + assert invalid_config is None, "Invalid port IDs should result in None config" + print("[OK] Invalid port IDs handled correctly") + + return True + +def test_stage_config(): + """Test StageConfig with multi-series support""" + print("\\nTesting StageConfig with multi-series...") + + from core.functions.InferencePipeline import StageConfig + + # Test creating StageConfig with multi-series + multi_series_config = { + "KL520": {"port_ids": [28, 32]}, + "KL720": {"port_ids": [4]} + } + + stage_config = StageConfig( + stage_id="test_stage", + port_ids=[], # Not used in multi-series mode + scpu_fw_path='', + ncpu_fw_path='', + model_path='', + upload_fw=False, + multi_series_mode=True, + multi_series_config=multi_series_config + ) + + print(f"Created StageConfig with multi_series_mode: {stage_config.multi_series_mode}") + print(f"Multi-series config: {stage_config.multi_series_config}") + + assert stage_config.multi_series_mode == True, "multi_series_mode should be True" + assert stage_config.multi_series_config == multi_series_config, "multi_series_config should match" + + print("[OK] StageConfig supports multi-series configuration") + return True + +def main(): + """Run all tests""" + print("Testing Multi-Series Configuration Fix") + print("=" * 50) + + try: + # Test config building + if not test_multi_series_config_building(): + print("[ERROR] Config building test failed") + return False + + # Test StageConfig + if not test_stage_config(): + print("[ERROR] StageConfig test failed") + return False + + print("\\n" + "=" * 50) + print("[SUCCESS] All tests passed!") + print("\\nThe fix should now properly:") + print("1. Detect multi_series_mode from node properties") + print("2. Build multi_series_config from series-specific port IDs") + print("3. Pass the config to MultiDongle for true multi-series operation") + + return True + + except Exception as e: + print(f"[ERROR] Test failed with exception: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_multidongle_start.py b/test_multidongle_start.py new file mode 100644 index 0000000..6ab038d --- /dev/null +++ b/test_multidongle_start.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +Test MultiDongle start/stop functionality +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def test_multidongle_start(): + """Test MultiDongle start method""" + try: + from core.functions.Multidongle import MultiDongle + + # Test multi-series configuration + multi_series_config = { + "KL520": {"port_ids": [28, 32]}, + "KL720": {"port_ids": [4]} + } + + print("Creating MultiDongle with multi-series config...") + multidongle = MultiDongle(multi_series_config=multi_series_config) + + print(f"Multi-series mode: {multidongle.multi_series_mode}") + print(f"Has _start_multi_series method: {hasattr(multidongle, '_start_multi_series')}") + print(f"Has _stop_multi_series method: {hasattr(multidongle, '_stop_multi_series')}") + + print("MultiDongle created successfully!") + + # Test that the required attributes exist + expected_attrs = ['send_threads', 'receive_threads', 'dispatcher_thread', 'result_ordering_thread'] + for attr in expected_attrs: + if hasattr(multidongle, attr): + print(f"[OK] Has attribute: {attr}") + else: + print(f"[ERROR] Missing attribute: {attr}") + + print("Test completed successfully!") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + test_multidongle_start() \ No newline at end of file diff --git a/test_port_id_config.py b/test_port_id_config.py new file mode 100644 index 0000000..508f6d2 --- /dev/null +++ b/test_port_id_config.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Test script for new series-specific port ID configuration functionality +""" + +import sys +import os + +# Add the project root to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + from core.nodes.exact_nodes import ExactModelNode + print("[OK] Successfully imported ExactModelNode") +except ImportError as e: + print(f"[ERROR] Failed to import ExactModelNode: {e}") + sys.exit(1) + +def test_port_id_properties(): + """Test that new port ID properties are created correctly""" + print("\n=== Testing Port ID Properties Creation ===") + + try: + node = ExactModelNode() + + # Test that all series port ID properties exist + series_properties = ['kl520_port_ids', 'kl720_port_ids', 'kl630_port_ids', 'kl730_port_ids', 'kl540_port_ids'] + + for prop in series_properties: + if hasattr(node, 'get_property'): + try: + value = node.get_property(prop) + print(f"[OK] Property {prop} exists with value: '{value}'") + except: + print(f"[ERROR] Property {prop} does not exist or cannot be accessed") + else: + print(f"[WARN] Node does not have get_property method (NodeGraphQt not available)") + break + + # Test property options + if hasattr(node, '_property_options'): + for prop in series_properties: + if prop in node._property_options: + options = node._property_options[prop] + print(f"[OK] Property options for {prop}: {options}") + else: + print(f"[ERROR] No property options found for {prop}") + else: + print("[WARN] Node does not have _property_options") + + except Exception as e: + print(f"[ERROR] Error testing port ID properties: {e}") + +def test_display_properties(): + """Test that display properties work correctly""" + print("\n=== Testing Display Properties ===") + + try: + node = ExactModelNode() + + if not hasattr(node, 'get_display_properties'): + print("[WARN] Node does not have get_display_properties method (NodeGraphQt not available)") + return + + # Test single-series mode + if hasattr(node, 'set_property'): + node.set_property('multi_series_mode', False) + single_props = node.get_display_properties() + print(f"[OK] Single-series display properties: {single_props}") + + # Test multi-series mode + node.set_property('multi_series_mode', True) + node.set_property('enabled_series', ['520', '720']) + multi_props = node.get_display_properties() + print(f"[OK] Multi-series display properties: {multi_props}") + + # Check if port ID properties are included + expected_port_props = ['kl520_port_ids', 'kl720_port_ids'] + found_port_props = [prop for prop in multi_props if prop in expected_port_props] + print(f"[OK] Found port ID properties in display: {found_port_props}") + + # Test with different enabled series + node.set_property('enabled_series', ['630', '730']) + multi_props_2 = node.get_display_properties() + print(f"[OK] Display properties with KL630/730: {multi_props_2}") + + else: + print("[WARN] Node does not have set_property method (NodeGraphQt not available)") + + except Exception as e: + print(f"[ERROR] Error testing display properties: {e}") + +def test_multi_series_config(): + """Test multi-series configuration building""" + print("\n=== Testing Multi-Series Config Building ===") + + try: + node = ExactModelNode() + + if not hasattr(node, '_build_multi_series_config'): + print("[ERROR] Node does not have _build_multi_series_config method") + return + + if not hasattr(node, 'set_property'): + print("[WARN] Node does not have set_property method (NodeGraphQt not available)") + return + + # Test with sample configuration + node.set_property('enabled_series', ['520', '720']) + node.set_property('kl520_port_ids', '28,32') + node.set_property('kl720_port_ids', '30,34') + node.set_property('assets_folder', '/fake/assets/path') + + # Build multi-series config + config = node._build_multi_series_config() + print(f"[OK] Generated multi-series config: {config}") + + # Verify structure + if config: + expected_keys = ['KL520', 'KL720'] + for key in expected_keys: + if key in config: + series_config = config[key] + print(f"[OK] {key} config: {series_config}") + + if 'port_ids' in series_config: + print(f" - Port IDs: {series_config['port_ids']}") + else: + print(f" [ERROR] Missing port_ids in {key} config") + else: + print(f"[ERROR] Missing {key} in config") + else: + print("[ERROR] Generated config is None or empty") + + # Test with invalid port IDs + node.set_property('kl520_port_ids', 'invalid,port,ids') + config_invalid = node._build_multi_series_config() + print(f"[OK] Config with invalid port IDs: {config_invalid}") + + except Exception as e: + print(f"[ERROR] Error testing multi-series config: {e}") + +def test_inference_config(): + """Test inference configuration""" + print("\n=== Testing Inference Config ===") + + try: + node = ExactModelNode() + + if not hasattr(node, 'get_inference_config'): + print("[ERROR] Node does not have get_inference_config method") + return + + if not hasattr(node, 'set_property'): + print("[WARN] Node does not have set_property method (NodeGraphQt not available)") + return + + # Test multi-series inference config + node.set_property('multi_series_mode', True) + node.set_property('enabled_series', ['520', '720']) + node.set_property('kl520_port_ids', '28,32') + node.set_property('kl720_port_ids', '30,34') + node.set_property('assets_folder', '/fake/assets') + node.set_property('max_queue_size', 50) + + inference_config = node.get_inference_config() + print(f"[OK] Inference config: {inference_config}") + + # Check if multi_series_config is included + if 'multi_series_config' in inference_config: + ms_config = inference_config['multi_series_config'] + print(f"[OK] Multi-series config included: {ms_config}") + else: + print("[WARN] Multi-series config not found in inference config") + + # Test single-series mode + node.set_property('multi_series_mode', False) + node.set_property('model_path', '/fake/model.nef') + node.set_property('port_id', '28') + + single_config = node.get_inference_config() + print(f"[OK] Single-series config: {single_config}") + + except Exception as e: + print(f"[ERROR] Error testing inference config: {e}") + +def main(): + """Run all tests""" + print("Testing Series-Specific Port ID Configuration") + print("=" * 50) + + test_port_id_properties() + test_display_properties() + test_multi_series_config() + test_inference_config() + + print("\n" + "=" * 50) + print("Test completed!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ui/dialogs/deployment.py b/ui/dialogs/deployment.py index 157582b..dfa83f6 100644 --- a/ui/dialogs/deployment.py +++ b/ui/dialogs/deployment.py @@ -79,8 +79,10 @@ class StdoutCapture: def write(self, text): # Write to original stdout/stderr (so it still appears in terminal) - self.original.write(text) - self.original.flush() + # Check if original exists (it might be None in PyInstaller builds) + if self.original is not None: + self.original.write(text) + self.original.flush() # Capture for GUI if it's a substantial message and not already emitting if text.strip() and not self._emitting: @@ -91,7 +93,9 @@ class StdoutCapture: self._emitting = False def flush(self): - self.original.flush() + # Check if original exists before calling flush + if self.original is not None: + self.original.flush() # Replace stdout and stderr with our tee writers sys.stdout = TeeWriter(self.original_stdout, self.captured_output, self.signal_emitter) diff --git a/verify_properties.py b/verify_properties.py new file mode 100644 index 0000000..a9f3c49 --- /dev/null +++ b/verify_properties.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +""" +Verify that properties are correctly set for multi-series +""" + +def verify_properties(): + """Check the expected multi-series properties""" + + print("Multi-Series Configuration Checklist:") + print("=" * 50) + + print("\n1. In your Dashboard, Model Node properties should have:") + print(" ✓ multi_series_mode = True") + print(" ✓ enabled_series = ['520', '720']") + print(" ✓ kl520_port_ids = '28,32'") + print(" ✓ kl720_port_ids = '4'") + print(" ✓ assets_folder = (optional, for auto model/firmware detection)") + + print("\n2. After setting these properties, when you deploy:") + print(" Expected output should show:") + print(" '[stage_1_Model_Node] Using multi-series mode with config: ...'") + print(" NOT: 'Single-series config converted to multi-series format'") + + print("\n3. If you still see single-series behavior:") + print(" a) Double-check property names (they should be lowercase)") + print(" b) Make sure multi_series_mode is checked/enabled") + print(" c) Verify port IDs are comma-separated strings") + print(" d) Save the .mflow file and re-deploy") + + print("\n4. Property format reference:") + print(" - kl520_port_ids: '28,32' (string, comma-separated)") + print(" - kl720_port_ids: '4' (string)") + print(" - enabled_series: ['520', '720'] (list)") + print(" - multi_series_mode: True (boolean)") + + print("\n" + "=" * 50) + print("If properties are set correctly, your deployment should use") + print("true multi-series load balancing across KL520 and KL720 dongles!") + +if __name__ == "__main__": + verify_properties() \ No newline at end of file