diff --git a/lib/error_collector_test.py b/lib/error_collector_test.py new file mode 100644 index 0000000..7111f4d --- /dev/null +++ b/lib/error_collector_test.py @@ -0,0 +1,191 @@ +"""Vollständige Tests für ErrorCollector - Alle Datentypen""" + +import sys +sys.stdout.reconfigure(encoding='utf-8') + +from error_collector import ErrorCollector + +print("="*70) +print("Test Suite: ErrorCollector - Alle Datentypen und Kombinationen") +print("="*70) + +# Test 1: Mehrere einzelne String-Fehler (create_numbers.py Szenario) +print("\nTest 1: Mehrere einzelne String-Fehler") +ec1 = ErrorCollector() +ec1.add_errors({"symbol_error": "Fehler 1"}) +ec1.add_errors({"symbol_error": "Fehler 2"}) +ec1.add_errors({"symbol_error": "Fehler 3"}) +assert ec1.errors["symbol_error"] == ["Fehler 1", "Fehler 2", "Fehler 3"] +assert isinstance(ec1.errors["symbol_error"], list) +print("✓ Strings werden zu flacher Liste zusammengefasst") + +# Test 2: Dictionary bleibt Dictionary (getpositions.py Szenario) +print("\nTest 2: Dictionary bleibt Dictionary (double_ids)") +ec2 = ErrorCollector() +double_ids = {"ID1": [[100, 200], [300, 400]], "ID2": [[500, 600]]} +ec2.add_errors({"double_ids": double_ids}) +assert isinstance(ec2.errors["double_ids"], dict) +assert ec2.errors["double_ids"]["ID1"] == [[100, 200], [300, 400]] +assert ec2.errors["double_ids"]["ID2"] == [[500, 600]] +print("✓ Dictionary bleibt Dictionary (nicht zu Liste verpackt)") + +# Test 3: Dictionary Update/Merge bei mehrmaligem Hinzufügen +print("\nTest 3: Dictionary Merge bei zweimaligem Hinzufügen") +ec3 = ErrorCollector() +ec3.add_errors({"duplicate_ids": {"ID1": [[10, 20]], "ID2": [[30, 40]]}}) +ec3.add_errors({"duplicate_ids": {"ID3": [[50, 60]]}}) +assert isinstance(ec3.errors["duplicate_ids"], dict) +assert ec3.errors["duplicate_ids"]["ID1"] == [[10, 20]] +assert ec3.errors["duplicate_ids"]["ID2"] == [[30, 40]] +assert ec3.errors["duplicate_ids"]["ID3"] == [[50, 60]] +print("✓ Dictionaries werden gemerged (nicht verschachtelt)") + +# Test 4: Liste bleibt Liste +print("\nTest 4: Liste bleibt Liste (overdefined_tunnel)") +ec4 = ErrorCollector() +ec4.add_errors({"overdefined_tunnel": ["TUNNEL1", "TUNNEL2"]}) +assert isinstance(ec4.errors["overdefined_tunnel"], list) +assert ec4.errors["overdefined_tunnel"] == ["TUNNEL1", "TUNNEL2"] +print("✓ Liste bleibt Liste") + +# Test 5: Liste Extend bei mehrmaligem Hinzufügen +print("\nTest 5: Liste Extend bei zweimaligem Hinzufügen") +ec5 = ErrorCollector() +ec5.add_errors({"tunnel_errors": ["T1", "T2"]}) +ec5.add_errors({"tunnel_errors": ["T3", "T4"]}) +assert isinstance(ec5.errors["tunnel_errors"], list) +assert ec5.errors["tunnel_errors"] == ["T1", "T2", "T3", "T4"] +print("✓ Listen werden extended (nicht verschachtelt)") + +# Test 6: Warnungen mit Dictionary (missing_attributes) +print("\nTest 6: Warnungen mit Dictionary (missing_attributes)") +ec6 = ErrorCollector() +missing_attrs = {"BX0101": "Fehlendes Attribut A", "BX0102": "Fehlendes Attribut B"} +ec6.add_warnings({"missing_attributes": missing_attrs}) +assert isinstance(ec6.warnings["missing_attributes"], dict) +assert ec6.warnings["missing_attributes"]["BX0101"] == "Fehlendes Attribut A" +print("✓ Warnung Dictionary bleibt Dictionary") + +# Test 7: Warnungen Dictionary Merge +print("\nTest 7: Warnungen Dictionary Merge") +ec7 = ErrorCollector() +ec7.add_warnings({"missing_attributes": {"ID1": "Error 1"}}) +ec7.add_warnings({"missing_attributes": {"ID2": "Error 2"}}) +assert isinstance(ec7.warnings["missing_attributes"], dict) +assert ec7.warnings["missing_attributes"]["ID1"] == "Error 1" +assert ec7.warnings["missing_attributes"]["ID2"] == "Error 2" +print("✓ Warnungs-Dictionaries werden gemerged") + +# Test 8: Mixed - mehrere Keys mit verschiedenen Typen +print("\nTest 8: Mixed - verschiedene Typen gleichzeitig") +ec8 = ErrorCollector() +ec8.add_errors({ + "string_errors": "Error 1", + "dict_errors": {"ID1": [[1, 2]]}, + "list_errors": ["Item1", "Item2"] +}) +ec8.add_errors({ + "string_errors": "Error 2", + "dict_errors": {"ID2": [[3, 4]]}, + "list_errors": ["Item3"] +}) +assert ec8.errors["string_errors"] == ["Error 1", "Error 2"] +assert ec8.errors["dict_errors"] == {"ID1": [[1, 2]], "ID2": [[3, 4]]} +assert ec8.errors["list_errors"] == ["Item1", "Item2", "Item3"] +print("✓ Verschiedene Typen werden korrekt behandelt") + +# Test 9: Reales getpositions.py Szenario +print("\nTest 9: Reales getpositions.py Szenario") +ec9 = ErrorCollector() +# Wie in getpositions.py +double_ids = {} +double_ids["MA-1001"] = [[100, 200], [300, 400]] +double_ids["MA-1002"] = [[500, 600]] +ec9.add_errors({"double_ids": double_ids}) + +missing_attribs = {} +missing_attribs["BX0101"] = "Nur ein Block und/oder fehlende Attribute: dict_keys(['A', 'B'])" +ec9.add_warnings({"missing_attributes": missing_attribs}) + +assert isinstance(ec9.errors["double_ids"], dict) +assert isinstance(ec9.warnings["missing_attributes"], dict) +# Verifiziere dass es für .items() funktioniert +for id_name, positions in ec9.errors["double_ids"].items(): + assert isinstance(id_name, str) + assert isinstance(positions, list) +print("✓ getpositions.py Format funktioniert für ioconverter.py") + +# Test 10: Reales create_numbers.py Szenario +print("\nTest 10: Reales create_numbers.py Szenario") +ec10 = ErrorCollector() +# Mehrmals einzelne Strings mit gleichem Key +ec10.add_errors({"symbol_with_at_not_in_boundary": "Symbol at (10, 20) not in boundary"}) +ec10.add_errors({"symbol_with_at_not_in_boundary": "Symbol at (30, 40) not in boundary"}) +ec10.add_errors({"symbol_with_at_not_in_boundary": "Symbol at (50, 60) not in boundary"}) + +# Liste mit mehreren Elementen +ec10.add_errors({"no_symbols_found": [ + "Renamer 'BG-1@@@' hat keine Symbole gefunden", + "Renamer 'BG-2@@@' hat keine Symbole gefunden" +]}) + +assert isinstance(ec10.errors["symbol_with_at_not_in_boundary"], list) +assert len(ec10.errors["symbol_with_at_not_in_boundary"]) == 3 +assert isinstance(ec10.errors["no_symbols_found"], list) +assert len(ec10.errors["no_symbols_found"]) == 2 + +# Prüfe auf Verschachtelung +for item in ec10.errors["symbol_with_at_not_in_boundary"]: + assert isinstance(item, str), f"Verschachtelung gefunden: {type(item)}" +print("✓ create_numbers.py Format - keine Verschachtelung") + +# Test 11: Realistisches Szenario mit vielen Fehleraufrufen (wie test_real_scenario.py) +print("\nTest 11: Realistisches Szenario mit vielen einzelnen Fehleraufrufen") +ec11 = ErrorCollector() + +# Simuliere "no_symbols_found" Fehler als Liste +ec11.add_errors({ + "no_symbols_found": [ + "Renamer-Block-Gruppe 'BG-1@@@' (DIRECTION: LEFT_RIGHT) hat keine passenden Symbole gefunden. Gesucht auf Layer: ILS_Eingang, ILS_Ausgang. Renamer-Blöcke an Positionen: (300.0, 500.0), (3000.0, 2000.0)", + "Renamer-Block-Gruppe 'BG-2@@@' (DIRECTION: LEFT_RIGHT) hat keine passenden Symbole gefunden. Gesucht auf Layer: ILS_Eingang, ILS_Ausgang. Renamer-Blöcke an Positionen: (3200.0, 500.0)" + ] +}) + +# Simuliere viele einzelne "symbol_with_at_not_in_boundary" Fehler +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (95.5, 509.5) mit IO='BG-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (2895.5, 2159.5) mit IO='BG-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (4195.5, 2209.5) mit IO='BG-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (5495.5, 2109.5) mit IO='BG-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (2895.5, 2609.5) mit IO='MB-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (4195.5, 2659.5) mit IO='MB-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (5495.5, 2559.5) mit IO='MB-1@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (3095.5, 509.5) mit IO='BG-2@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (4395.5, 509.5) mit IO='BG-2@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) +ec11.add_errors({"symbol_with_at_not_in_boundary": "Symbol an Position (5695.5, 509.5) mit IO='BG-2@@' auf Layer 'ILS_MOTOR' enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich."}) + +# Verifiziere Struktur +assert isinstance(ec11.errors["symbol_with_at_not_in_boundary"], list) +assert len(ec11.errors["symbol_with_at_not_in_boundary"]) == 10 +assert isinstance(ec11.errors["no_symbols_found"], list) +assert len(ec11.errors["no_symbols_found"]) == 2 + +# Überprüfe, dass KEINE Verschachtelung vorliegt +symbol_errors = ec11.errors["symbol_with_at_not_in_boundary"] +all_strings = all(isinstance(item, str) for item in symbol_errors) +no_nested_lists = not any(isinstance(item, list) for item in symbol_errors) +assert all_strings, "Nicht alle Einträge sind Strings!" +assert no_nested_lists, "Verschachtelte Listen gefunden!" + +print("✓ Realistisches Szenario mit 10 einzelnen Fehleraufrufen - keine Verschachtelung") + +print("\n" + "="*70) +print("✓✓✓ ALLE TESTS BESTANDEN ✓✓✓") +print("="*70) +print("\nZusammenfassung:") +print(" - Strings → Listen (flach, keine Verschachtelung)") +print(" - Dictionaries → Dictionaries (gemerged bei mehrmaligem Hinzufügen)") +print(" - Listen → Listen (extended bei mehrmaligem Hinzufügen)") +print(" - Mixed Types werden korrekt behandelt") +print(" - Kompatibel mit getpositions.py und create_numbers.py") +print(" - Kompatibel mit ioconverter.py Erwartungen") +print(" - Realistisches Szenario mit vielen einzelnen Aufrufen funktioniert") \ No newline at end of file diff --git a/lib/flags_test.py b/lib/flags_test.py new file mode 100644 index 0000000..e55a53d --- /dev/null +++ b/lib/flags_test.py @@ -0,0 +1,17 @@ +""" +Test-Skript zum Generieren der Flaggen-Icons. +Erstellt Icons für verschiedene Sprachen basierend auf dxfCs.png. +""" +import flags + +# Generiere alle Flaggen-Icons +icons = [] +icons.append(flags.add_flag(flags.flag_en, "dxfen")) +icons.append(flags.add_flag(flags.flag_fr, "dxffr")) +icons.append(flags.add_flag(flags.flag_it, "dxfit")) +icons.append(flags.add_flag(flags.flag_es, "dxfes")) + +print("Generierte Icons:") +for icon in icons: + print(f" - {icon}") +print(f"\nAlle Icons wurden in {flags.ICON_DIR} gespeichert.") diff --git a/lib/getpositions_test.py b/lib/getpositions_test.py new file mode 100644 index 0000000..ac3d02e --- /dev/null +++ b/lib/getpositions_test.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test für die check_rack_z_coordinates Funktion +""" + +import sys +from pathlib import Path + +# Füge lib zum Path hinzu +sys.path.insert(0, str(Path(__file__).parent)) + +from getpositions import check_rack_z_coordinates +from errorhandler import ErrorCollector + +def test_no_deviation(): + """Test mit Racks ohne starke Abweichung (< 2000mm)""" + print("Test 1: Keine starke Abweichung (< 2000mm)") + + racks = { + 'Rack_1': [[100.0, 200.0, 0.0], [150.0, 200.0, 0.0]], + 'Rack_2': [[200.0, 300.0, 500.0], [250.0, 300.0, 500.0]], + 'Rack_3': [[300.0, 400.0, 1000.0], [350.0, 400.0, 1000.0]] + } + + error_collector = ErrorCollector() + check_rack_z_coordinates(racks, error_collector) + + warnings = error_collector.warnings + if warnings: + print(f" FEHLER: Warnung wurde ausgegeben, obwohl keine erwartet: {warnings}") + else: + print(" OK: Keine Warnung (erwartet)") + print() + +def test_with_deviation(): + """Test mit Racks mit starker Abweichung (> 2000mm)""" + print("Test 2: Starke Abweichung (> 2000mm)") + + racks = { + 'Rack_1': [[100.0, 200.0, 0.0], [150.0, 200.0, 0.0]], + 'Rack_2': [[200.0, 300.0, 50000.0], [250.0, 300.0, 50000.0]], # 50m Höhe! + 'Rack_3': [[300.0, 400.0, 100.0], [350.0, 400.0, 100.0]] + } + + error_collector = ErrorCollector() + check_rack_z_coordinates(racks, error_collector) + + warnings = error_collector.warnings + if warnings: + print(f" OK: Warnung wurde ausgegeben (erwartet): {warnings}") + else: + print(" FEHLER: Keine Warnung, aber eine war erwartet!") + print() + +def test_exactly_2000mm(): + """Test mit genau 2000mm Abweichung (Grenzfall)""" + print("Test 3: Genau 2000mm Abweichung (Grenzfall)") + + racks = { + 'Rack_1': [[100.0, 200.0, 0.0], [150.0, 200.0, 0.0]], + 'Rack_2': [[200.0, 300.0, 2000.0], [250.0, 300.0, 2000.0]], + } + + error_collector = ErrorCollector() + check_rack_z_coordinates(racks, error_collector) + + warnings = error_collector.warnings + if warnings: + print(f" Warnung wurde ausgegeben: {warnings}") + else: + print(" Keine Warnung (Grenzfall, hängt von > oder >= ab)") + print() + +def test_dict_format(): + """Test mit Dictionary-Format für Koordinaten""" + print("Test 4: Dictionary-Format für Koordinaten") + + racks = { + 'Rack_1': [{'x': 100.0, 'y': 200.0, 'z': 0.0}, {'x': 150.0, 'y': 200.0, 'z': 0.0}], + 'Rack_2': [{'x': 200.0, 'y': 300.0, 'z': 50000.0}, {'x': 250.0, 'y': 300.0, 'z': 50000.0}], + } + + error_collector = ErrorCollector() + check_rack_z_coordinates(racks, error_collector) + + warnings = error_collector.warnings + if warnings: + print(f" OK: Warnung wurde ausgegeben (erwartet): {warnings}") + else: + print(" FEHLER: Keine Warnung, aber eine war erwartet!") + print() + +if __name__ == '__main__': + print("="*80) + print("Tests für check_rack_z_coordinates") + print("="*80) + print() + + test_no_deviation() + test_with_deviation() + test_exactly_2000mm() + test_dict_format() + + print("="*80) + print("Tests abgeschlossen") + print("="*80) diff --git a/lib/utils_test.py b/lib/utils_test.py new file mode 100644 index 0000000..0a7bff3 --- /dev/null +++ b/lib/utils_test.py @@ -0,0 +1,316 @@ +""" +Unit tests for ezdxf helper functions in utils.py + +Tests cover: +- Attribute extraction from INSERT blocks +- Layer creation and management +- Geometry creation (rectangles, text) +- Block reference operations +- Entity querying +""" + +import pytest +import ezdxf +from pathlib import Path +import sys + +# Add lib directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / 'lib')) + +from utils import ( + extract_insert_attributes, + extract_insert_attributes_with_doc, + extract_attributes_with_positions, + ensure_layer_exists, + add_rectangle_lwpolyline, + add_text_with_alignment, + add_blockref_with_attributes, + query_entities_by_layer, + create_layers_from_config +) + + +@pytest.fixture +def doc(): + """Create a new DXF document for testing""" + return ezdxf.new('R2010') + + +@pytest.fixture +def msp(doc): + """Get modelspace from document""" + return doc.modelspace() + + +class TestAttributeExtraction: + """Tests for attribute extraction functions""" + + def test_extract_insert_attributes_with_doc_simple(self, doc, msp): + """Test extracting attributes from a simple block with direct attributes""" + # Create block with attributes + block = doc.blocks.new('TEST_BLOCK') + block.add_attdef('IO', (0, 0), dxfattribs={'height': 2.5}) + block.add_attdef('KENNZEICHNUNG', (0, 5), dxfattribs={'height': 2.5}) + + # Insert block and set attributes + insert = msp.add_blockref('TEST_BLOCK', (10, 10)) + insert.add_auto_attribs({'IO': 'MA0001', 'KENNZEICHNUNG': '=A01+UH00'}) + + # Extract attributes + attrs = extract_insert_attributes_with_doc(doc, insert) + + assert attrs['IO'] == 'MA0001' + assert attrs['KENNZEICHNUNG'] == '=A01+UH00' + + def test_extract_insert_attributes_empty_block(self, doc, msp): + """Test extracting attributes from block without attributes""" + block = doc.blocks.new('EMPTY_BLOCK') + insert = msp.add_blockref('EMPTY_BLOCK', (10, 10)) + + attrs = extract_insert_attributes_with_doc(doc, insert) + + assert attrs == {} + + def test_extract_insert_attributes_non_insert(self, doc, msp): + """Test that non-INSERT entities return empty dict""" + line = msp.add_line((0, 0), (10, 10)) + + attrs = extract_insert_attributes_with_doc(doc, line) + + assert attrs == {} + + def test_extract_attributes_with_positions(self, doc, msp): + """Test extracting attributes with position information""" + # Create block with attributes + block = doc.blocks.new('POS_TEST') + block.add_attdef('IO', (0, 0), dxfattribs={'height': 2.5}) + block.add_attdef('NAME', (0, 5), dxfattribs={'height': 2.5}) + + # Insert blocks + insert1 = msp.add_blockref('POS_TEST', (100, 200)) + insert1.add_auto_attribs({'IO': 'MA0001', 'NAME': 'Motor1'}) + + insert2 = msp.add_blockref('POS_TEST', (300, 400)) + insert2.add_auto_attribs({'IO': 'MA0002', 'NAME': 'Motor2'}) + + # Extract with positions + all_inserts, all_positions = extract_attributes_with_positions(msp) + + assert len(all_inserts) == 2 + assert len(all_positions) == 2 + assert all_inserts[0]['IO'] == 'MA0001' + assert all_inserts[1]['IO'] == 'MA0002' + + +class TestLayerOperations: + """Tests for layer creation and management functions""" + + def test_ensure_layer_exists_new_layer(self, doc): + """Test creating a new layer""" + layer = ensure_layer_exists(doc, 'TEST_LAYER', color=5) + + assert 'TEST_LAYER' in doc.layers + assert doc.layers.get('TEST_LAYER').color == 5 + + def test_ensure_layer_exists_existing_layer(self, doc): + """Test that existing layer is returned unchanged""" + # Create layer first + doc.layers.add('EXISTING', color=1) + + # Ensure it exists (should not error) + layer = ensure_layer_exists(doc, 'EXISTING', color=3) + + assert 'EXISTING' in doc.layers + # Color should be updated + assert doc.layers.get('EXISTING').color == 3 + + def test_ensure_layer_with_all_options(self, doc): + """Test creating layer with all options""" + layer = ensure_layer_exists( + doc, 'FULL_LAYER', + color=5, + linetype='DASHED', + lineweight=25, + description='Test layer description' + ) + + assert 'FULL_LAYER' in doc.layers + layer_obj = doc.layers.get('FULL_LAYER') + assert layer_obj.color == 5 + assert layer_obj.dxf.linetype == 'DASHED' + assert layer_obj.dxf.lineweight == 25 + + def test_create_layers_from_config(self, doc): + """Test creating multiple layers from configuration""" + layer_config = { + 'ILS_MOTOR': {'color': 1, 'description': 'Motor layer'}, + 'ILS_SENSOR': {'color': 3, 'lineweight': 25}, + 'ILS_CABLE': {'color': 5} + } + + create_layers_from_config(doc, layer_config) + + assert 'ILS_MOTOR' in doc.layers + assert 'ILS_SENSOR' in doc.layers + assert 'ILS_CABLE' in doc.layers + assert doc.layers.get('ILS_MOTOR').color == 1 + assert doc.layers.get('ILS_SENSOR').color == 3 + assert doc.layers.get('ILS_CABLE').color == 5 + + +class TestGeometryCreation: + """Tests for geometry creation functions""" + + def test_add_rectangle_lwpolyline_simple(self, doc, msp): + """Test adding a simple rectangle""" + rect = add_rectangle_lwpolyline(msp, 0, 0, 100, 50) + + assert rect.dxftype() == 'LWPOLYLINE' + assert rect.closed is True + assert len(list(rect.get_points())) == 4 + + def test_add_rectangle_with_color(self, doc, msp): + """Test adding rectangle with color""" + rect = add_rectangle_lwpolyline(msp, 10, 10, 200, 100, color=5) + + assert rect.dxf.color == 5 + + def test_add_rectangle_with_rgb(self, doc, msp): + """Test adding rectangle with RGB color""" + rect = add_rectangle_lwpolyline(msp, 20, 20, 150, 75, rgb=(255, 0, 0)) + + assert rect.rgb == (255, 0, 0) + + def test_add_rectangle_on_layer(self, doc, msp): + """Test adding rectangle on specific layer""" + ensure_layer_exists(doc, 'TEST_LAYER') + rect = add_rectangle_lwpolyline(msp, 0, 0, 100, 50, layer='TEST_LAYER') + + assert rect.dxf.layer == 'TEST_LAYER' + + def test_add_text_with_alignment_simple(self, doc, msp): + """Test adding simple text""" + text = add_text_with_alignment(msp, "Test", 100, 100) + + assert text.dxftype() == 'TEXT' + assert text.dxf.text == "Test" + + def test_add_text_with_rotation(self, doc, msp): + """Test adding text with rotation""" + text = add_text_with_alignment(msp, "Rotated", 200, 200, rotation=45.0) + + assert text.dxf.rotation == 45.0 + + def test_add_text_with_color_and_height(self, doc, msp): + """Test adding text with color and height""" + text = add_text_with_alignment(msp, "Styled", 300, 300, height=75, color=3) + + assert text.dxf.height == 75 + assert text.dxf.color == 3 + + +class TestBlockOperations: + """Tests for block reference operations""" + + def test_add_blockref_without_attributes(self, doc, msp): + """Test adding block reference without attributes""" + # Create a simple block + block = doc.blocks.new('SIMPLE_BLOCK') + block.add_line((0, 0), (10, 10)) + + # Add block reference + insert = add_blockref_with_attributes(msp, 'SIMPLE_BLOCK', (100, 100)) + + assert insert.dxftype() == 'INSERT' + assert insert.dxf.name == 'SIMPLE_BLOCK' + + def test_add_blockref_with_attributes(self, doc, msp): + """Test adding block reference with attributes""" + # Create block with attribute definitions + block = doc.blocks.new('ATTR_BLOCK') + block.add_attdef('IO', (0, 0)) + block.add_attdef('NAME', (0, 5)) + + # Add block reference with attributes + attrs = {'IO': 'MA0001', 'NAME': 'Motor'} + insert = add_blockref_with_attributes( + msp, 'ATTR_BLOCK', (200, 200), attributes=attrs + ) + + assert insert.dxf.name == 'ATTR_BLOCK' + # Check attributes were set + attrib_values = {a.dxf.tag: a.dxf.text for a in insert.attribs} + assert attrib_values['IO'] == 'MA0001' + assert attrib_values['NAME'] == 'Motor' + + def test_add_blockref_with_scale_and_rotation(self, doc, msp): + """Test adding block reference with scale and rotation""" + block = doc.blocks.new('TRANSFORM_BLOCK') + block.add_line((0, 0), (10, 10)) + + insert = add_blockref_with_attributes( + msp, 'TRANSFORM_BLOCK', (300, 300), + scale=0.5, rotation=90.0 + ) + + assert insert.dxf.xscale == 0.5 + assert insert.dxf.yscale == 0.5 + assert insert.dxf.rotation == 90.0 + + +class TestEntityQuerying: + """Tests for entity querying functions""" + + def test_query_entities_by_layer_single_layer(self, doc, msp): + """Test querying entities on a single layer""" + ensure_layer_exists(doc, 'MOTOR') + ensure_layer_exists(doc, 'SENSOR') + + # Add entities on different layers + msp.add_line((0, 0), (10, 10), dxfattribs={'layer': 'MOTOR'}) + msp.add_line((0, 0), (20, 20), dxfattribs={'layer': 'SENSOR'}) + msp.add_line((0, 0), (30, 30), dxfattribs={'layer': 'MOTOR'}) + + # Query only MOTOR layer + motor_entities = query_entities_by_layer(msp, 'LINE', layers=['MOTOR']) + + assert len(motor_entities) == 2 + assert all(e.dxf.layer == 'MOTOR' for e in motor_entities) + + def test_query_entities_by_type(self, doc, msp): + """Test querying entities by type""" + msp.add_line((0, 0), (10, 10)) + msp.add_circle((0, 0), 5) + msp.add_line((0, 0), (20, 20)) + + lines = query_entities_by_layer(msp, 'LINE') + + assert len(lines) == 2 + assert all(e.dxftype() == 'LINE' for e in lines) + + def test_query_entities_exclude_layers(self, doc, msp): + """Test querying entities excluding certain layers""" + msp.add_line((0, 0), (10, 10), dxfattribs={'layer': '0'}) + msp.add_line((0, 0), (20, 20), dxfattribs={'layer': 'ILS'}) + msp.add_line((0, 0), (30, 30), dxfattribs={'layer': '0'}) + + # Exclude layer '0' + entities = query_entities_by_layer(msp, 'LINE', exclude_layers=['0']) + + assert len(entities) == 1 + assert entities[0].dxf.layer == 'ILS' + + def test_query_entities_multiple_layers(self, doc, msp): + """Test querying entities from multiple layers""" + msp.add_line((0, 0), (10, 10), dxfattribs={'layer': 'L1'}) + msp.add_line((0, 0), (20, 20), dxfattribs={'layer': 'L2'}) + msp.add_line((0, 0), (30, 30), dxfattribs={'layer': 'L3'}) + + entities = query_entities_by_layer(msp, 'LINE', layers=['L1', 'L2']) + + assert len(entities) == 2 + assert set(e.dxf.layer for e in entities) == {'L1', 'L2'} + + +if __name__ == '__main__': + pytest.main([__file__, '-v'])