KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_footprint_library_adapter.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <atomic>
21#include <filesystem>
22#include <fstream>
23#include <algorithm>
24#include <iterator>
25#include <memory>
26#include <set>
27#include <thread>
28#include <vector>
29
30#ifdef __unix__
31#include <unistd.h>
32#endif
33
34#include <qa_utils/file_utils.h>
37
38#include <board.h>
39#include <footprint.h>
44
45
46namespace
47{
48
54class TEST_FOOTPRINT_LIBRARY_ADAPTER : public FOOTPRINT_LIBRARY_ADAPTER
55{
56public:
58
59 void SeedLoadError( const wxString& aNickname )
60 {
61 m_libraries[aNickname].status.load_status = LOAD_STATUS::LOAD_ERROR;
62 }
63
69 void SeedLoadedLibrary( const wxString& aNickname, const wxString& aUri )
70 {
71 LIBRARY_TABLE_ROW* row = m_rows.emplace_back( std::make_unique<LIBRARY_TABLE_ROW>() ).get();
72 row->SetNickname( aNickname );
73 row->SetType( wxS( "KiCad" ) );
74 row->SetURI( aUri );
75
76 LIB_DATA& data = m_libraries[aNickname];
78 data.plugin = std::make_unique<PCB_IO_KICAD_SEXPR>();
79 data.row = row;
80 }
81
82private:
83 std::vector<std::unique_ptr<LIBRARY_TABLE_ROW>> m_rows;
84};
85
86
88wxString getResistorLibPath()
89{
90 // qa/data/pcbnew/.. -> qa/data/libraries/Resistor_SMD.pretty
91 wxFileName fn( wxString::FromUTF8( KI_TEST::GetPcbnewTestDataDir() ), wxEmptyString );
92 fn.RemoveLastDir();
93 fn.AppendDir( wxS( "libraries" ) );
94 fn.AppendDir( wxS( "Resistor_SMD.pretty" ) );
95 return fn.GetPath();
96}
97
98} // namespace
99
100
101BOOST_AUTO_TEST_SUITE( FootprintLibraryAdapter )
102
103
104
112BOOST_AUTO_TEST_CASE( IsFootprintLibWritableHandlesFailedLoad )
113{
114 LIBRARY_MANAGER manager;
115 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
116
117 adapter.SeedLoadError( wxS( "BadLib" ) );
118
119 BOOST_CHECK_EQUAL( adapter.IsFootprintLibWritable( wxS( "BadLib" ) ), false );
120
121 // A library that was never even attempted must also be safe.
122 BOOST_CHECK_EQUAL( adapter.IsFootprintLibWritable( wxS( "NeverSeen" ) ), false );
123}
124
125
133BOOST_AUTO_TEST_CASE( SaveFootprintReadOnlyFilePropagatesError )
134{
135#ifdef __unix__
136 // The superuser ignores mode bits, so a read-only file stays writable and this path
137 // cannot be exercised.
138 if( ::geteuid() == 0 )
139 {
140 BOOST_TEST_MESSAGE( "Skipping read-only footprint save test when running as root." );
141 return;
142 }
143#endif
144
145 // FootprintSave validates the whole containing directory as a library, so it needs a
146 // private directory no unrelated .kicad_mod can pollute.
147 KI_TEST::SCOPED_TEMP_DIR tmpLib( "kicad_qa_adapter_save_readonly" );
148 const std::filesystem::path libPath = tmpLib.CreateChildDir( "kicad_qa_adapter_save_readonly.pretty" );
149
150 LIBRARY_MANAGER manager;
151 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
152
153 const wxString nickname = wxS( "scratch" );
154 adapter.SeedLoadedLibrary( nickname, libPath.string() );
155
156 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
157
158 FOOTPRINT* fp = new FOOTPRINT( board.get() );
159 board->Add( fp );
160 fp->SetFPID( LIB_ID( nickname, wxS( "readonly_fp" ) ) );
161
162 BOOST_REQUIRE( adapter.SaveFootprint( nickname, fp ) == FOOTPRINT_LIBRARY_ADAPTER::SAVE_OK );
163
164 std::filesystem::path savedFile = libPath / "readonly_fp.kicad_mod";
165 BOOST_REQUIRE( std::filesystem::exists( savedFile ) );
166
167 // Mark only the file read-only, mirroring the issue; the directory stays writable so the
168 // writability gate still passes and the temporary directory can unlink it.
169 std::filesystem::permissions( savedFile,
170 std::filesystem::perms::owner_write | std::filesystem::perms::group_write
171 | std::filesystem::perms::others_write,
172 std::filesystem::perm_options::remove );
173
174 BOOST_CHECK_THROW( adapter.SaveFootprint( nickname, fp ), IO_ERROR );
175}
176
177
178// Concurrency regression for the FP_CACHE heap corruption (Sentry 6819786130 / 6322230945 /
179// 7271165487): a writer churns the library while readers rebuild the cache, which races unserialized.
180BOOST_AUTO_TEST_CASE( ConcurrentPluginAccessIsSerialized )
181{
182 // Writable copy so the writer can churn the library and force concurrent cache rebuilds.
183 KI_TEST::SCOPED_TEMP_DIR tmpLib( "kicad_qa_adapter_concurrent" );
184 const std::filesystem::path libPath = tmpLib.CreateChildDir( "kicad_qa_adapter_concurrent.pretty" );
185
186 for( const auto& entry : std::filesystem::directory_iterator(
187 std::filesystem::path( getResistorLibPath().ToStdString() ) ) )
188 {
189 if( entry.is_regular_file() )
190 std::filesystem::copy_file( entry.path(), libPath / entry.path().filename() );
191 }
192
193 LIBRARY_MANAGER manager;
194 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
195
196 const wxString nickname = wxS( "Resistor_SMD" );
197 adapter.SeedLoadedLibrary( nickname, libPath.string() );
198
199 // Readers probe this; the writer only writes scratch names, so it always resolves.
200 const wxString stableFp = wxS( "R_0603_1608Metric" );
201
202 BOOST_REQUIRE( adapter.FootprintExists( nickname, stableFp ) );
203
204 constexpr int readerCount = 6;
205 constexpr int iterations = 40;
206
207 std::atomic<bool> sawMissing{ false };
208 std::atomic<bool> sawNullLoad{ false };
209 std::atomic<int> savedCount{ 0 };
210
211 // Release all threads together so readers and writer overlap deterministically.
212 std::atomic<int> ready{ 0 };
213 std::atomic<bool> go{ false };
214
215 auto waitForStart = [&]()
216 {
217 ready.fetch_add( 1 );
218
219 while( !go.load() )
220 std::this_thread::yield();
221 };
222
223 std::vector<std::thread> workers;
224 workers.reserve( readerCount + 1 );
225
226 for( int t = 0; t < readerCount; ++t )
227 {
228 workers.emplace_back(
229 [&]()
230 {
231 waitForStart();
232
233 for( int i = 0; i < iterations; ++i )
234 {
235 // Base-class path whose guard this change adds.
236 adapter.IsWritable( nickname );
237
238 if( !adapter.FootprintExists( nickname, stableFp ) )
239 sawMissing = true;
240
241 std::unique_ptr<FOOTPRINT> fp{ adapter.LoadFootprint( nickname, stableFp, false ) };
242
243 if( !fp )
244 sawNullLoad = true;
245 }
246 } );
247 }
248
249 workers.emplace_back(
250 [&]()
251 {
252 std::unique_ptr<FOOTPRINT> seed{ adapter.LoadFootprint( nickname, stableFp, false ) };
253
254 if( !seed )
255 {
256 sawNullLoad = true;
257 return;
258 }
259
260 waitForStart();
261
262 for( int i = 0; i < iterations; ++i )
263 {
264 // Each save bumps the library timestamp, forcing the next validateCache() to rebuild.
265 seed->SetFPID( LIB_ID( nickname, wxString::Format( wxS( "scratch_%d" ), i % 4 ) ) );
266
267 try
268 {
269 if( adapter.SaveFootprint( nickname, seed.get(), true ) == FOOTPRINT_LIBRARY_ADAPTER::SAVE_OK )
270 savedCount.fetch_add( 1 );
271 }
272 catch( const IO_ERROR& )
273 {
274 // Transient write failures are fine; a total failure trips savedCount below.
275 }
276 }
277 } );
278
279 while( ready.load() < readerCount + 1 )
280 std::this_thread::yield();
281
282 go.store( true );
283
284 for( std::thread& worker : workers )
285 worker.join();
286
287 BOOST_CHECK( !sawMissing.load() );
288 BOOST_CHECK( !sawNullLoad.load() );
289
290 // Confirm the writer churned the cache, else the readers never raced a rebuild.
291 BOOST_CHECK( savedCount.load() > 0 );
292}
293
294
295BOOST_AUTO_TEST_CASE( RefreshChangedLibrariesPicksUpExternalAddition )
296{
297 KI_TEST::SCOPED_TEMP_DIR tmpLib( "kicad_qa_adapter_stale" );
298 KI_TEST::SCOPED_TEMP_DIR tmpTable( "kicad_qa_adapter_stale_table" );
299
300 const std::filesystem::path libPath = tmpLib.CreateChildDir( "kicad_qa_adapter_stale.pretty" );
301
302 const std::filesystem::path source =
303 std::filesystem::path( getResistorLibPath().ToStdString() ) / "R_0402_1005Metric.kicad_mod";
304
305 std::filesystem::copy_file( source, libPath / "R_0402_1005Metric.kicad_mod" );
306
307 const wxString nickname = wxS( "StaleCheck" );
308
309 {
310 std::ofstream table( tmpTable.Path() / "fp-lib-table" );
311 table << "(fp_lib_table\n (version 7)\n";
312 table << " (lib (name \"" << nickname.ToStdString() << "\")(type \"KiCad\")(uri \""
313 << libPath.string() << "\")(options \"\")(descr \"\"))\n)\n";
314 }
315
316 LIBRARY_MANAGER manager;
317 manager.LoadProjectTables( tmpTable.PathStr(), { LIBRARY_TABLE_TYPE::FOOTPRINT } );
318
319 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
320 adapter.SeedLoadedLibrary( nickname, libPath.string() );
321
322 adapter.RefreshLibraryIfChanged( nickname );
323 BOOST_REQUIRE_EQUAL( adapter.GetFootprints( nickname, true ).size(), 1u );
324
325 std::filesystem::copy_file( source, libPath / "ZZ_PulledFootprint.kicad_mod" );
326
327 adapter.RefreshChangedLibraries();
328
329 bool found = false;
330
331 for( FOOTPRINT* fp : adapter.GetFootprints( nickname, true ) )
332 {
333 if( fp && fp->GetFPID().GetLibItemName().wx_str() == wxS( "ZZ_PulledFootprint" ) )
334 found = true;
335 }
336
337 BOOST_CHECK_MESSAGE( found, "a footprint added to the library on disk is missing from the listing" );
338}
339
340
341BOOST_AUTO_TEST_CASE( RefreshChangedLibrariesSkipsUnchangedLibraries )
342{
343 KI_TEST::SCOPED_TEMP_DIR tmpA( "kicad_qa_adapter_skip_a" );
344 KI_TEST::SCOPED_TEMP_DIR tmpB( "kicad_qa_adapter_skip_b" );
345 KI_TEST::SCOPED_TEMP_DIR tmpTable( "kicad_qa_adapter_skip_table" );
346
347 const std::filesystem::path libPathA = tmpA.CreateChildDir( "kicad_qa_adapter_skip_a.pretty" );
348 const std::filesystem::path libPathB = tmpB.CreateChildDir( "kicad_qa_adapter_skip_b.pretty" );
349
350 const std::filesystem::path source =
351 std::filesystem::path( getResistorLibPath().ToStdString() ) / "R_0402_1005Metric.kicad_mod";
352
353 std::filesystem::copy_file( source, libPathA / "R_0402_1005Metric.kicad_mod" );
354
355 for( int i = 0; i < 8; ++i )
356 {
357 std::filesystem::copy_file( source, libPathB / ( "R_" + std::to_string( i ) + ".kicad_mod" ) );
358 }
359
360 const wxString nickA = wxS( "SkipCheckA" );
361 const wxString nickB = wxS( "SkipCheckB" );
362
363 const std::filesystem::path tablePath = tmpTable.Path() / "fp-lib-table";
364
365 {
366 std::ofstream table( tablePath );
367 table << "(fp_lib_table\n (version 7)\n";
368 table << " (lib (name \"" << nickA.ToStdString() << "\")(type \"KiCad\")(uri \"" << libPathA.string()
369 << "\")(options \"\")(descr \"\"))\n";
370 table << " (lib (name \"" << nickB.ToStdString() << "\")(type \"KiCad\")(uri \"" << libPathB.string()
371 << "\")(options \"\")(descr \"\"))\n)\n";
372 }
373
374 LIBRARY_MANAGER manager;
375 manager.LoadProjectTables( tmpTable.PathStr(), { LIBRARY_TABLE_TYPE::FOOTPRINT } );
376
377 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
378 adapter.SeedLoadedLibrary( nickA, libPathA.string() );
379 adapter.SeedLoadedLibrary( nickB, libPathB.string() );
380
381 adapter.RefreshLibraryIfChanged( nickA );
382 adapter.RefreshLibraryIfChanged( nickB );
383
384 BOOST_REQUIRE_EQUAL( adapter.GetFootprints( nickA, true ).size(), 1u );
385 BOOST_REQUIRE_EQUAL( adapter.GetFootprints( nickB, true ).size(), 8u );
386
387 // Re-enumeration frees every footprint in the library and allocates replacements. A single
388 // address could be reused by chance, a whole vector of them could not.
389 std::vector<FOOTPRINT*> untouched = adapter.GetFootprints( nickB, true );
390
391 std::filesystem::copy_file( source, libPathA / "ZZ_Added.kicad_mod" );
392
393 adapter.RefreshChangedLibraries();
394
395 BOOST_CHECK_EQUAL( adapter.GetFootprints( nickA, true ).size(), 2u );
396 BOOST_CHECK_MESSAGE( adapter.GetFootprints( nickB, true ) == untouched, "an unchanged library was re-enumerated" );
397}
398
399
405BOOST_AUTO_TEST_CASE( EditorRoundTripKeepsFileUuids )
406{
407 KI_TEST::SCOPED_TEMP_DIR tmpLib( "kicad_qa_adapter_rt" );
408 KI_TEST::SCOPED_TEMP_DIR tmpTable( "kicad_qa_adapter_rt_table" );
409
410 const std::filesystem::path libPath = tmpLib.CreateChildDir( "kicad_qa_adapter_rt.pretty" );
411
412 const std::filesystem::path source =
413 std::filesystem::path( getResistorLibPath().ToStdString() ) / "R_0402_1005Metric.kicad_mod";
414 const std::filesystem::path target = libPath / "R_0402_1005Metric.kicad_mod";
415
416 std::filesystem::copy_file( source, target );
417
418 const wxString nickname = wxS( "RoundTrip" );
419 const wxString fpName = wxS( "R_0402_1005Metric" );
420
421 {
422 std::ofstream table( tmpTable.Path() / "fp-lib-table" );
423 table << "(fp_lib_table\n (version 7)\n";
424 table << " (lib (name \"" << nickname.ToStdString() << "\")(type \"KiCad\")(uri \""
425 << libPath.string() << "\")(options \"\")(descr \"\"))\n)\n";
426 }
427
428 auto fileUuids =
429 []( const std::filesystem::path& aPath )
430 {
431 std::set<std::string> ids;
432 std::ifstream in( aPath );
433 std::string line;
434
435 while( std::getline( in, line ) )
436 {
437 size_t pos = line.find( "(uuid \"" );
438
439 if( pos != std::string::npos )
440 ids.insert( line.substr( pos + 7, 36 ) );
441 }
442
443 return ids;
444 };
445
446 const std::set<std::string> before = fileUuids( target );
447
448 LIBRARY_MANAGER manager;
449 manager.LoadProjectTables( tmpTable.PathStr(), { LIBRARY_TABLE_TYPE::FOOTPRINT } );
450
451 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
452 adapter.SeedLoadedLibrary( nickname, libPath.string() );
453
454 // Populates the preloaded-footprint cache the editor then loads through
455 adapter.RefreshLibraryIfChanged( nickname );
456
457 std::unique_ptr<FOOTPRINT> edited( adapter.LoadFootprint( nickname, fpName, true ) );
458 BOOST_REQUIRE( edited );
459
460 BOOST_REQUIRE( adapter.SaveFootprint( nickname, edited.get() ) == FOOTPRINT_LIBRARY_ADAPTER::SAVE_OK );
461
462 const std::set<std::string> after = fileUuids( target );
463
464 std::vector<std::string> common;
465 std::set_intersection( before.begin(), before.end(), after.begin(), after.end(),
466 std::back_inserter( common ) );
467
468 // Only the empty "Footprint" property is dropped on write; every other id must survive
469 BOOST_REQUIRE( !before.empty() );
470 BOOST_CHECK_EQUAL( after.size() + 1, before.size() );
471 BOOST_CHECK_MESSAGE( common.size() == after.size(),
472 "a load/save round trip through the editor path rewrote UUIDs in the .kicad_mod" );
473}
474
475
An interface to the global shared library manager that is schematic-specific and linked to one projec...
FOOTPRINT_LIBRARY_ADAPTER(LIBRARY_MANAGER &aManager)
void SetFPID(const LIB_ID &aFPID)
Definition footprint.h:474
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:62
const std::filesystem::path & Path() const
Get the path to the temporary directory as a std::filesystem::path.
Definition file_utils.h:59
std::filesystem::path CreateChildDir(const wxString &aName) const
Create and return the path to a direct child directory.
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
void SetNickname(const wxString &aNickname)
void SetType(const wxString &aType)
void SetURI(const wxString &aUri)
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
static std::string ToStdString(const wxString &aStr)
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
LIB_STATUS status
std::unique_ptr< IO_BASE > plugin
const LIBRARY_TABLE_ROW * row
LOAD_STATUS load_status
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(IsFootprintLibWritableHandlesFailedLoad)
Regression test for a null-plugin dereference crash.
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")