KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_library_tables.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 <filesystem>
22#include <fstream>
23
24#include <mock_pgm_base.h>
25#include <richio.h>
29#include <pegtl/contrib/analyze.hpp>
30
36
37
38BOOST_AUTO_TEST_SUITE( LibraryTables )
39
40
42{
43 BOOST_REQUIRE( tao::pegtl::analyze< LIBRARY_TABLE_GRAMMAR::LIB_TABLE_FILE >( 1 ) == 0 );
44}
45
46
48{
50 tl::expected<LIBRARY_TABLE_IR, LIBRARY_PARSE_ERROR> result = parser.ParseBuffer( "" );
51 BOOST_REQUIRE( !result.has_value() );
52}
53
54
55BOOST_AUTO_TEST_CASE( ParseFromFile )
56{
57 std::vector<std::string> cases = {
58 "sym-lib-table",
59 "fp-lib-table"
60 };
61
62 std::filesystem::path p( KI_TEST::GetTestDataRootDir() );
63 p.append( "libraries/" );
64
66
67 for( const std::string& path : cases )
68 {
69 p.remove_filename();
70 p.append( path );
71
72 auto result = parser.Parse( p );
73
74 BOOST_REQUIRE( result.has_value() );
75 }
76}
77
78
79BOOST_AUTO_TEST_CASE( ParseAndConstruct )
80{
81 struct TESTCASE
82 {
83 wxString filename;
84 wxString expected_error;
85 size_t expected_rows;
86 bool check_formatted = true;
87 };
88
89 std::vector<TESTCASE> cases = {
90 { .filename = "sym-lib-table", .expected_rows = 224 },
91 { .filename = "fp-lib-table", .expected_rows = 146 },
92 { .filename = "nested-symbols", .expected_rows = 6 },
93 { .filename = "nested-disabled", .expected_rows = 4 },
94 { .filename = "nested-hidden", .expected_rows = 4 },
95 { .filename = "cycle", .expected_rows = 2 },
96 { .filename = "sym-hand-edited", .expected_rows = 2, .check_formatted = false },
97 { .filename = "corrupted", .expected_error = "Syntax error at line 6, column 9" },
98 { .filename = "truncated", .expected_error = "Syntax error at line 11, column 1" }
99 };
100
101 wxFileName fn( KI_TEST::GetTestDataRootDir(), wxEmptyString );
102 fn.AppendDir( "libraries" );
103
104 for( const auto& [filename, expected_error, expected_rows, check_formatted] : cases )
105 {
106 BOOST_TEST_CONTEXT( filename )
107 {
108 fn.SetName( filename );
110
111 BOOST_REQUIRE( table.IsOk() == ( expected_error.IsEmpty() ) );
112
113 BOOST_REQUIRE_MESSAGE( table.Rows().size() == expected_rows,
114 wxString::Format( "Expected %zu rows but got %zu",
115 expected_rows, table.Rows().size() ) );
116
117 BOOST_REQUIRE_MESSAGE( table.ErrorDescription() == expected_error,
118 wxString::Format( "Expected error '%s' but got '%s'",
119 expected_error, table.ErrorDescription() ) );
120
121 // Non-parsed tables can't be formatted
122 if( !table.IsOk() || !check_formatted )
123 continue;
124
125 std::ifstream inFp;
126 inFp.open( fn.GetFullPath().fn_str() );
127 BOOST_REQUIRE( inFp.is_open() );
128
129 std::stringstream inBuf;
130 inBuf << inFp.rdbuf();
131 std::string inData = inBuf.str();
132
133 STRING_FORMATTER formatter;
134 table.Format( &formatter );
136 KICAD_FORMAT::FORMAT_MODE::LIBRARY_TABLE );
137
138 if( formatter.GetString().compare( inData ) != 0 )
139 {
140 BOOST_TEST_MESSAGE( "--- original ---" );
141 BOOST_TEST_MESSAGE( inData );
142 BOOST_TEST_MESSAGE( "--- formatted ---" );
143 BOOST_TEST_MESSAGE( formatter.GetString() );
144 }
145
146 BOOST_REQUIRE( formatter.GetString().compare( inData ) == 0 );
147 }
148 }
149}
150
152{
153 LIBRARY_MANAGER manager;
154 manager.LoadGlobalTables();
155
156 BOOST_REQUIRE( manager.Rows( LIBRARY_TABLE_TYPE::SYMBOL ).size() == 3 );
157 BOOST_REQUIRE( manager.Rows( LIBRARY_TABLE_TYPE::FOOTPRINT ).size() == 146 );
158}
159
160
161// Regression coverage for the LoadGlobalTables guard: when a global library
162// table file does not exist on disk, loadTables() never inserts an entry for
163// that type into m_tables, so Table( aType, GLOBAL ) must return std::nullopt
164// instead of an entry holding a null pointer. The cleanupRemovedPCMLibraries
165// lambda inside LoadGlobalTables relies on this contract via .value_or(
166// nullptr ) and must not dereference the result. Exercising the full PCM
167// cleanup path requires extensive environment setup (3RD_PARTY env var plus
168// PCM auto-remove enabled in user settings), so we test the underlying
169// contract directly here.
170BOOST_AUTO_TEST_CASE( ManagerTableReturnsNulloptForUnloadedType )
171{
172 LIBRARY_MANAGER manager;
173
175 BOOST_REQUIRE( !result.has_value() );
176
177 LIBRARY_TABLE* table = result.value_or( nullptr );
178 BOOST_REQUIRE( table == nullptr );
179}
180
181
182BOOST_AUTO_TEST_CASE( NestedTablesDisabledHidden )
183{
184 // Test that disabled and hidden nested library table rows are parsed correctly
185 // This is a regression test for https://gitlab.com/kicad/code/kicad/-/issues/22784
186 // Note that full end-to-end testing requires the library manager to process the tables,
187 // but the parse test verifies the flag is correctly read from disk.
188
189 wxFileName fn( KI_TEST::GetTestDataRootDir(), wxEmptyString );
190 fn.AppendDir( "libraries" );
191
192 // Test with the disabled nested table
193 fn.SetName( "nested-disabled" );
194 LIBRARY_TABLE disabledTable( fn, LIBRARY_TABLE_SCOPE::GLOBAL );
195 BOOST_REQUIRE( disabledTable.IsOk() );
196 BOOST_REQUIRE_MESSAGE( disabledTable.Rows().size() == 4,
197 wxString::Format( "Expected 4 rows but got %zu",
198 disabledTable.Rows().size() ) );
199
200 // Verify the disabled flag is parsed correctly on the nested table row
201 bool foundDisabledRow = false;
202
203 for( const LIBRARY_TABLE_ROW& row : disabledTable.Rows() )
204 {
205 if( row.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
206 {
207 BOOST_REQUIRE_MESSAGE( row.Disabled(),
208 "Nested table row should have disabled flag set" );
209 foundDisabledRow = true;
210 }
211 }
212
213 BOOST_REQUIRE_MESSAGE( foundDisabledRow,
214 "Disabled nested table row not found in parsed table" );
215
216 // Test hidden nested table has same behavior
217 fn.SetName( "nested-hidden" );
219 BOOST_REQUIRE( hiddenTable.IsOk() );
220 BOOST_REQUIRE_MESSAGE( hiddenTable.Rows().size() == 4,
221 wxString::Format( "Expected 4 rows but got %zu",
222 hiddenTable.Rows().size() ) );
223
224 bool foundHiddenRow = false;
225
226 for( const LIBRARY_TABLE_ROW& row : hiddenTable.Rows() )
227 {
228 if( row.Type() == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
229 {
230 BOOST_REQUIRE_MESSAGE( row.Hidden(),
231 "Nested table row should have hidden flag set" );
232 foundHiddenRow = true;
233 }
234 }
235
236 BOOST_REQUIRE_MESSAGE( foundHiddenRow,
237 "Hidden nested table row not found in parsed table" );
238}
239
240
250BOOST_AUTO_TEST_CASE( InsertRowPreservesExistingRowPointers )
251{
252 LIBRARY_TABLE table( true, wxEmptyString, LIBRARY_TABLE_SCOPE::PROJECT );
254
255 // Seed with a few rows and snapshot pointers plus the expected URIs.
256 std::vector<const LIBRARY_TABLE_ROW*> seededPointers;
257 std::vector<wxString> seededUris;
258
259 for( int i = 0; i < 4; ++i )
260 {
261 LIBRARY_TABLE_ROW& row = table.InsertRow();
262 row.SetNickname( wxString::Format( wxS( "seed_%d" ), i ) );
263 row.SetURI( wxString::Format( wxS( "${KIPRJMOD}/libs/seed_%d.kicad_sym" ), i ) );
264 row.SetType( wxS( "KiCad" ) );
265
266 seededPointers.push_back( &row );
267 seededUris.push_back( row.URI() );
268 }
269
270 // Insert additional rows to force container growth that would reallocate
271 // a std::vector, and verify the seeded pointers continue to resolve to the
272 // same logical rows (same nickname and URI).
273 for( int i = 0; i < 64; ++i )
274 {
275 LIBRARY_TABLE_ROW& row = table.InsertRow();
276 row.SetNickname( wxString::Format( wxS( "extra_%d" ), i ) );
277 row.SetURI( wxString::Format( wxS( "${KIPRJMOD}/libs/extra_%d.kicad_sym" ), i ) );
278 row.SetType( wxS( "KiCad" ) );
279
280 for( size_t j = 0; j < seededPointers.size(); ++j )
281 {
282 BOOST_REQUIRE_MESSAGE(
283 seededPointers[j]->URI() == seededUris[j],
284 wxString::Format(
285 wxS( "Seed row %zu pointer was invalidated after inserting %d rows: "
286 "expected URI '%s', got '%s'" ),
287 j, i + 1, seededUris[j], seededPointers[j]->URI() ) );
288 }
289 }
290}
291
292
306BOOST_AUTO_TEST_CASE( IsPcmManagedRow_URITemplateMatching )
307{
308 struct CASE
309 {
310 wxString uri;
311 bool expectedPcmManaged;
312 wxString description;
313 };
314
315 std::vector<CASE> cases = {
316 { wxS( "${KICAD10_3RD_PARTY}/symbols/foo/foo.kicad_sym" ), true,
317 wxS( "Versioned 3RD_PARTY template should be recognised as PCM-managed" ) },
318 { wxS( "${KICAD9_3RD_PARTY}/symbols/legacy/legacy.kicad_sym" ), true,
319 wxS( "Legacy versioned 3RD_PARTY template should still match the wildcard" ) },
320 { wxS( "${KICAD10_3RD_PARTY}/footprints/bar/bar.pretty" ), true,
321 wxS( "Footprint library using 3RD_PARTY template should match" ) },
322 { wxS( "${KICAD_USER_LIB}/symbols/test.kicad_sym" ), false,
323 wxS( "Row using a different env var must not be flagged as PCM-managed" ) },
324 { wxS( "${KIPRJMOD}/libs/local.kicad_sym" ), false,
325 wxS( "Project-relative row must not be flagged as PCM-managed" ) },
326 { wxS( "/abs/path/to/lib.kicad_sym" ), false,
327 wxS( "Absolute path row must not be flagged as PCM-managed" ) },
328 { wxS( "${}" ), false,
329 wxS( "Malformed empty var name must not match" ) },
330 { wxS( "${KICAD10_3RD_PARTY_EXTRA}/foo" ), false,
331 wxS( "Similar-but-different var name must not match" ) },
332 };
333
334 for( const CASE& c : cases )
335 {
337 row.SetURI( c.uri );
338
340
342 actual == c.expectedPcmManaged,
343 wxString::Format( wxS( "%s: URI='%s' expected=%d actual=%d" ),
344 c.description, c.uri, c.expectedPcmManaged ? 1 : 0,
345 actual ? 1 : 0 ) );
346 }
347}
348
349
350BOOST_AUTO_TEST_CASE( ReadOnlyTable )
351{
352 // Create a temporary copy of a library table and make it read-only
353 wxFileName fn( KI_TEST::GetTestDataRootDir(), wxEmptyString );
354 fn.AppendDir( "libraries" );
355 fn.SetName( "sym-lib-table" );
356
357 wxFileName tmpFn = wxFileName::CreateTempFileName( "kicad_test_ro_" );
358 wxCopyFile( fn.GetFullPath(), tmpFn.GetFullPath() );
359
360 // Verify a writable table is not read-only
361 {
362 LIBRARY_TABLE writableTable( tmpFn, LIBRARY_TABLE_SCOPE::GLOBAL );
363 BOOST_REQUIRE( writableTable.IsOk() );
364 BOOST_REQUIRE( !writableTable.IsReadOnly() );
365 }
366
367 // Make the file read-only
368 tmpFn.SetPermissions( wxS_IRUSR | wxS_IRGRP | wxS_IROTH );
369
370 // CI containers commonly run as root, where access(W_OK) succeeds regardless of the
371 // permission bits, so a read-only file still reports as writable. IsReadOnly() uses the
372 // same IsFileWritable() check, so when the file remains writable here the read-only
373 // assertions cannot hold and are not meaningful.
374 if( wxFileName( tmpFn.GetFullPath() ).IsFileWritable() )
375 {
376 BOOST_TEST_MESSAGE( "Skipping read-only table checks; file remains writable despite "
377 "read-only permissions (running as root?)" );
378 }
379 else
380 {
382 BOOST_REQUIRE( roTable.IsOk() );
383 BOOST_REQUIRE( roTable.IsReadOnly() );
384
385 // Save should return an error for read-only tables
386 LIBRARY_RESULT<void> result = roTable.Save();
387 BOOST_REQUIRE( !result.has_value() );
388 }
389
390 // Clean up
391 tmpFn.SetPermissions( wxS_IRUSR | wxS_IWUSR );
392 wxRemoveFile( tmpFn.GetFullPath() );
393}
394
395
396BOOST_AUTO_TEST_CASE( LibOverrideSettings )
397{
398 // Test that LIB_OVERRIDE serialization in KICAD_SETTINGS works via the
399 // LIBRARY_MANAGER override API.
400 LIBRARY_MANAGER manager;
401
402 wxString tablePath = wxT( "/some/read-only/path/sym-lib-table" );
403 wxString nickname1 = wxT( "LibA" );
404 wxString nickname2 = wxT( "LibB" );
405
406 // Set an override
407 manager.SetLibOverride( tablePath, nickname1, true, false );
408 manager.SetLibOverride( tablePath, nickname2, false, true );
409
410 // Verify overrides via settings
412 KICAD_SETTINGS* settings = mgr.GetAppSettings<KICAD_SETTINGS>( "kicad" );
413
414 BOOST_REQUIRE( settings != nullptr );
415 BOOST_REQUIRE( settings->m_LibOverrides.count( tablePath ) == 1 );
416 BOOST_REQUIRE( settings->m_LibOverrides[tablePath].count( nickname1 ) == 1 );
417 BOOST_REQUIRE( settings->m_LibOverrides[tablePath][nickname1].disabled == true );
418 BOOST_REQUIRE( settings->m_LibOverrides[tablePath][nickname1].hidden == false );
419 BOOST_REQUIRE( settings->m_LibOverrides[tablePath][nickname2].disabled == false );
420 BOOST_REQUIRE( settings->m_LibOverrides[tablePath][nickname2].hidden == true );
421
422 // Clear override (both disabled and hidden are false)
423 manager.ClearLibOverride( tablePath, nickname1 );
424 BOOST_REQUIRE( settings->m_LibOverrides[tablePath].count( nickname1 ) == 0 );
425
426 // Clear last entry should remove the table key too
427 manager.ClearLibOverride( tablePath, nickname2 );
428 BOOST_REQUIRE( settings->m_LibOverrides.count( tablePath ) == 0 );
429
430 // SetLibOverride with both false should also clear
431 manager.SetLibOverride( tablePath, nickname1, true, false );
432 BOOST_REQUIRE( settings->m_LibOverrides.count( tablePath ) == 1 );
433 manager.SetLibOverride( tablePath, nickname1, false, false );
434 BOOST_REQUIRE( settings->m_LibOverrides.count( tablePath ) == 0 );
435}
436
437
std::map< wxString, std::map< wxString, LIB_OVERRIDE > > m_LibOverrides
Overrides for libraries in read-only nested tables.
void ClearLibOverride(const wxString &aTablePath, const wxString &aNickname)
Removes any override for a library that no longer needs one.
std::optional< LIBRARY_TABLE * > Table(LIBRARY_TABLE_TYPE aType, LIBRARY_TABLE_SCOPE aScope)
Retrieves a given table; creating a new empty project table if a valid project is loaded and the give...
void SetLibOverride(const wxString &aTablePath, const wxString &aNickname, bool aDisabled, bool aHidden)
Set a user override for a library in a read-only nested table.
static bool IsPcmManagedRow(const LIBRARY_TABLE_ROW &aRow)
Return true if a library table row was added by the Plugin and Content Manager.
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
tl::expected< LIBRARY_TABLE_IR, LIBRARY_PARSE_ERROR > ParseBuffer(const std::string &aBuffer)
tl::expected< LIBRARY_TABLE_IR, LIBRARY_PARSE_ERROR > Parse(const std::filesystem::path &aPath)
void SetNickname(const wxString &aNickname)
void SetType(const wxString &aType)
static const wxString TABLE_TYPE_NAME
void SetURI(const wxString &aUri)
const wxString & URI() const
LIBRARY_RESULT< void > Save()
bool IsReadOnly() const
Returns true if the underlying file exists but is not writable.
const std::deque< LIBRARY_TABLE_ROW > & Rows() const
bool IsOk() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:130
T * GetAppSettings(const char *aFilename)
Return a handle to the a given settings by type.
Implement an OUTPUTFORMATTER to a memory buffer.
Definition richio.h:422
std::string & MutableString()
Definition richio.h:450
const std::string & GetString()
Definition richio.h:445
tl::expected< ResultType, LIBRARY_ERROR > LIBRARY_RESULT
void Prettify(std::string &aSource, FORMAT_MODE aMode)
Pretty-prints s-expression text according to KiCad format rules.
std::string GetTestDataRootDir()
PGM_BASE & Pgm()
The global program "get" accessor.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
BOOST_AUTO_TEST_CASE(Grammar)
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))
std::vector< std::vector< std::string > > table
BOOST_TEST_CONTEXT("Test Clearance")
int actual
wxString result
Test unit parsing edge cases and error handling.