KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_symbol_library.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 * @author Jon Evans <[email protected]>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <fmt/format.h>
22#include <mock_pgm_base.h>
24#include <pgm_base.h>
25#include "eeschema_test_utils.h"
26
28
29// NOTE: this is for the new symbol library manager adapter and
30// library manager system. There is also test_symbol_library_manager.cpp
31// which is for the old code; that one can be removed once the old
32// library manager doesn't exist anymore
33
34
35namespace
36{
37constexpr const char* kDeviceLibNickname = "Device";
38}
39
40
42{
43protected:
48 {
50 std::make_unique<SYMBOL_LIBRARY_ADAPTER>( aManager ) );
51
52 std::optional<LIBRARY_MANAGER_ADAPTER*> adapterOpt =
54 BOOST_REQUIRE( adapterOpt.has_value() );
55 return static_cast<SYMBOL_LIBRARY_ADAPTER*>( *adapterOpt );
56 }
57
58 wxFileName GetTestProjectSchPath() const
59 {
60 wxFileName fn( KI_TEST::GetTestDataRootDir(), "test_project.kicad_sch" );
61 fn.AppendDir( "libraries" );
62 fn.AppendDir( "test_project" );
63 return fn;
64 }
65
70 {
71 if( !wxGetEnv( wxT( "KICAD9_SYMBOL_DIR" ), nullptr ) )
72 {
74 path += wxT( "/libraries" );
75 wxSetEnv( wxT( "KICAD9_SYMBOL_DIR" ), path );
76 }
77 }
78};
79
80BOOST_FIXTURE_TEST_SUITE( SymbolLibrary, TEST_SYMBOL_LIBRARY_FIXTURE )
81
82BOOST_AUTO_TEST_CASE( ProjectLibraryTable )
83{
84 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
85 {
86 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
87 return;
88 }
89
90 LIBRARY_MANAGER manager;
91 manager.LoadGlobalTables();
92
93 wxFileName fn( KI_TEST::GetTestDataRootDir(), "test_project.kicad_sch" );
94 fn.AppendDir( "libraries" );
95 fn.AppendDir( "test_project" );
96
97 std::vector<LIBRARY_TABLE_ROW*> rows = manager.Rows( LIBRARY_TABLE_TYPE::SYMBOL );
98
99 BOOST_REQUIRE( rows.size() == 3 );
100 BOOST_REQUIRE( rows[0]->Nickname() == "Device" );
101
102 LoadSchematic( fn.GetFullPath() );
103 PROJECT& project = SettingsManager().Prj();
104 manager.LoadProjectTables( project.GetProjectDirectory() );
105
106 rows = manager.Rows( LIBRARY_TABLE_TYPE::SYMBOL );
107
108 BOOST_REQUIRE( rows.size() == 4 );
109 BOOST_REQUIRE( rows[0]->Nickname() == "Device" );
110 BOOST_REQUIRE( rows[0]->URI() == "${KIPRJMOD}/Device.kicad_sym" );
111
112 SYMBOL_LIBRARY_ADAPTER adapter( manager );
113 adapter.LoadOne( "Device" );
114
115 std::vector<LIB_SYMBOL*> symbols = adapter.GetSymbols( "Device" );
116
117 // Check that the project's copy of Device is the one being returned
118 BOOST_REQUIRE( symbols.size() == 3 );
119
120 symbols = adapter.GetSymbols( "power" );
121 BOOST_TEST_MESSAGE( symbols.size() );
122
123 BOOST_REQUIRE( adapter.LoadSymbol( "Device", "R" ) );
124}
125
126
127// If you want to do benchmarking, it can be useful to run this testcase standalone with
128// the KICAD_CONFIG_HOME and KICAD9_SYMBOL_DIR environment variables set to an actual KiCad
129// installation so that you can benchmark a typical load
131{
132 LIBRARY_MANAGER manager;
133 manager.LoadGlobalTables();
134
136 SYMBOL_LIBRARY_ADAPTER adapter( manager );
137
138 auto tstart = std::chrono::high_resolution_clock::now();
139 adapter.AsyncLoad();
140
141 constexpr static int interval = 250;
142 constexpr static int timeLimit = 20000;
143 int elapsed = 0;
144
145 while( true )
146 {
147 std::this_thread::sleep_for( std::chrono::milliseconds( interval ) );
148
149 if( std::optional<float> loadStatus = adapter.AsyncLoadProgress(); loadStatus.has_value() )
150 {
151 BOOST_TEST_MESSAGE( fmt::format( "Loading libraries: ({:.1f}%)", *loadStatus * 100 ) );
152
153 if( loadStatus >= 1 )
154 break;
155 }
156 else
157 {
158 // Just informational; this could be fine when running in a QA context
159 BOOST_TEST_MESSAGE( "Async load not in progress" );
160 break;
161 }
162
163 elapsed += interval;
164
165 if( elapsed > timeLimit )
166 {
167 BOOST_TEST_FAIL( "Exceeded timeout" );
168 break;
169 }
170 }
171
172 auto duration = std::chrono::high_resolution_clock::now() - tstart;
173
175 fmt::format( "took {}ms",
176 std::chrono::duration_cast<std::chrono::milliseconds>( duration ).count() ) );
177
178 adapter.BlockUntilLoaded();
179
180 std::vector<LIBRARY_TABLE_ROW*> rows = manager.Rows( LIBRARY_TABLE_TYPE::SYMBOL );
181
182 BOOST_REQUIRE_GE( rows.size(), 2 );
183
184 for( auto& [nickname, status] : adapter.GetLibraryStatuses() )
185 {
186 wxString msg = nickname;
187 BOOST_REQUIRE( status.load_status != LOAD_STATUS::LOADING );
188
189 switch( status.load_status )
190 {
191 default:
193 msg << ": loaded OK";
194 BOOST_REQUIRE( !status.error.has_value() );
195 break;
196
198 BOOST_REQUIRE( status.error.has_value() );
199 msg << ": error: " << status.error->message;
200 break;
201 }
202
203 BOOST_TEST_MESSAGE( msg );
204 }
205}
206
207
208// A LIBRARY_MANAGER_ADAPTER caches loaded global libraries in the process-wide static
209// GlobalLibraries with raw LIBRARY_TABLE_ROW pointers into its own manager's tables. When a
210// transient manager is destroyed those pointers dangle, and a later manager's fetchIfLoaded()
211// would return the stale entry. The adapter destructor must evict the entries it owns.
212BOOST_AUTO_TEST_CASE( GlobalCacheEvictedOnAdapterDestruction )
213{
214 wxString loadedNickname;
215
216 {
217 LIBRARY_MANAGER manager;
218 manager.LoadGlobalTables();
219
221 SYMBOL_LIBRARY_ADAPTER adapter( manager );
222
223 adapter.AsyncLoad();
224 adapter.BlockUntilLoaded();
225
226 for( auto& [nickname, status] : adapter.GetLibraryStatuses() )
227 {
228 if( status.load_status == LOAD_STATUS::LOADED )
229 {
230 loadedNickname = nickname;
231 break;
232 }
233 }
234
235 if( loadedNickname.IsEmpty() )
236 {
237 BOOST_TEST_MESSAGE( "No global symbol library loaded; skipping" );
238 return;
239 }
240
241 BOOST_REQUIRE( adapter.IsLibraryLoaded( loadedNickname ) );
242 }
243
244 // The owning manager and adapter are gone. A fresh adapter must not still see the library
245 // loaded in the global cache. IsLibraryLoaded() reads only status, so it is safe even when
246 // the entry's row dangles: pre-fix it returns true (stale entry), post-fix false (evicted).
247 LIBRARY_MANAGER manager2;
248 SYMBOL_LIBRARY_ADAPTER adapter2( manager2 );
249
250 BOOST_CHECK_MESSAGE( !adapter2.IsLibraryLoaded( loadedNickname ),
251 "global cache entry must be evicted when its owning adapter is destroyed" );
252}
253
254// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23756
255// Verifies that LoadProjectTables() clears the adapter's cached LIB_DATA entries
256// before destroying the backing LIBRARY_TABLE objects. Without this, LIB_DATA::row
257// raw pointers dangle across a reload and later calls such as GetLibraryDescription()
258// crash when dereferencing freed LIBRARY_TABLE_ROW memory. The user-visible trigger
259// was creating a new library through File -> Export -> Symbols, which re-enters the
260// SelectLibrary loop and iterates all loaded libraries to show their descriptions.
261BOOST_AUTO_TEST_CASE( LoadProjectTablesClearsAdapterCache )
262{
263 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
264 {
265 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
266 return;
267 }
268
269 LIBRARY_MANAGER manager;
270 manager.LoadGlobalTables();
271
272 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
273 PROJECT& project = SettingsManager().Prj();
274 manager.LoadProjectTables( project.GetProjectDirectory() );
275
276 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
277
278 // Force a project-scope library into m_libraries so that LIB_DATA::row holds
279 // a raw pointer into the project LIBRARY_TABLE's deque. The project-scope Device
280 // row shadows any global Device entry thanks to LoadProjectTables search order.
281 adapter->LoadOne( kDeviceLibNickname );
282 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
283
284 // Reload the project tables. Before the fix, this destroyed the old project
285 // LIBRARY_TABLE (via loadTables()'s aTarget.erase(type)) while leaving the stale
286 // LIB_DATA entry with its dangling row pointer in the adapter's m_libraries cache.
287 manager.LoadProjectTables( project.GetProjectDirectory() );
288
289 // After the fix, the project cache must be cleared. IsLibraryLoaded() reads only
290 // status (no row deref) so it is safe to call even if the cache was not cleared,
291 // which makes it a reliable regression assertion: pre-fix it returns true
292 // (stale LOADED entry), post-fix it returns false (cache emptied).
293 BOOST_REQUIRE_MESSAGE( !adapter->IsLibraryLoaded( kDeviceLibNickname ),
294 "Project library cache must be cleared by LoadProjectTables "
295 "to prevent dangling LIB_DATA::row pointers" );
296
297 // Reloading the library should succeed and produce a readable description using
298 // the fresh row pointer from the new project table.
299 adapter->LoadOne( kDeviceLibNickname );
300 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
301 BOOST_REQUIRE( adapter->GetLibraryDescription( kDeviceLibNickname ).has_value() );
302}
303
304
305// Follow-up regression for https://gitlab.com/kicad/code/kicad/-/issues/23756
306// Verifies that LoadProjectTables() preserves project-over-global shadowing for
307// cached nicknames. SYMBOL_LIBRARY_ADAPTER::GlobalLibraries is a process-wide
308// static, so a global library loaded by an earlier code path can persist as
309// LOADED while a later project introduces a same-named project row that should
310// shadow it. Erasing the project cache on reload would expose the stale global
311// entry until the project library is explicitly reloaded; resetting in place
312// (LIB_DATA{}) keeps the project nickname as a sentinel so fetchIfLoaded() and
313// IsLibraryLoaded() correctly report it as not loaded.
314BOOST_AUTO_TEST_CASE( ProjectReloadPreservesShadowing )
315{
316 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
317 {
318 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
319 return;
320 }
321
322 EnsureGlobalSymbolDir();
323
324 LIBRARY_MANAGER manager;
325
326 // Register the adapter BEFORE loading tables. LoadGlobalTables() routes
327 // GlobalTablesChanged() through registered adapters to clear the static
328 // GlobalLibraries cache. Without this, stale LIB_DATA entries left over
329 // from earlier test cases (AsyncLoad populates the process-wide
330 // GlobalLibraries with LIB_DATA::row pointers into a LIBRARY_MANAGER that
331 // is subsequently destroyed) would dangle into this test run.
332 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
333
334 manager.LoadGlobalTables();
335
336 // Force the global "Device" into the process-wide globalLibs cache before
337 // the project is loaded. With no project tables present, loadIfNeeded()
338 // falls through PROJECT scope and populates GLOBAL scope. After this call
339 // the static GlobalLibraries map holds Device with status LOADED.
340 adapter->LoadOne( kDeviceLibNickname );
341 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
342
343 // Now load the project. Its symbol library table contains a "Device" row
344 // that shadows the global Device (verified by ProjectLibraryTable above).
345 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
346 PROJECT& project = SettingsManager().Prj();
347 manager.LoadProjectTables( project.GetProjectDirectory() );
348
349 // LoadOne now resolves through PROJECT scope first and populates m_libraries
350 // with the project Device row, masking the global Device entry.
351 adapter->LoadOne( kDeviceLibNickname );
352 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
353
354 // Reload the project tables to fire ProjectTablesChanged. The project cache
355 // entry must be invalidated in place rather than erased so it continues to
356 // mask the still-LOADED global Device entry until the project library is
357 // explicitly reloaded.
358 manager.LoadProjectTables( project.GetProjectDirectory() );
359
360 // Pre-fix (m_libraries.clear()): the project Device entry is gone, so
361 // IsLibraryLoaded() falls through to globalLibs and returns true.
362 // Post-fix (in-place reset): the project Device entry remains but is no
363 // longer LOADED, so IsLibraryLoaded() returns false immediately.
364 BOOST_REQUIRE_MESSAGE( !adapter->IsLibraryLoaded( kDeviceLibNickname ),
365 "Project library reload must preserve project-over-global "
366 "shadowing for cached nicknames" );
367
368 // Reloading repopulates the entry from the rebuilt project table.
369 adapter->LoadOne( kDeviceLibNickname );
370 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
371 BOOST_REQUIRE( adapter->GetLibraryDescription( kDeviceLibNickname ).has_value() );
372}
373
374
375// Follow-up regression for https://gitlab.com/kicad/code/kicad/-/issues/23756
376// Complements ProjectReloadPreservesShadowing: if a project library is removed
377// (so the rebuilt project table no longer contains its nickname), the cached
378// sentinel installed by ProjectTablesChanged() must not permanently mask a
379// same-named global library. ProjectTablesReloaded() is responsible for erasing
380// sentinels that no longer have a backing project row.
381BOOST_AUTO_TEST_CASE( ProjectReloadReleasesRemovedShadow )
382{
383 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
384 {
385 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
386 return;
387 }
388
389 EnsureGlobalSymbolDir();
390
391 LIBRARY_MANAGER manager;
392
393 // Register adapter first so LoadGlobalTables() clears any stale static
394 // GlobalLibraries entries left over from earlier test cases.
395 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
396
397 manager.LoadGlobalTables();
398
399 // Warm the global cache with Device.
400 adapter->LoadOne( kDeviceLibNickname );
401 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
402
403 // Load the project and materialise the project-scope Device entry in
404 // m_libraries (shadows the global Device).
405 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
406 PROJECT& project = SettingsManager().Prj();
407 manager.LoadProjectTables( project.GetProjectDirectory() );
408
409 adapter->LoadOne( kDeviceLibNickname );
410 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
411
412 // Now simulate the project losing its Device row. Pointing LoadProjectTables
413 // at a non-readable path tears down every project-scope LIBRARY_TABLE so the
414 // rebuilt state has no project Device row. ProjectTablesChanged() installs
415 // the sentinel in m_libraries, and ProjectTablesReloaded() must erase it
416 // because GetRow(PROJECT, "Device") no longer resolves.
417 manager.LoadProjectTables( wxString( wxS( "/nonexistent/path/for/test" ) ) );
418
419 // Pre-fix (sentinel left in place): the stale INVALID entry in m_libraries
420 // short-circuits IsLibraryLoaded() before it consults globalLibs(), so it
421 // returns false even though the global Device is still LOADED.
422 // Post-fix (ProjectTablesReloaded prunes the sentinel): IsLibraryLoaded()
423 // falls through to globalLibs() and finds the still-LOADED global Device.
424 BOOST_REQUIRE_MESSAGE( adapter->IsLibraryLoaded( kDeviceLibNickname ),
425 "Removing a project library must expose the same-named global "
426 "library rather than leave a permanent sentinel mask" );
427}
428
429
430// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23480
431// Verifies that reloading project tables during an async library load does not
432// crash due to dangling LIBRARY_TABLE_ROW pointers held by background workers.
433BOOST_AUTO_TEST_CASE( ProjectChangeDuringAsyncLoad )
434{
435 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
436 {
437 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
438 return;
439 }
440
441 LIBRARY_MANAGER manager;
442 manager.LoadGlobalTables();
443
444 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
445 PROJECT& project = SettingsManager().Prj();
446 manager.LoadProjectTables( project.GetProjectDirectory() );
447
448 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
449
450 adapter->AsyncLoad();
451
452 // Immediately trigger a project table reload while async workers are running.
453 // Before the fix, this destroyed table rows that workers were still using.
454 manager.ProjectChanged();
455
456 adapter->BlockUntilLoaded();
457
458 BOOST_TEST_MESSAGE( "ProjectChanged during async load completed without crash" );
459}
460
461
A generic fixture for loading schematics and associated settings for qa tests.
std::optional< float > AsyncLoadProgress() const
Returns async load progress between 0.0 and 1.0, or nullopt if load is not in progress.
bool IsLibraryLoaded(const wxString &aNickname)
std::optional< wxString > GetLibraryDescription(const wxString &aNickname) const
void AsyncLoad()
Loads all available libraries for this adapter type in the background.
std::vector< std::pair< wxString, LIB_STATUS > > GetLibraryStatuses() const
Returns a list of all library nicknames and their status (even if they failed to load)
std::optional< LIBRARY_MANAGER_ADAPTER * > Adapter(LIBRARY_TABLE_TYPE aType) const
void RegisterAdapter(LIBRARY_TABLE_TYPE aType, std::unique_ptr< LIBRARY_MANAGER_ADAPTER > &&aAdapter)
void LoadProjectTables(std::initializer_list< LIBRARY_TABLE_TYPE > aTablesToLoad={})
(Re)loads the project library tables in the given list, or all tables if no list is given
std::vector< LIBRARY_TABLE_ROW * > Rows(LIBRARY_TABLE_TYPE aType, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH, bool aIncludeInvalid=false) const
Returns a flattened list of libraries of the given type.
void LoadGlobalTables(std::initializer_list< LIBRARY_TABLE_TYPE > aTablesToLoad={})
(Re)loads the global library tables in the given list, or all tables if no list is given
void ProjectChanged()
Notify all adapters that the project has changed.
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
Container for project specific data.
Definition project.h:62
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
An interface to the global shared library manager that is schematic-specific and linked to one projec...
std::optional< LIB_STATUS > LoadOne(LIB_DATA *aLib) override
Loads or reloads the given library, if it exists.
LIB_SYMBOL * LoadSymbol(const wxString &aNickname, const wxString &aName)
Load a LIB_SYMBOL having aName from the library given by aNickname.
std::vector< LIB_SYMBOL * > GetSymbols(const wxString &aNickname, SYMBOL_TYPE aType=SYMBOL_TYPE::ALL_SYMBOLS)
wxFileName GetTestProjectSchPath() const
void EnsureGlobalSymbolDir()
Ensure KICAD9_SYMBOL_DIR points at the QA data libraries so the global sym-lib-table's ${KICAD9_SYMBO...
SYMBOL_LIBRARY_ADAPTER * RegisterSymbolAdapter(LIBRARY_MANAGER &aManager)
Register a fresh SYMBOL_LIBRARY_ADAPTER on the manager and return it.
std::string GetTestDataRootDir()
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
static void LoadSchematic(SCHEMATIC *aSchematic, SCH_SHEET *aRootSheet, const wxString &aFileName)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
BOOST_AUTO_TEST_CASE(ProjectLibraryTable)