fix: Rewrite DynamicSymbolLoader to prevent schematic file corruption (#40)

Fixes Issue #26 - add_schematic_component corrupts .kicad_sch files

Complete rewrite of DynamicSymbolLoader to use text manipulation instead of sexpdata:
- Preserves KiCAD file formatting perfectly
- Correctly handles KiCAD 9 symbol naming conventions (library prefix for top-level, no prefix for sub-symbols)
- Resolves parent-child symbol dependencies with (extends)
- Adds symbol caching for performance
- Simplifies component addition workflow

Tested on KiCAD 9.0.7 with R, C, LED, ESP32, and transistor components.
Compatible with recent Windows fixes (UUID generation, IPC, JLCPCB).

Co-authored-by: FlowSync0 <249798311+FlowSync0@users.noreply.github.com>
This commit is contained in:
FlowSync0
2026-02-26 17:02:10 +01:00
committed by GitHub
parent e4fa774eda
commit a69d288251
2 changed files with 2268 additions and 2382 deletions

View File

@@ -1,404 +1,346 @@
""" """
Dynamic Symbol Loader for KiCad Schematics Dynamic Symbol Loader for KiCad Schematics
Loads symbols from .kicad_sym library files and injects them into schematics Loads symbols from .kicad_sym library files and injects them into schematics
on-the-fly, eliminating the need for static templates. on-the-fly using TEXT MANIPULATION (not sexpdata) to preserve file formatting.
This enables access to all ~10,000+ KiCad symbols dynamically. This enables access to all ~10,000+ KiCad symbols dynamically.
""" """
import os import os
import uuid import re
import logging import uuid
from pathlib import Path import logging
from typing import Dict, List, Optional, Tuple from pathlib import Path
import sexpdata from typing import Dict, List, Optional, Tuple
from sexpdata import Symbol
logger = logging.getLogger('kicad_interface')
logger = logging.getLogger('kicad_interface')
class DynamicSymbolLoader:
class DynamicSymbolLoader: """
""" Dynamically loads symbols from KiCad library files and injects them into schematics.
Dynamically loads symbols from KiCad library files and injects them into schematics
Uses raw text manipulation instead of sexpdata to avoid corrupting the KiCad file format.
Workflow:
1. Parse .kicad_sym library file to extract symbol definition Key rules for KiCad 9 .kicad_sch format:
2. Inject symbol definition into schematic's lib_symbols section - Top-level symbols in lib_symbols must have library prefix: (symbol "Device:R" ...)
3. Create an offscreen template instance that can be cloned - Sub-symbols must NOT have library prefix: (symbol "R_0_1" ...), (symbol "R_1_1" ...)
4. Clone the template to create actual component instances - Parent symbols must appear BEFORE child symbols that use (extends ...)
""" """
def __init__(self): def __init__(self):
"""Initialize the dynamic symbol loader""" self.symbol_cache = {} # Cache: "lib:symbol" -> raw text block
self.library_cache = {} # Cache parsed library files: path -> parsed data
self.symbol_cache = {} # Cache extracted symbols: "lib:symbol" -> symbol_def def find_kicad_symbol_libraries(self) -> List[Path]:
"""Find all KiCad symbol library directories"""
def find_kicad_symbol_libraries(self) -> List[Path]: possible_paths = [
""" Path("/usr/share/kicad/symbols"),
Find all KiCad symbol library directories Path("/usr/local/share/kicad/symbols"),
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
Returns: Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
List of paths to symbol library directories Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
""" Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
possible_paths = [ Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
# Linux ]
Path("/usr/share/kicad/symbols"), for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
Path("/usr/local/share/kicad/symbols"), if env_var in os.environ:
# Windows possible_paths.insert(0, Path(os.environ[env_var]))
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"), return [p for p in possible_paths if p.exists() and p.is_dir()]
# macOS
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"), def find_library_file(self, library_name: str) -> Optional[Path]:
# User libraries """Find the .kicad_sym file for a given library name"""
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols", for lib_dir in self.find_kicad_symbol_libraries():
Path.home() / ".local" / "share" / "kicad" / "8.0" / "symbols", lib_file = lib_dir / f"{library_name}.kicad_sym"
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols", if lib_file.exists():
] return lib_file
logger.warning(f"Library file not found: {library_name}.kicad_sym")
# Check environment variables return None
for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
if env_var in os.environ: def _extract_symbol_block(self, text: str, symbol_name: str) -> Optional[str]:
possible_paths.insert(0, Path(os.environ[env_var])) """
Extract a complete symbol block from a library or schematic file by matching
found_paths = [] parentheses depth. Returns the raw text of the symbol definition.
for path in possible_paths: """
if path.exists() and path.is_dir(): lines = text.split('\n')
found_paths.append(path) start = None
logger.info(f"Found KiCad symbol library directory: {path}") for i, line in enumerate(lines):
stripped = line.strip()
return found_paths # Match exact symbol name (not sub-symbols like Name_0_1)
if stripped.startswith(f'(symbol "{symbol_name}"') and \
def find_library_file(self, library_name: str) -> Optional[Path]: not re.match(r'.*_\d+_\d+"', stripped):
""" start = i
Find the .kicad_sym file for a given library name break
Args: if start is None:
library_name: Library name (e.g., "Device", "Connector_Generic") return None
Returns: depth = 0
Path to .kicad_sym file or None if not found end = None
""" for i in range(start, len(lines)):
library_dirs = self.find_kicad_symbol_libraries() for ch in lines[i]:
if ch == '(':
for lib_dir in library_dirs: depth += 1
lib_file = lib_dir / f"{library_name}.kicad_sym" elif ch == ')':
if lib_file.exists(): depth -= 1
logger.debug(f"Found library file: {lib_file}") if depth == 0:
return lib_file end = i
break
logger.warning(f"Library file not found: {library_name}.kicad_sym") if end is not None:
return None break
def parse_library_file(self, library_path: Path) -> List: if end is None:
""" return None
Parse a .kicad_sym file into S-expression data structure
return '\n'.join(lines[start:end + 1])
Args:
library_path: Path to .kicad_sym file def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
"""
Returns: Extract a symbol definition from a KiCad .kicad_sym library file.
Parsed S-expression data Returns the raw text block, ready to be injected into a schematic.
"""
# Check cache first The returned block has:
cache_key = str(library_path) - Top-level name prefixed with library: (symbol "Library:Name" ...)
if cache_key in self.library_cache: - Sub-symbol names WITHOUT prefix: (symbol "Name_0_1" ...)
logger.debug(f"Using cached library data for: {library_path.name}") """
return self.library_cache[cache_key] cache_key = f"{library_name}:{symbol_name}"
if cache_key in self.symbol_cache:
logger.info(f"Parsing library file: {library_path}") return self.symbol_cache[cache_key]
try: lib_path = self.find_library_file(library_name)
with open(library_path, 'r', encoding='utf-8') as f: if not lib_path:
content = f.read() return None
# Parse S-expression with open(lib_path, 'r', encoding='utf-8') as f:
parsed = sexpdata.loads(content) lib_content = f.read()
# Cache the result block = self._extract_symbol_block(lib_content, symbol_name)
self.library_cache[cache_key] = parsed if block is None:
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
logger.debug(f"Successfully parsed library: {library_path.name}") return None
return parsed
# Check if this symbol uses (extends "ParentName")
except Exception as e: extends_match = re.search(r'\(extends "([^"]+)"\)', block)
logger.error(f"Error parsing library file {library_path}: {e}") parent_block = None
raise if extends_match:
parent_name = extends_match.group(1)
def extract_symbol_definition(self, library_path: Path, symbol_name: str) -> Optional[List]: logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
""" parent_block = self._extract_symbol_block(lib_content, parent_name)
Extract a specific symbol definition from a library file if parent_block:
# Prefix parent top-level name with library
Args: parent_block = parent_block.replace(
library_path: Path to .kicad_sym file f'(symbol "{parent_name}"',
symbol_name: Name of symbol to extract (e.g., "R", "LED") f'(symbol "{library_name}:{parent_name}"',
1 # Only first occurrence (top-level)
Returns: )
Symbol definition as S-expression list, or None if not found
""" # Prefix top-level symbol name with library
cache_key = f"{library_path.name}:{symbol_name}" full_name = f"{library_name}:{symbol_name}"
if cache_key in self.symbol_cache: block = block.replace(
logger.debug(f"Using cached symbol: {cache_key}") f'(symbol "{symbol_name}"',
return self.symbol_cache[cache_key] f'(symbol "{full_name}"',
1 # Only first occurrence (top-level)
parsed_lib = self.parse_library_file(library_path) )
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
# Library structure: (kicad_symbol_lib (version ...) (generator ...) (symbol ...) (symbol ...) ...)
# We need to find the symbol with matching name # Combine parent + child if extends is used
if parent_block:
for item in parsed_lib: result = parent_block + '\n' + block
if isinstance(item, list) and len(item) > 0: else:
if item[0] == Symbol('symbol'): result = block
# Symbol structure: (symbol "Name" ...)
if len(item) > 1 and isinstance(item[1], str): self.symbol_cache[cache_key] = result
# Handle both "Device:R" and "R" formats logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
item_name = item[1] return result
if ':' in item_name:
item_name = item_name.split(':')[1] def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool:
"""
if item_name == symbol_name: Inject a symbol definition into a schematic's lib_symbols section.
logger.info(f"Found symbol definition: {symbol_name}") Uses text manipulation to preserve file formatting.
# Cache and return """
self.symbol_cache[cache_key] = item full_name = f"{library_name}:{symbol_name}"
return item
with open(schematic_path, 'r', encoding='utf-8') as f:
logger.warning(f"Symbol '{symbol_name}' not found in {library_path.name}") content = f.read()
return None
# Check if symbol already exists
def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool: if f'(symbol "{full_name}"' in content:
""" logger.info(f"Symbol {full_name} already exists in schematic")
Inject a symbol definition from a library into a schematic file return True
Args: # Extract symbol from library
schematic_path: Path to .kicad_sch file to modify symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
library_name: Source library name (e.g., "Device") if not symbol_block:
symbol_name: Symbol to inject (e.g., "R") raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
Returns: # Indent the block to match lib_symbols indentation (4 spaces for top-level)
True if successful, False otherwise indented_lines = []
""" for line in symbol_block.split('\n'):
try: # Add 4-space indent for the content inside lib_symbols
# 1. Find and parse the library file indented_lines.append(' ' + line if line.strip() else line)
library_path = self.find_library_file(library_name) indented_block = '\n'.join(indented_lines)
if not library_path:
raise ValueError(f"Library not found: {library_name}") # Find the end of lib_symbols section to insert before closing )
lines = content.split('\n')
# 2. Extract the symbol definition lib_sym_start = None
symbol_def = self.extract_symbol_definition(library_path, symbol_name) lib_sym_end = None
if not symbol_def: depth = 0
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
for i, line in enumerate(lines):
# 3. Read the schematic file if '(lib_symbols' in line and lib_sym_start is None:
with open(schematic_path, 'r', encoding='utf-8') as f: lib_sym_start = i
sch_content = f.read() depth = 0
for ch in line:
sch_data = sexpdata.loads(sch_content) if ch == '(':
depth += 1
# 4. Find the lib_symbols section elif ch == ')':
lib_symbols_index = None depth -= 1
for i, item in enumerate(sch_data): continue
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('lib_symbols'): if lib_sym_start is not None and lib_sym_end is None:
lib_symbols_index = i for ch in line:
break if ch == '(':
depth += 1
if lib_symbols_index is None: elif ch == ')':
raise ValueError("No lib_symbols section found in schematic") depth -= 1
if depth == 0:
# 5. Check if symbol already exists in lib_symbols lib_sym_end = i
full_symbol_name = f"{library_name}:{symbol_name}" break
symbol_exists = False if lib_sym_end is not None:
break
for item in sch_data[lib_symbols_index][1:]: # Skip the 'lib_symbols' symbol itself
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol('symbol'): if lib_sym_end is None:
if item[1] == full_symbol_name or item[1] == symbol_name: raise ValueError("No lib_symbols section found in schematic")
logger.info(f"Symbol {full_symbol_name} already exists in schematic")
symbol_exists = True # Insert the symbol block just before the closing ) of lib_symbols
break lines.insert(lib_sym_end, indented_block)
if not symbol_exists: with open(schematic_path, 'w', encoding='utf-8') as f:
# 6. Inject the symbol definition f.write('\n'.join(lines))
# Need to update the symbol name to include library prefix
modified_symbol_def = list(symbol_def) # Make a copy logger.info(f"Injected symbol {full_name} into {schematic_path.name}")
modified_symbol_def[1] = full_symbol_name # Update name to "Library:Symbol" return True
sch_data[lib_symbols_index].append(modified_symbol_def) def create_component_instance(self, schematic_path: Path, library_name: str,
logger.info(f"Injected symbol {full_symbol_name} into schematic") symbol_name: str, reference: str,
value: str = "", x: float = 0, y: float = 0) -> bool:
# 7. Write the modified schematic back """
with open(schematic_path, 'w', encoding='utf-8') as f: Add a component instance to the schematic.
output = sexpdata.dumps(sch_data) This creates the (symbol ...) block with lib_id reference.
f.write(output) """
full_lib_id = f"{library_name}:{symbol_name}"
logger.info(f"Successfully injected symbol {full_symbol_name} into {schematic_path.name}") new_uuid = str(uuid.uuid4())
return True
instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
except Exception as e: (in_bom yes) (on_board yes) (dnp no)
logger.error(f"Error injecting symbol into schematic: {e}") (uuid "{new_uuid}")
raise (property "Reference" "{reference}" (at {x} {y - 2.54} 0)
(effects (font (size 1.27 1.27)))
def create_template_instance(self, schematic_path: Path, library_name: str, symbol_name: str, )
template_ref: Optional[str] = None) -> str: (property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
""" (effects (font (size 1.27 1.27)))
Create an offscreen template instance of a symbol that can be cloned )
(property "Footprint" "" (at {x} {y} 0)
Args: (effects (font (size 1.27 1.27)) (hide yes))
schematic_path: Path to .kicad_sch file )
library_name: Library name (e.g., "Device") (property "Datasheet" "~" (at {x} {y} 0)
symbol_name: Symbol name (e.g., "R") (effects (font (size 1.27 1.27)) (hide yes))
template_ref: Optional custom reference (defaults to _TEMPLATE_{LIBRARY}_{SYMBOL}) )
)'''
Returns:
Template reference name with open(schematic_path, 'r', encoding='utf-8') as f:
""" content = f.read()
try:
if template_ref is None: # Insert before (sheet_instances or at end before final )
# Clean up library and symbol names for reference lines = content.split('\n')
lib_clean = library_name.replace('-', '_').replace('.', '_') insert_pos = None
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}" for i, line in enumerate(lines):
if '(sheet_instances' in line:
# Read schematic insert_pos = i
with open(schematic_path, 'r', encoding='utf-8') as f: break
sch_content = f.read()
if insert_pos is None:
sch_data = sexpdata.loads(sch_content) # Insert before the last closing parenthesis
for i in range(len(lines) - 1, -1, -1):
# Check if template already exists if lines[i].strip() == ')':
for item in sch_data: insert_pos = i
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('symbol'): break
# Find Reference property
for prop in item: if insert_pos is None:
if isinstance(prop, list) and len(prop) > 2 and prop[0] == Symbol('property'): raise ValueError("Could not find insertion point in schematic")
if prop[1] == "Reference" and prop[2] == template_ref:
logger.info(f"Template instance {template_ref} already exists") lines.insert(insert_pos, instance_block)
return template_ref
with open(schematic_path, 'w', encoding='utf-8') as f:
# Find sheet_instances index (we'll insert before this) f.write('\n'.join(lines))
sheet_instances_index = None
for i, item in enumerate(sch_data): logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('sheet_instances'): return True
sheet_instances_index = i
break def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str:
"""
if sheet_instances_index is None: Complete workflow: inject symbol definition and create a template instance.
raise ValueError("No sheet_instances section found in schematic") Returns a template reference name.
"""
# Create template symbol instance logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}")
full_lib_id = f"{library_name}:{symbol_name}"
# Step 1: Inject symbol definition into lib_symbols
# Calculate y position based on existing templates self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
template_count = sum(1 for item in sch_data if isinstance(item, list) and len(item) > 0
and item[0] == Symbol('symbol') # Step 2: Create an offscreen template instance
and any(isinstance(p, list) and len(p) > 2 and p[0] == Symbol('property') lib_clean = library_name.replace('-', '_').replace('.', '_')
and p[1] == "Reference" and str(p[2]).startswith('_TEMPLATE') sym_clean = symbol_name.replace('-', '_').replace('.', '_')
for p in item)) template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
y_offset = -100 - (template_count * 10)
self.create_component_instance(
new_uuid = str(uuid.uuid4()) schematic_path, library_name, symbol_name,
reference=template_ref, value=symbol_name,
# Build the symbol instance S-expression x=-200, y=-200
template_instance = [ )
Symbol('symbol'),
[Symbol('lib_id'), full_lib_id], logger.info(f"Symbol loaded. Template reference: {template_ref}")
[Symbol('at'), -100, y_offset, 0], return template_ref
[Symbol('unit'), 1],
[Symbol('in_bom'), Symbol('no')], def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
[Symbol('on_board'), Symbol('no')], reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
[Symbol('dnp'), Symbol('yes')], """
[Symbol('uuid'), new_uuid], High-level: ensure symbol definition exists in schematic, then add an instance.
[Symbol('property'), "Reference", template_ref, This is the main entry point for adding components.
[Symbol('at'), -100, y_offset - 2.54, 0], """
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]] # Ensure symbol definition is in lib_symbols
], self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
[Symbol('property'), "Value", symbol_name,
[Symbol('at'), -100, y_offset + 2.54, 0], # Add the component instance
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]] return self.create_component_instance(
], schematic_path, library_name, symbol_name,
[Symbol('property'), "Footprint", "", reference=reference, value=value, x=x, y=y
[Symbol('at'), -100, y_offset, 0], )
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')]
],
[Symbol('property'), "Datasheet", "~", if __name__ == '__main__':
[Symbol('at'), -100, y_offset, 0], logging.basicConfig(level=logging.INFO)
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')] loader = DynamicSymbolLoader()
],
] print("\n=== Testing Dynamic Symbol Loader (Text-based) ===\n")
# Insert before sheet_instances print("1. Finding KiCad symbol library directories...")
sch_data.insert(sheet_instances_index, template_instance) lib_dirs = loader.find_kicad_symbol_libraries()
print(f" Found {len(lib_dirs)} directories")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f: print("\n2. Extracting symbols...")
output = sexpdata.dumps(sch_data) for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
f.write(output) block = loader.extract_symbol_from_library(lib, sym)
if block:
logger.info(f"Created template instance: {template_ref} at y={y_offset}") print(f" OK: {lib}:{sym} ({len(block)} chars)")
return template_ref else:
print(f" FAIL: {lib}:{sym}")
except Exception as e:
logger.error(f"Error creating template instance: {e}") print("\n3. Testing extends resolution...")
raise block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
if block and 'LM2596S-12' in block:
def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str: print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
""" else:
Complete workflow: inject symbol and create template instance print(f" FAIL: extends not resolved")
Args: print("\nAll tests passed!")
schematic_path: Path to .kicad_sch file
library_name: Library name (e.g., "Device")
symbol_name: Symbol name (e.g., "R")
Returns:
Template reference that can be used with kicad-skip clone()
"""
logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}")
# Step 1: Inject symbol definition into lib_symbols
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
# Step 2: Create template instance
template_ref = self.create_template_instance(schematic_path, library_name, symbol_name)
logger.info(f"Symbol loaded successfully. Template reference: {template_ref}")
return template_ref
if __name__ == '__main__':
# Test the dynamic symbol loader
logging.basicConfig(level=logging.INFO)
loader = DynamicSymbolLoader()
print("\n=== Testing Dynamic Symbol Loader ===\n")
# Test 1: Find library directories
print("1. Finding KiCad symbol library directories...")
lib_dirs = loader.find_kicad_symbol_libraries()
print(f" Found {len(lib_dirs)} directories:")
for lib_dir in lib_dirs:
print(f" - {lib_dir}")
# Test 2: Find Device library
print("\n2. Finding Device.kicad_sym library file...")
device_lib = loader.find_library_file("Device")
if device_lib:
print(f" ✓ Found: {device_lib}")
else:
print(" ✗ Not found")
exit(1)
# Test 3: Parse library file
print("\n3. Parsing Device.kicad_sym...")
parsed = loader.parse_library_file(device_lib)
print(f" ✓ Parsed successfully ({len(parsed)} top-level items)")
# Test 4: Extract specific symbols
print("\n4. Extracting symbol definitions...")
for symbol in ['R', 'C', 'LED']:
symbol_def = loader.extract_symbol_definition(device_lib, symbol)
if symbol_def:
print(f" ✓ Extracted: {symbol}")
else:
print(f" ✗ Failed: {symbol}")
print("\n✓ All basic tests passed!")

File diff suppressed because it is too large Load Diff