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 along
18 * with this program. If not, see <http://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// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23756
208// Verifies that LoadProjectTables() clears the adapter's cached LIB_DATA entries
209// before destroying the backing LIBRARY_TABLE objects. Without this, LIB_DATA::row
210// raw pointers dangle across a reload and later calls such as GetLibraryDescription()
211// crash when dereferencing freed LIBRARY_TABLE_ROW memory. The user-visible trigger
212// was creating a new library through File -> Export -> Symbols, which re-enters the
213// SelectLibrary loop and iterates all loaded libraries to show their descriptions.
214BOOST_AUTO_TEST_CASE( LoadProjectTablesClearsAdapterCache )
215{
216 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
217 {
218 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
219 return;
220 }
221
222 LIBRARY_MANAGER manager;
223 manager.LoadGlobalTables();
224
225 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
226 PROJECT& project = SettingsManager().Prj();
227 manager.LoadProjectTables( project.GetProjectDirectory() );
228
229 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
230
231 // Force a project-scope library into m_libraries so that LIB_DATA::row holds
232 // a raw pointer into the project LIBRARY_TABLE's deque. The project-scope Device
233 // row shadows any global Device entry thanks to LoadProjectTables search order.
234 adapter->LoadOne( kDeviceLibNickname );
235 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
236
237 // Reload the project tables. Before the fix, this destroyed the old project
238 // LIBRARY_TABLE (via loadTables()'s aTarget.erase(type)) while leaving the stale
239 // LIB_DATA entry with its dangling row pointer in the adapter's m_libraries cache.
240 manager.LoadProjectTables( project.GetProjectDirectory() );
241
242 // After the fix, the project cache must be cleared. IsLibraryLoaded() reads only
243 // status (no row deref) so it is safe to call even if the cache was not cleared,
244 // which makes it a reliable regression assertion: pre-fix it returns true
245 // (stale LOADED entry), post-fix it returns false (cache emptied).
246 BOOST_REQUIRE_MESSAGE( !adapter->IsLibraryLoaded( kDeviceLibNickname ),
247 "Project library cache must be cleared by LoadProjectTables "
248 "to prevent dangling LIB_DATA::row pointers" );
249
250 // Reloading the library should succeed and produce a readable description using
251 // the fresh row pointer from the new project table.
252 adapter->LoadOne( kDeviceLibNickname );
253 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
254 BOOST_REQUIRE( adapter->GetLibraryDescription( kDeviceLibNickname ).has_value() );
255}
256
257
258// Follow-up regression for https://gitlab.com/kicad/code/kicad/-/issues/23756
259// Verifies that LoadProjectTables() preserves project-over-global shadowing for
260// cached nicknames. SYMBOL_LIBRARY_ADAPTER::GlobalLibraries is a process-wide
261// static, so a global library loaded by an earlier code path can persist as
262// LOADED while a later project introduces a same-named project row that should
263// shadow it. Erasing the project cache on reload would expose the stale global
264// entry until the project library is explicitly reloaded; resetting in place
265// (LIB_DATA{}) keeps the project nickname as a sentinel so fetchIfLoaded() and
266// IsLibraryLoaded() correctly report it as not loaded.
267BOOST_AUTO_TEST_CASE( ProjectReloadPreservesShadowing )
268{
269 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
270 {
271 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
272 return;
273 }
274
275 EnsureGlobalSymbolDir();
276
277 LIBRARY_MANAGER manager;
278
279 // Register the adapter BEFORE loading tables. LoadGlobalTables() routes
280 // GlobalTablesChanged() through registered adapters to clear the static
281 // GlobalLibraries cache. Without this, stale LIB_DATA entries left over
282 // from earlier test cases (AsyncLoad populates the process-wide
283 // GlobalLibraries with LIB_DATA::row pointers into a LIBRARY_MANAGER that
284 // is subsequently destroyed) would dangle into this test run.
285 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
286
287 manager.LoadGlobalTables();
288
289 // Force the global "Device" into the process-wide globalLibs cache before
290 // the project is loaded. With no project tables present, loadIfNeeded()
291 // falls through PROJECT scope and populates GLOBAL scope. After this call
292 // the static GlobalLibraries map holds Device with status LOADED.
293 adapter->LoadOne( kDeviceLibNickname );
294 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
295
296 // Now load the project. Its symbol library table contains a "Device" row
297 // that shadows the global Device (verified by ProjectLibraryTable above).
298 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
299 PROJECT& project = SettingsManager().Prj();
300 manager.LoadProjectTables( project.GetProjectDirectory() );
301
302 // LoadOne now resolves through PROJECT scope first and populates m_libraries
303 // with the project Device row, masking the global Device entry.
304 adapter->LoadOne( kDeviceLibNickname );
305 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
306
307 // Reload the project tables to fire ProjectTablesChanged. The project cache
308 // entry must be invalidated in place rather than erased so it continues to
309 // mask the still-LOADED global Device entry until the project library is
310 // explicitly reloaded.
311 manager.LoadProjectTables( project.GetProjectDirectory() );
312
313 // Pre-fix (m_libraries.clear()): the project Device entry is gone, so
314 // IsLibraryLoaded() falls through to globalLibs and returns true.
315 // Post-fix (in-place reset): the project Device entry remains but is no
316 // longer LOADED, so IsLibraryLoaded() returns false immediately.
317 BOOST_REQUIRE_MESSAGE( !adapter->IsLibraryLoaded( kDeviceLibNickname ),
318 "Project library reload must preserve project-over-global "
319 "shadowing for cached nicknames" );
320
321 // Reloading repopulates the entry from the rebuilt project table.
322 adapter->LoadOne( kDeviceLibNickname );
323 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
324 BOOST_REQUIRE( adapter->GetLibraryDescription( kDeviceLibNickname ).has_value() );
325}
326
327
328// Follow-up regression for https://gitlab.com/kicad/code/kicad/-/issues/23756
329// Complements ProjectReloadPreservesShadowing: if a project library is removed
330// (so the rebuilt project table no longer contains its nickname), the cached
331// sentinel installed by ProjectTablesChanged() must not permanently mask a
332// same-named global library. ProjectTablesReloaded() is responsible for erasing
333// sentinels that no longer have a backing project row.
334BOOST_AUTO_TEST_CASE( ProjectReloadReleasesRemovedShadow )
335{
336 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
337 {
338 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
339 return;
340 }
341
342 EnsureGlobalSymbolDir();
343
344 LIBRARY_MANAGER manager;
345
346 // Register adapter first so LoadGlobalTables() clears any stale static
347 // GlobalLibraries entries left over from earlier test cases.
348 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
349
350 manager.LoadGlobalTables();
351
352 // Warm the global cache with Device.
353 adapter->LoadOne( kDeviceLibNickname );
354 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
355
356 // Load the project and materialise the project-scope Device entry in
357 // m_libraries (shadows the global Device).
358 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
359 PROJECT& project = SettingsManager().Prj();
360 manager.LoadProjectTables( project.GetProjectDirectory() );
361
362 adapter->LoadOne( kDeviceLibNickname );
363 BOOST_REQUIRE( adapter->IsLibraryLoaded( kDeviceLibNickname ) );
364
365 // Now simulate the project losing its Device row. Pointing LoadProjectTables
366 // at a non-readable path tears down every project-scope LIBRARY_TABLE so the
367 // rebuilt state has no project Device row. ProjectTablesChanged() installs
368 // the sentinel in m_libraries, and ProjectTablesReloaded() must erase it
369 // because GetRow(PROJECT, "Device") no longer resolves.
370 manager.LoadProjectTables( wxString( wxS( "/nonexistent/path/for/test" ) ) );
371
372 // Pre-fix (sentinel left in place): the stale INVALID entry in m_libraries
373 // short-circuits IsLibraryLoaded() before it consults globalLibs(), so it
374 // returns false even though the global Device is still LOADED.
375 // Post-fix (ProjectTablesReloaded prunes the sentinel): IsLibraryLoaded()
376 // falls through to globalLibs() and finds the still-LOADED global Device.
377 BOOST_REQUIRE_MESSAGE( adapter->IsLibraryLoaded( kDeviceLibNickname ),
378 "Removing a project library must expose the same-named global "
379 "library rather than leave a permanent sentinel mask" );
380}
381
382
383// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23480
384// Verifies that reloading project tables during an async library load does not
385// crash due to dangling LIBRARY_TABLE_ROW pointers held by background workers.
386BOOST_AUTO_TEST_CASE( ProjectChangeDuringAsyncLoad )
387{
388 if( !wxGetEnv( wxT( "KICAD_CONFIG_HOME_IS_QA" ), nullptr ) )
389 {
390 BOOST_TEST_MESSAGE( "QA test is running using unknown config home; skipping" );
391 return;
392 }
393
394 LIBRARY_MANAGER manager;
395 manager.LoadGlobalTables();
396
397 LoadSchematic( GetTestProjectSchPath().GetFullPath() );
398 PROJECT& project = SettingsManager().Prj();
399 manager.LoadProjectTables( project.GetProjectDirectory() );
400
401 SYMBOL_LIBRARY_ADAPTER* adapter = RegisterSymbolAdapter( manager );
402
403 adapter->AsyncLoad();
404
405 // Immediately trigger a project table reload while async workers are running.
406 // Before the fix, this destroyed table rows that workers were still using.
407 manager.ProjectChanged();
408
409 adapter->BlockUntilLoaded();
410
411 BOOST_TEST_MESSAGE( "ProjectChanged during async load completed without crash" );
412}
413
414
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:130
Container for project specific data.
Definition project.h:66
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_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
BOOST_AUTO_TEST_CASE(ProjectLibraryTable)