KiCad PCB EDA Suite
Loading...
Searching...
No Matches
eeschema/test_easyedapro_v3_import.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
28
29#include <boost/test/unit_test.hpp>
30
31#include <array>
32#include <set>
33
34#include <qa_utils/file_utils.h>
36
39
41
42#include <pgm_base.h>
43#include <schematic.h>
44#include <sch_io/sch_io.h>
45#include <sch_io/sch_io_mgr.h>
46#include <sch_label.h>
47#include <sch_line.h>
48#include <sch_pin.h>
49#include <sch_screen.h>
50#include <sch_symbol.h>
51#include <sch_text.h>
52
54
55#include <wx/filefn.h>
56#include <wx/filename.h>
57#include <wx/wfstream.h>
58#include <wx/zipstrm.h>
59
60
61namespace
62{
63
64static wxString getEasyEdaProV3ArchivePath()
65{
66 return wxString::FromUTF8( KI_TEST::GetTestDataRootDir()
67 + "pcbnew/plugins/easyedapro/ProProject_LS2K0300Core_2025-11-14.epro2" );
68}
69
70
71static void writeZipEntry( wxZipOutputStream& aZip, const wxString& aName, const std::string& aContent )
72{
73 aZip.PutNextEntry( aName );
74 aZip.Write( aContent.data(), aContent.size() );
75 aZip.CloseEntry();
76}
77
78
79static wxString makeTempElibz2Path( const wxString& aPrefix )
80{
81 wxString tempPath = wxFileName::CreateTempFileName( aPrefix );
82 wxRemoveFile( tempPath );
83
84 wxFileName fn( tempPath );
85 fn.SetExt( wxS( "elibz2" ) );
86
87 return fn.GetFullPath();
88}
89
90
91static void writeV3LibraryArchive( const wxString& aPath, const std::string& aSymbolIndex,
92 const std::string& aFootprintIndex, const std::string& aElibu )
93{
94 wxFFileOutputStream out( aPath );
95 BOOST_REQUIRE( out.IsOk() );
96
97 wxZipOutputStream zip( out, -1, wxConvUTF8 );
98
99 if( !aSymbolIndex.empty() )
100 writeZipEntry( zip, wxS( "symbol2.json" ), aSymbolIndex );
101
102 if( !aFootprintIndex.empty() )
103 writeZipEntry( zip, wxS( "footprint2.json" ), aFootprintIndex );
104
105 writeZipEntry( zip, wxS( "library.elibu" ), aElibu );
106}
107
108} // namespace
109
110
111BOOST_AUTO_TEST_SUITE( EasyEdaProV3Import )
112
113
114BOOST_AUTO_TEST_CASE( LibraryIndexMergeKeepsSymbolAndFootprintNames )
115{
116 wxString archivePath = makeTempElibz2Path( wxS( "easyedapro_v3_mixed" ) );
117
118 const std::string symbolIndex = R"({
119 "devices": {},
120 "symbols": {
121 "sym_uuid": { "uuid": "sym_uuid", "display_title": "SymbolName" }
122 },
123 "footprints": {},
124 "panelLibs": {}
125 })";
126
127 const std::string footprintIndex = R"({
128 "devices": {},
129 "symbols": {},
130 "footprints": {
131 "fp_uuid": { "uuid": "fp_uuid", "display_title": "FootprintName" }
132 },
133 "panelLibs": {}
134 })";
135
136 const std::string elibu = "{\"type\":\"DOCHEAD\"}||{\"docType\":\"SYMBOL\",\"uuid\":\"sym_uuid\"}|\n"
137 "{\"type\":\"DOCHEAD\"}||{\"docType\":\"FOOTPRINT\",\"uuid\":\"fp_uuid\"}|\n";
138
139 writeV3LibraryArchive( archivePath, symbolIndex, footprintIndex, elibu );
140
141 EASYEDAPRO::V3_DOC_PARSER parser( archivePath );
142 BOOST_REQUIRE_NO_THROW( parser.LoadLibrary() );
143
144 std::map<wxString, wxString> symbols = EASYEDAPRO::BuildV3LibraryItemMap( parser, "symbols", wxS( "SYMBOL" ) );
145 std::map<wxString, wxString> footprints =
146 EASYEDAPRO::BuildV3LibraryItemMap( parser, "footprints", wxS( "FOOTPRINT" ) );
147
148 BOOST_CHECK_EQUAL( symbols.at( wxS( "SymbolName" ) ), wxString( wxS( "sym_uuid" ) ) );
149 BOOST_CHECK_EQUAL( footprints.at( wxS( "FootprintName" ) ), wxString( wxS( "fp_uuid" ) ) );
150
151 BOOST_CHECK( wxRemoveFile( archivePath ) );
152}
153
154
155BOOST_AUTO_TEST_CASE( LibraryItemDuplicateNamesGetNumericSuffix )
156{
157 wxString archivePath = makeTempElibz2Path( wxS( "easyedapro_v3_dupes" ) );
158
159 const std::string symbolIndex = R"({
160 "devices": {},
161 "symbols": {
162 "aaaaaaaa0001": { "uuid": "aaaaaaaa0001", "display_title": "DUP" },
163 "aaaaaaaa0002": { "uuid": "aaaaaaaa0002", "display_title": "DUP" },
164 "aaaaaaaa0003": { "uuid": "aaaaaaaa0003", "display_title": "DUP" }
165 },
166 "footprints": {},
167 "panelLibs": {}
168 })";
169
170 const std::string elibu = "{\"type\":\"DOCHEAD\"}||{\"docType\":\"SYMBOL\",\"uuid\":\"aaaaaaaa0001\"}|\n"
171 "{\"type\":\"DOCHEAD\"}||{\"docType\":\"SYMBOL\",\"uuid\":\"aaaaaaaa0002\"}|\n"
172 "{\"type\":\"DOCHEAD\"}||{\"docType\":\"SYMBOL\",\"uuid\":\"aaaaaaaa0003\"}|\n";
173
174 writeV3LibraryArchive( archivePath, symbolIndex, std::string(), elibu );
175
176 EASYEDAPRO::V3_DOC_PARSER parser( archivePath );
177 BOOST_REQUIRE_NO_THROW( parser.LoadLibrary() );
178
179 std::map<wxString, wxString> symbols = EASYEDAPRO::BuildV3LibraryItemMap( parser, "symbols", wxS( "SYMBOL" ) );
180
181 BOOST_REQUIRE_EQUAL( symbols.size(), 3 );
182 BOOST_CHECK( symbols.contains( wxS( "DUP" ) ) );
183 BOOST_CHECK( symbols.contains( wxS( "DUP_2" ) ) );
184 BOOST_CHECK( symbols.contains( wxS( "DUP_3" ) ) );
185
186 BOOST_CHECK( wxRemoveFile( archivePath ) );
187}
188
189
190BOOST_AUTO_TEST_CASE( RawDocNullTombstonesRemovedAfterDeduplication )
191{
192 wxString archivePath = makeTempElibz2Path( wxS( "easyedapro_v3_tombstone" ) );
193
194 const std::string footprintIndex = R"({
195 "devices": {},
196 "symbols": {},
197 "footprints": {},
198 "panelLibs": {}
199 })";
200
201 // p1: live row superseded by a newer null tombstone (must disappear).
202 // p2: null tombstone superseded by a newer live row (must survive).
203 // p3: plain live row (must survive).
204 const std::string elibu =
205 "{\"type\":\"DOCHEAD\"}||{\"docType\":\"FOOTPRINT\",\"uuid\":\"fp_tombstone\"}|\n"
206 "{\"type\":\"POLY\",\"id\":\"p1\",\"ticket\":1}||{\"path\":[[0,0],[10,10]],\"width\":1}|\n"
207 "{\"type\":\"POLY\",\"id\":\"p1\",\"ticket\":2}|||\n"
208 "{\"type\":\"POLY\",\"id\":\"p2\",\"ticket\":1}|||\n"
209 "{\"type\":\"POLY\",\"id\":\"p2\",\"ticket\":2}||{\"path\":[[0,0],[10,10]],\"width\":1}|\n"
210 "{\"type\":\"FILL\",\"id\":\"p3\",\"ticket\":1}||{\"path\":[[0,0],[10,10]]}|\n";
211
212 writeV3LibraryArchive( archivePath, std::string(), footprintIndex, elibu );
213
214 EASYEDAPRO::V3_DOC_PARSER parser( archivePath );
215 BOOST_REQUIRE_NO_THROW( parser.LoadLibrary() );
216
217 const EASYEDAPRO::V3_DOC_RAW* doc = parser.FindRawDoc( wxS( "FOOTPRINT" ), wxS( "fp_tombstone" ) );
218 BOOST_REQUIRE( doc );
219
220 // Every surviving row carries a non-null inner payload: null tombstones are dropped.
221 for( const EASYEDAPRO::V3_ROW& row : doc->rows )
222 BOOST_CHECK( !row.inner.is_null() );
223
224 // p1 was superseded by a newer null tombstone and must be gone.
225 BOOST_CHECK( !doc->rowById.contains( wxS( "p1" ) ) );
226
227 // p2 and p3 must survive.
228 BOOST_REQUIRE( doc->rowById.contains( wxS( "p2" ) ) );
229 BOOST_REQUIRE( doc->rowById.contains( wxS( "p3" ) ) );
230 BOOST_CHECK_EQUAL( doc->rows.size(), 2 );
231
232 // rowById must index every surviving row at its compacted position.
233 for( const auto& [id, index] : doc->rowById )
234 {
235 BOOST_REQUIRE( index < doc->rows.size() );
236 BOOST_CHECK_EQUAL( doc->rows[index].id, id );
237 }
238
239 BOOST_CHECK( wxRemoveFile( archivePath ) );
240}
241
242
243BOOST_AUTO_TEST_CASE( BuildProjectIndexHasSchematicSheets )
244{
246 BOOST_REQUIRE_NO_THROW( v3.Load() );
247
249
250 BOOST_REQUIRE( project.contains( "schematics" ) );
251 BOOST_REQUIRE( project.at( "schematics" ).is_object() );
252 BOOST_REQUIRE( !project.at( "schematics" ).empty() );
253
254 bool hasAtLeastOneSheet = false;
255
256 for( const auto& [schUuid, sch] : project.at( "schematics" ).items() )
257 {
258 if( sch.contains( "sheets" ) && sch.at( "sheets" ).is_array()
259 && !sch.at( "sheets" ).empty() )
260 {
261 hasAtLeastOneSheet = true;
262 break;
263 }
264 }
265
266 BOOST_CHECK( hasAtLeastOneSheet );
267}
268
269
270BOOST_AUTO_TEST_CASE( RawSchematicPageContainsTextWireAndNetAttr )
271{
273 BOOST_REQUIRE_NO_THROW( v3.Load() );
274
275 const auto& pages = v3.GetRawDocs( wxS( "SCH_PAGE" ) );
276 BOOST_REQUIRE( !pages.empty() );
277
278 bool hasTextValue = false;
279 bool hasLineStartEnd = false;
280 bool hasWireNetAttr = false;
281
282 for( const auto& [pageUuid, pageDoc] : pages )
283 {
284 for( const EASYEDAPRO::V3_ROW& row : pageDoc.rows )
285 {
286 if( row.type == wxS( "TEXT" ) && row.inner.contains( "value" ) )
287 hasTextValue = true;
288
289 if( row.type == wxS( "LINE" )
290 && row.inner.contains( "startX" ) && row.inner.contains( "startY" )
291 && row.inner.contains( "endX" ) && row.inner.contains( "endY" ) )
292 {
293 hasLineStartEnd = true;
294 }
295
296 if( row.type == wxS( "ATTR" )
297 && EASYEDAPRO::V3GetString( row.inner, "key" ) == wxS( "NET" ) )
298 {
299 hasWireNetAttr = true;
300 }
301 }
302 }
303
304 BOOST_CHECK( hasTextValue );
305 BOOST_CHECK( hasLineStartEnd );
306 BOOST_CHECK( hasWireNetAttr );
307}
308
309
310BOOST_AUTO_TEST_CASE( RawSymbolContainsBlobObjectReference )
311{
313 BOOST_REQUIRE_NO_THROW( v3.Load() );
314
315 const auto& symbols = v3.GetRawDocs( wxS( "SYMBOL" ) );
316 BOOST_REQUIRE( !symbols.empty() );
317
318 bool hasBlobObject = false;
319
320 for( const auto& [symbolUuid, symbolDoc] : symbols )
321 {
322 for( const EASYEDAPRO::V3_ROW& row : symbolDoc.rows )
323 {
324 if( row.type != wxS( "OBJ" ) )
325 continue;
326
327 wxString content = EASYEDAPRO::V3GetString( row.inner, "content" );
328
329 if( content.StartsWith( wxS( "blob:" ) ) )
330 {
331 hasBlobObject = true;
332 break;
333 }
334 }
335
336 if( hasBlobObject )
337 break;
338 }
339
340 BOOST_CHECK( hasBlobObject );
341}
342
343
344BOOST_AUTO_TEST_CASE( RawSymbolPartUnitsAndPinOrientationAreStable )
345{
347 BOOST_REQUIRE_NO_THROW( v3.Load() );
348
350 std::map<wxString, EASYEDAPRO::PRJ_DEVICE> devices = project.at( "devices" );
351
352 auto findDevice = [&]( const wxString& aTitle ) -> const EASYEDAPRO::PRJ_DEVICE*
353 {
354 for( const auto& [devUuid, dev] : devices )
355 {
356 if( dev.title == aTitle )
357 return &dev;
358 }
359
360 return nullptr;
361 };
362
363 const EASYEDAPRO::PRJ_DEVICE* ls2kDevice = findDevice( wxS( "LS2K0300" ) );
364 BOOST_REQUIRE( ls2kDevice );
365
366 const EASYEDAPRO::PRJ_DEVICE* sgmDevice = findDevice( wxS( "SGM61032BXKB6G/TR" ) );
367 BOOST_REQUIRE( sgmDevice );
368
369 SCH_EASYEDAPRO_V3_PARSER parser( nullptr, nullptr );
370
371 wxString ls2kSymbolUuid = ls2kDevice->attributes.at( wxS( "Symbol" ) );
372 const EASYEDAPRO::V3_DOC_RAW* ls2kRaw = v3.FindRawDoc( wxS( "SYMBOL" ), ls2kSymbolUuid );
373 BOOST_REQUIRE( ls2kRaw );
374
375 EASYEDAPRO::SYM_INFO ls2kInfo = parser.ParseSymbol( *ls2kRaw, ls2kDevice->attributes );
376 BOOST_REQUIRE( ls2kInfo.libSymbol );
377 BOOST_CHECK_EQUAL( ls2kInfo.libSymbol->GetUnitCount(), 5 );
378
379 std::array<int, 6> unitPinCount = {};
380
382 {
383 int unit = pin->GetUnit();
384
385 if( unit >= 1 && unit <= 5 )
386 unitPinCount[unit]++;
387 }
388
389 for( int unit = 1; unit <= 5; ++unit )
390 BOOST_CHECK_MESSAGE( unitPinCount[unit] > 0, "LS2K0300 should have pins in each unit" );
391
392 wxString sgmSymbolUuid = sgmDevice->attributes.at( wxS( "Symbol" ) );
393 const EASYEDAPRO::V3_DOC_RAW* sgmRaw = v3.FindRawDoc( wxS( "SYMBOL" ), sgmSymbolUuid );
394 BOOST_REQUIRE( sgmRaw );
395
396 EASYEDAPRO::SYM_INFO sgmInfo = parser.ParseSymbol( *sgmRaw, sgmDevice->attributes );
397 BOOST_REQUIRE( sgmInfo.libSymbol );
398
399 bool hasLeftPins = false;
400 bool hasRightPins = false;
401
403 {
404 if( pin->GetPosition().x < 0 )
405 {
406 hasLeftPins = true;
407 BOOST_CHECK_EQUAL( static_cast<int>( pin->GetOrientation() ),
408 static_cast<int>( PIN_ORIENTATION::PIN_RIGHT ) );
409 }
410 else if( pin->GetPosition().x > 0 )
411 {
412 hasRightPins = true;
413 BOOST_CHECK_EQUAL( static_cast<int>( pin->GetOrientation() ),
414 static_cast<int>( PIN_ORIENTATION::PIN_LEFT ) );
415 }
416 }
417
418 BOOST_CHECK( hasLeftPins );
419 BOOST_CHECK( hasRightPins );
420}
421
422
423BOOST_AUTO_TEST_CASE( PluginLoadProducesWireTextAndLabel )
424{
425 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
426 wxS( "easyedapro_v3_test" ) );
427
428 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
429
430 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
431
432 SCHEMATIC schematic( nullptr );
433 schematic.SetProject( &tempProject.Project() );
434
435 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
436 BOOST_REQUIRE( plugin );
437
438 SCH_SHEET* rootSheet = nullptr;
439 BOOST_REQUIRE_NO_THROW( rootSheet = plugin->LoadSchematicFile( archiveFile.GetFullPath(),
440 &schematic, nullptr, nullptr ) );
441 BOOST_REQUIRE( rootSheet );
442
443 schematic.RefreshHierarchy();
444
445 int wireCount = 0;
446 int textCount = 0;
447 int labelCount = 0;
448
449 for( const SCH_SHEET_PATH& sheetPath : schematic.Hierarchy() )
450 {
451 SCH_SCREEN* screen = sheetPath.LastScreen();
452
453 if( !screen )
454 continue;
455
456 for( SCH_ITEM* item : screen->Items().OfType( SCH_LINE_T ) )
457 {
458 SCH_LINE* line = static_cast<SCH_LINE*>( item );
459
460 if( line->GetLayer() == LAYER_WIRE )
461 wireCount++;
462 }
463
464 for( SCH_ITEM* item : screen->Items().OfType( SCH_TEXT_T ) )
465 {
466 SCH_TEXT* text = static_cast<SCH_TEXT*>( item );
467
468 if( !text->GetText().IsEmpty() )
469 textCount++;
470 }
471
472 for( SCH_ITEM* item : screen->Items().OfType( SCH_LABEL_T ) )
473 {
474 SCH_LABEL* label = static_cast<SCH_LABEL*>( item );
475
476 if( !label->GetText().IsEmpty() )
477 labelCount++;
478 }
479 }
480
481 BOOST_CHECK( wireCount > 0 );
482 BOOST_CHECK( textCount > 0 );
483 BOOST_CHECK( labelCount > 0 );
484}
485
486
487BOOST_AUTO_TEST_CASE( PluginLoadPreservesWireNetLabelAlignment )
488{
489 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
490 wxS( "easyedapro_v3_test" ) );
491
492 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
493
494 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
495
496 // Pick a stable, unique NET label from raw docs where align indicates the text is anchored
497 // on the right side of the connection point.
498 EASYEDAPRO::V3_DOC_PARSER v3( archiveFile.GetFullPath() );
499 BOOST_REQUIRE_NO_THROW( v3.Load() );
500
501 struct NET_ATTR
502 {
503 wxString value;
504 wxString align;
505 int rotation;
506 };
507
508 std::vector<NET_ATTR> candidates;
509 std::map<wxString, int> valueCounts;
510
511 for( const auto& [pageUuid, pageDoc] : v3.GetRawDocs( wxS( "SCH_PAGE" ) ) )
512 {
513 for( const EASYEDAPRO::V3_ROW& row : pageDoc.rows )
514 {
515 if( row.type != wxS( "ATTR" ) )
516 continue;
517
518 if( EASYEDAPRO::V3GetString( row.inner, "key" ) != wxS( "NET" ) )
519 continue;
520
521 if( !EASYEDAPRO::V3GetBool( row.inner, "valueVisible" ) )
522 continue;
523
524 wxString value =
525 EASYEDAPRO::V3JsonToString( row.inner.value( "value", nlohmann::json() ) );
526
527 if( value.IsEmpty() )
528 continue;
529
530 valueCounts[value]++;
531
532 wxString align = EASYEDAPRO::V3GetString( row.inner, "align", wxS( "LEFT_BOTTOM" ) );
533 int rotation = EASYEDAPRO::V3GetInt( row.inner, "rotation", 0 );
534
535 if( align.StartsWith( wxS( "RIGHT" ) ) )
536 candidates.push_back( { value, align, rotation } );
537 }
538 }
539
540 BOOST_REQUIRE( !candidates.empty() );
541
542 NET_ATTR selected = candidates.front();
543
544 for( const NET_ATTR& candidate : candidates )
545 {
546 if( valueCounts[candidate.value] == 1 )
547 {
548 selected = candidate;
549 break;
550 }
551 }
552
553 SPIN_STYLE expectedSpinStyle = SPIN_STYLE::RIGHT;
554
555 if( selected.align.StartsWith( wxS( "RIGHT" ) ) )
556 expectedSpinStyle = SPIN_STYLE::LEFT;
557 else if( selected.align.StartsWith( wxS( "LEFT" ) ) )
558 expectedSpinStyle = SPIN_STYLE::RIGHT;
559
560 int rot = selected.rotation % 360;
561
562 if( rot < 0 )
563 rot += 360;
564
565 if( rot % 90 == 0 )
566 {
567 for( int i = 0; i < rot; i += 90 )
568 expectedSpinStyle = expectedSpinStyle.RotateCCW();
569 }
570
571 // Load via the plugin and validate the resulting schematic keeps label spin style.
572 SCHEMATIC schematic( nullptr );
573 schematic.SetProject( &tempProject.Project() );
574
575 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
576 BOOST_REQUIRE( plugin );
577
578 SCH_SHEET* rootSheet = nullptr;
579 BOOST_REQUIRE_NO_THROW( rootSheet =
580 plugin->LoadSchematicFile( archiveFile.GetFullPath(),
581 &schematic, nullptr, nullptr ) );
582 BOOST_REQUIRE( rootSheet );
583
584 schematic.RefreshHierarchy();
585
586 int foundCount = 0;
587 bool hasExpectedSpinStyle = false;
588
589 for( const SCH_SHEET_PATH& sheetPath : schematic.Hierarchy() )
590 {
591 SCH_SCREEN* screen = sheetPath.LastScreen();
592
593 if( !screen )
594 continue;
595
596 for( SCH_ITEM* item : screen->Items().OfType( SCH_LABEL_T ) )
597 {
598 SCH_LABEL* label = static_cast<SCH_LABEL*>( item );
599
600 if( label->GetText() != selected.value )
601 continue;
602
603 foundCount++;
604
605 if( static_cast<int>( label->GetSpinStyle() ) == static_cast<int>( expectedSpinStyle ) )
606 hasExpectedSpinStyle = true;
607 }
608 }
609
610 BOOST_CHECK_EQUAL( foundCount, 1 );
611 BOOST_CHECK_MESSAGE( hasExpectedSpinStyle,
612 wxString::Format( "Label '%s' spin style mismatch", selected.value ) );
613}
614
615
616BOOST_AUTO_TEST_CASE( PluginLoadConvertsNetportsToGlobalLabels )
617{
618 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
619 wxS( "easyedapro_v3_test" ) );
620
621 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
622
623 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
624
625 // Extract expected netport instance names from the raw v3 docs.
626 EASYEDAPRO::V3_DOC_PARSER v3( archiveFile.GetFullPath() );
627 BOOST_REQUIRE_NO_THROW( v3.Load() );
628
629 std::set<wxString> netportSymbolUuids;
630
631 for( const auto& [uuid, symDoc] : v3.GetRawDocs( wxS( "SYMBOL" ) ) )
632 {
633 for( const EASYEDAPRO::V3_ROW& row : symDoc.rows )
634 {
635 if( row.type == wxS( "META" )
636 && EASYEDAPRO::V3GetInt( row.inner, "docType", 0 ) == 19 )
637 {
638 netportSymbolUuids.insert( uuid );
639 break;
640 }
641 }
642 }
643
644 BOOST_REQUIRE( !netportSymbolUuids.empty() );
645
646 std::set<wxString> netportComponentIds;
647
648 for( const auto& [pageUuid, pageDoc] : v3.GetRawDocs( wxS( "SCH_PAGE" ) ) )
649 {
650 for( const EASYEDAPRO::V3_ROW& row : pageDoc.rows )
651 {
652 if( row.type != wxS( "ATTR" ) )
653 continue;
654
655 if( EASYEDAPRO::V3GetString( row.inner, "key" ) != wxS( "Symbol" ) )
656 continue;
657
658 wxString symbolUuid = EASYEDAPRO::V3GetString( row.inner, "value" );
659
660 if( netportSymbolUuids.count( symbolUuid ) )
661 netportComponentIds.insert( EASYEDAPRO::V3GetString( row.inner, "parentId" ) );
662 }
663 }
664
665 BOOST_REQUIRE( !netportComponentIds.empty() );
666
667 std::set<wxString> expectedNetportNames;
668
669 for( const auto& [pageUuid, pageDoc] : v3.GetRawDocs( wxS( "SCH_PAGE" ) ) )
670 {
671 for( const EASYEDAPRO::V3_ROW& row : pageDoc.rows )
672 {
673 if( row.type != wxS( "ATTR" ) )
674 continue;
675
676 wxString parentId = EASYEDAPRO::V3GetString( row.inner, "parentId" );
677
678 if( !netportComponentIds.count( parentId ) )
679 continue;
680
681 if( EASYEDAPRO::V3GetString( row.inner, "key" ) != wxS( "Name" ) )
682 continue;
683
685 row.inner.value( "value", nlohmann::json() ) );
686
687 if( !name.IsEmpty() )
688 expectedNetportNames.insert( name );
689 }
690 }
691
692 BOOST_REQUIRE( !expectedNetportNames.empty() );
693
694 // Load via the plugin and validate the resulting schematic contains global labels (not symbols).
695 SCHEMATIC schematic( nullptr );
696 schematic.SetProject( &tempProject.Project() );
697
698 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
699 BOOST_REQUIRE( plugin );
700
701 SCH_SHEET* rootSheet = nullptr;
702 BOOST_REQUIRE_NO_THROW( rootSheet =
703 plugin->LoadSchematicFile( archiveFile.GetFullPath(),
704 &schematic, nullptr, nullptr ) );
705 BOOST_REQUIRE( rootSheet );
706
707 schematic.RefreshHierarchy();
708
709 int netportSymbolCount = 0;
710 int globalLabelCount = 0;
711 bool hasInputLabel = false;
712 bool hasOutputLabel = false;
713 bool hasBidiLabel = false;
714
715 std::set<wxString> importedGlobalLabelNames;
716
717 for( const SCH_SHEET_PATH& sheetPath : schematic.Hierarchy() )
718 {
719 SCH_SCREEN* screen = sheetPath.LastScreen();
720
721 if( !screen )
722 continue;
723
724 for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
725 {
726 SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
727 wxString ref = symbol->GetRef( &sheetPath, false );
728
729 if( ref.StartsWith( wxS( "#NET" ) ) )
730 netportSymbolCount++;
731 }
732
733 for( SCH_ITEM* item : screen->Items().OfType( SCH_GLOBAL_LABEL_T ) )
734 {
735 SCH_GLOBALLABEL* label = static_cast<SCH_GLOBALLABEL*>( item );
736
737 if( label->GetText().IsEmpty() )
738 continue;
739
740 globalLabelCount++;
741 importedGlobalLabelNames.insert( label->GetText() );
742
743 switch( label->GetShape() )
744 {
745 case LABEL_FLAG_SHAPE::L_INPUT: hasInputLabel = true; break;
746 case LABEL_FLAG_SHAPE::L_OUTPUT: hasOutputLabel = true; break;
747 case LABEL_FLAG_SHAPE::L_BIDI: hasBidiLabel = true; break;
748 default: break;
749 }
750 }
751 }
752
753 BOOST_CHECK_EQUAL( netportSymbolCount, 0 );
754 BOOST_CHECK( globalLabelCount > 0 );
755
756 // The test project contains Netport-IN/OUT/BI symbols; ensure we preserve their direction.
757 BOOST_CHECK( hasInputLabel );
758 BOOST_CHECK( hasOutputLabel );
759 BOOST_CHECK( hasBidiLabel );
760
761 // All netport instance names should import as global labels.
762 for( const wxString& expected : expectedNetportNames )
763 {
764 BOOST_CHECK_MESSAGE( importedGlobalLabelNames.count( expected ) > 0,
765 wxString::Format( "Missing global label '%s'", expected ) );
766 }
767}
768
769
770BOOST_AUTO_TEST_CASE( PluginLoadKeepsSchematicVerticalOrderAndFootprints )
771{
772 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
773 wxS( "easyedapro_v3_test" ) );
774
775 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
776
777 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
778
779 SCHEMATIC schematic( nullptr );
780 schematic.SetProject( &tempProject.Project() );
781
782 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
783 BOOST_REQUIRE( plugin );
784
785 SCH_SHEET* rootSheet = nullptr;
786 BOOST_REQUIRE_NO_THROW( rootSheet =
787 plugin->LoadSchematicFile( archiveFile.GetFullPath(),
788 &schematic, nullptr, nullptr ) );
789 BOOST_REQUIRE( rootSheet );
790
791 schematic.RefreshHierarchy();
792
793 SCH_SYMBOL* u6 = nullptr;
794 SCH_SYMBOL* u7 = nullptr;
795 SCH_SYMBOL* u1 = nullptr;
796
797 for( const SCH_SHEET_PATH& sheetPath : schematic.Hierarchy() )
798 {
799 SCH_SCREEN* screen = sheetPath.LastScreen();
800
801 if( !screen )
802 continue;
803
804 for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
805 {
806 SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
807 wxString ref = symbol->GetRef( &sheetPath, false );
808
809 if( ref == wxS( "U6" ) )
810 u6 = symbol;
811 else if( ref == wxS( "U7" ) )
812 u7 = symbol;
813 else if( ref == wxS( "U1" ) )
814 u1 = symbol;
815 }
816 }
817
818 BOOST_REQUIRE( u6 );
819 BOOST_REQUIRE( u7 );
820 BOOST_REQUIRE( u1 );
821
822 BOOST_CHECK_MESSAGE( u7->GetPosition().y < u6->GetPosition().y,
823 "U7 should remain above U6 after EasyEDA Pro v3 import" );
824
825 wxString u6Footprint = u6->GetFootprintFieldText( nullptr, RAW_VALUE );
826 wxString u1Footprint = u1->GetFootprintFieldText( nullptr, RAW_VALUE );
827
828 BOOST_CHECK( !u6Footprint.IsEmpty() );
829 BOOST_CHECK( u6Footprint.Contains( wxS( "SOT-563-6" ) ) );
830
831 BOOST_CHECK( !u1Footprint.IsEmpty() );
832 BOOST_CHECK( u1Footprint.Contains( wxS( "BGA-286_17x17_12.0x12.0mm" ) ) );
833}
834
835
836BOOST_AUTO_TEST_CASE( PluginLoadPowerSymbolsKeepValueAndVisibility )
837{
838 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
839 wxS( "easyedapro_v3_test" ) );
840
841 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
842
843 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
844
845 SCHEMATIC schematic( nullptr );
846 schematic.SetProject( &tempProject.Project() );
847
848 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
849 BOOST_REQUIRE( plugin );
850
851 SCH_SHEET* rootSheet = nullptr;
852 BOOST_REQUIRE_NO_THROW( rootSheet =
853 plugin->LoadSchematicFile( archiveFile.GetFullPath(),
854 &schematic, nullptr, nullptr ) );
855 BOOST_REQUIRE( rootSheet );
856
857 schematic.RefreshHierarchy();
858
859 int plus5vCount = 0;
860 int plus5vShownCount = 0;
861 int plus5vOffsetCount = 0;
862 int plus5vUprightCount = 0;
863 int plus5vUprightAtSymbolXCount = 0;
864 int powerValueOnPinBodySideCheckedCount = 0;
865 int powerValueOnWrongPinSideCount = 0;
866 int otherPowerValueCount = 0;
867 int otherPowerShownCount = 0;
868
869 for( const SCH_SHEET_PATH& sheetPath : schematic.Hierarchy() )
870 {
871 SCH_SCREEN* screen = sheetPath.LastScreen();
872
873 if( !screen )
874 continue;
875
876 for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
877 {
878 SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
879
880 if( !symbol->IsGlobalPower() )
881 continue;
882
883 SCH_FIELD* valueField = symbol->GetField( FIELD_T::VALUE );
884
885 if( !valueField )
886 continue;
887
888 wxString valueText = valueField->GetShownText( &sheetPath, INTERNAL );
889
890 if( valueField->IsVisible() && !valueText.IsEmpty() )
891 {
892 VECTOR2I delta = valueField->GetPosition() - symbol->GetPosition();
893
894 if( delta != VECTOR2I() )
895 {
896 std::vector<SCH_PIN*> pins = symbol->GetPins( &sheetPath );
897
898 if( !pins.empty() )
899 {
900 VECTOR2I dir;
901
902 switch( pins[0]->PinDrawOrient( symbol->GetTransform() ) )
903 {
904 case PIN_ORIENTATION::PIN_LEFT: dir = VECTOR2I( -1, 0 ); break;
905 case PIN_ORIENTATION::PIN_RIGHT: dir = VECTOR2I( 1, 0 ); break;
906 case PIN_ORIENTATION::PIN_UP: dir = VECTOR2I( 0, -1 ); break;
907 case PIN_ORIENTATION::PIN_DOWN: dir = VECTOR2I( 0, 1 ); break;
908 default: dir = VECTOR2I(); break;
909 }
910
911 if( dir != VECTOR2I() )
912 {
913 long long dot = static_cast<long long>( delta.x ) * dir.x
914 + static_cast<long long>( delta.y ) * dir.y;
915
916 powerValueOnPinBodySideCheckedCount++;
917
918 if( dot < 0 )
919 powerValueOnWrongPinSideCount++;
920 }
921 }
922 }
923 }
924
925 if( valueText == wxS( "+5V" ) )
926 {
927 plus5vCount++;
928
929 if( valueField->IsVisible() )
930 plus5vShownCount++;
931
932 if( valueField->GetPosition().y != symbol->GetPosition().y )
933 plus5vOffsetCount++;
934
935 if( symbol->GetOrientation() == SYM_ORIENT_0 )
936 {
937 plus5vUprightCount++;
938
939 if( valueField->GetPosition().x == symbol->GetPosition().x )
940 plus5vUprightAtSymbolXCount++;
941
944 }
945 }
946 else if( valueText.StartsWith( wxS( "+" ) ) )
947 {
948 otherPowerValueCount++;
949
950 if( valueField->IsVisible() )
951 otherPowerShownCount++;
952 }
953 }
954 }
955
956 BOOST_CHECK( plus5vCount > 0 );
957 BOOST_CHECK_EQUAL( plus5vShownCount, plus5vCount );
958 BOOST_CHECK( plus5vOffsetCount > 0 );
959 BOOST_CHECK( plus5vUprightCount > 0 );
960 BOOST_CHECK_EQUAL( plus5vUprightAtSymbolXCount, plus5vUprightCount );
961
962 BOOST_CHECK( powerValueOnPinBodySideCheckedCount > 0 );
963 BOOST_CHECK_EQUAL( powerValueOnWrongPinSideCount, 0 );
964
965 BOOST_CHECK( otherPowerValueCount > 0 );
966 BOOST_CHECK_EQUAL( otherPowerShownCount, otherPowerValueCount );
967}
968
969
974BOOST_AUTO_TEST_CASE( PluginLoadFillsRootSheetWithFirstPage )
975{
976 KI_TEST::SCOPED_TEMP_PROJECT tempProject( Pgm().GetSettingsManager(), wxS( "easyedapro_v3_import" ),
977 wxS( "easyedapro_v3_test" ) );
978
979 wxFileName archiveFile( tempProject.DirStr(), "sample", "epro2" );
980 BOOST_REQUIRE( wxCopyFile( getEasyEdaProV3ArchivePath(), archiveFile.GetFullPath() ) );
981
982 SCHEMATIC schematic( nullptr );
983 schematic.SetProject( &tempProject.Project() );
984
985 IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_EASYEDAPRO_V3 ) );
986 SCH_SHEET* rootSheet = plugin->LoadSchematicFile( archiveFile.GetFullPath(), &schematic );
987 BOOST_REQUIRE( rootSheet );
988 schematic.RefreshHierarchy();
989
990 int rootSymbols = 0;
991
992 for( SCH_ITEM* item : rootSheet->GetScreen()->Items().OfType( SCH_SYMBOL_T ) )
993 (void) item, rootSymbols++;
994
995 // The root screen must carry real schematic content, not just sub-sheet links.
996 BOOST_CHECK( rootSymbols > 0 );
997 BOOST_CHECK( !rootSheet->GetScreen()->GetPageNumber().IsEmpty() );
998}
999
1000
1006BOOST_AUTO_TEST_CASE( AlignTokenMapsToJustify )
1007{
1008 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontV( wxS( "CENTER_TOP" ) ), 0 );
1009 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontV( wxS( "CENTER_MIDDLE" ) ), 1 );
1010 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontV( wxS( "CENTER_BOTTOM" ) ), 2 );
1011 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontV( wxS( "LEFT_BOTTOM" ) ), 2 );
1012 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontV( wxS( "RIGHT_MIDDLE" ) ), 1 );
1013
1014 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontH( wxS( "CENTER_BOTTOM" ) ), 1 );
1015 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontH( wxS( "LEFT_BOTTOM" ) ), 0 );
1016 BOOST_CHECK_EQUAL( EASYEDAPRO::AlignToFontH( wxS( "RIGHT_TOP" ) ), 2 );
1017}
1018
1019
int index
const char * name
const wxString & GetPageNumber() const
Parses EasyEDA Pro v3 .epro2 archives.
const V3_DOC_RAW * FindRawDoc(const wxString &aDocType, const wxString &aUuid) const
void LoadLibrary()
Load all v3 docs from a library archive.
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:118
virtual bool IsVisible() const
Definition eda_text.h:226
GR_TEXT_H_ALIGN_T GetHorizJustify() const
Definition eda_text.h:239
GR_TEXT_V_ALIGN_T GetVertJustify() const
Definition eda_text.h:242
EE_TYPE OfType(KICAD_T aType) const
Definition sch_rtree.h:248
PROJECT & Project() const
Definition file_utils.h:147
wxString DirStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:150
int GetUnitCount() const override
std::vector< SCH_PIN * > GetGraphicalPins(int aUnit=0, int aBodyStyle=0) const override
Graphical pins: Return schematic pin objects as drawn (unexpanded), filtered by unit/body.
Holds all the data relating to one schematic.
Definition schematic.h:149
SCH_SHEET_LIST Hierarchy() const
Return the full schematic flattened hierarchical sheet list.
void SetProject(PROJECT *aPrj)
void RefreshHierarchy()
EASYEDAPRO::SYM_INFO ParseSymbol(const EASYEDAPRO::V3_DOC_RAW &aDoc, const std::map< wxString, wxString > &aDeviceAttributes, const std::map< wxString, EASYEDAPRO::BLOB > &aBlobMap={})
VECTOR2I GetPosition() const override
wxString GetShownText(const SCH_SHEET_PATH *aPath, RESOLUTION_CONTEXT aContext, const wxString &aVariantName=wxEmptyString, int aDepth=0) const
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:170
SCH_LAYER_ID GetLayer() const
Return the layer this item is on.
Definition sch_item.h:353
SPIN_STYLE GetSpinStyle() const
LABEL_FLAG_SHAPE GetShape() const
Definition sch_label.h:178
Segment description base class to describe items which have 2 end points (track, wire,...
Definition sch_line.h:39
EE_RTREE & Items()
Get the full RTree, usually for iterating.
Definition sch_screen.h:122
Handle access to a stack of flattened SCH_SHEET objects by way of a path for creating a flattened sch...
Sheet symbol placed in a schematic, and is the entry point for a sub schematic.
Definition sch_sheet.h:48
SCH_SCREEN * GetScreen() const
Definition sch_sheet.h:145
Schematic symbol object.
Definition sch_symbol.h:73
const wxString GetFootprintFieldText(const SCH_SHEET_PATH *aPath, RESOLUTION_CONTEXT aContext, const wxString &aVariantName=wxEmptyString) const
std::vector< const SCH_PIN * > GetPins(const SCH_SHEET_PATH *aSheet) const
Retrieve a list of the SCH_PINs for the given sheet path.
bool IsGlobalPower() const override
VECTOR2I GetPosition() const override
Definition sch_symbol.h:895
int GetOrientation() const override
Get the display symbol orientation.
const wxString GetRef(const SCH_SHEET_PATH *aSheet, bool aIncludeUnit=false) const override
SCH_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this symbol.
Spin style for labels of all kinds on schematics.
Definition sch_label.h:38
SPIN_STYLE RotateCCW()
const TRANSFORM & GetTransform() const
Definition symbol.h:247
@ RAW_VALUE
Definition common.h:96
@ INTERNAL
Definition common.h:94
BOOST_AUTO_TEST_CASE(LibraryIndexMergeKeepsSymbolAndFootprintNames)
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition io_mgr.h:33
@ LAYER_WIRE
Definition layer_ids.h:474
nlohmann::json BuildV3ProjectIndexFromRawDocs(const V3_DOC_PARSER &aParser, bool aIncludeLibraryMetadata=true)
Build a minimal legacy-style project index from parsed v3 raw documents.
bool V3GetBool(const nlohmann::json &aObj, const char *aKey, bool aDefault)
int AlignToFontV(const wxString &aAlign)
Map an EasyEDA "HORIZ_VERT" alignment token to a KiCad justify code (0 top/left, 1 center,...
wxString V3JsonToString(const nlohmann::json &aValue, const wxString &aDefault)
int V3GetInt(const nlohmann::json &aObj, const char *aKey, int aDefault)
int AlignToFontH(const wxString &aAlign)
std::map< wxString, wxString > BuildV3LibraryItemMap(const V3_DOC_PARSER &aParser, const char *aIndexKey, const wxString &aDocType)
wxString V3GetString(const nlohmann::json &aObj, const char *aKey, const wxString &aDefault)
std::string GetTestDataRootDir()
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
@ PIN_UP
The pin extends upwards from the connection point: Probably on the bottom side of the symbol.
Definition pin_type.h:123
@ PIN_RIGHT
The pin extends rightwards from the connection point.
Definition pin_type.h:107
@ PIN_LEFT
The pin extends leftwards from the connection point: Probably on the right side of the symbol.
Definition pin_type.h:114
@ PIN_DOWN
The pin extends downwards from the connection: Probably on the top side of the symbol.
Definition pin_type.h:131
@ L_BIDI
Definition sch_label.h:100
@ L_OUTPUT
Definition sch_label.h:99
@ L_INPUT
Definition sch_label.h:98
std::map< wxString, wxString > attributes
std::unique_ptr< LIB_SYMBOL > libSymbol
Raw parsed document from an EasyEDA Pro v3 .epru stream.
std::map< wxString, size_t > rowById
std::vector< V3_ROW > rows
One parsed row from an EasyEDA Pro v3 .epru document stream.
#define ALL_UNITS
Definition symbol.h:150
#define ALL_BODY_STYLES
Definition symbol.h:151
@ SYM_ORIENT_0
Definition symbol.h:36
@ VALUE
Field Value of part, i.e. "3.3K".
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
static wxString getEasyEdaProV3ArchivePath()
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
KIBIS_PIN * pin
VECTOR3I expected(15, 30, 45)
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2I v3(-2, 1)
int delta
@ GR_TEXT_H_ALIGN_CENTER
@ GR_TEXT_V_ALIGN_CENTER
@ SCH_LINE_T
Definition typeinfo.h:159
@ SCH_SYMBOL_T
Definition typeinfo.h:168
@ SCH_LABEL_T
Definition typeinfo.h:163
@ SCH_TEXT_T
Definition typeinfo.h:147
@ SCH_GLOBAL_LABEL_T
Definition typeinfo.h:164
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708