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 <memory>
23#include <thread>
24#include <vector>
25
26#ifdef __unix__
27#include <unistd.h>
28#endif
29
32
33#include <board.h>
34#include <footprint.h>
39
40
41namespace
42{
43
49class TEST_FOOTPRINT_LIBRARY_ADAPTER : public FOOTPRINT_LIBRARY_ADAPTER
50{
51public:
53
54 void SeedLoadError( const wxString& aNickname )
55 {
56 m_libraries[aNickname].status.load_status = LOAD_STATUS::LOAD_ERROR;
57 }
58
64 void SeedLoadedLibrary( const wxString& aNickname, const wxString& aUri )
65 {
66 m_row = std::make_unique<LIBRARY_TABLE_ROW>();
67 m_row->SetNickname( aNickname );
68 m_row->SetType( wxS( "KiCad" ) );
69 m_row->SetURI( aUri );
70
71 LIB_DATA& data = m_libraries[aNickname];
73 data.plugin = std::make_unique<PCB_IO_KICAD_SEXPR>();
74 data.row = m_row.get();
75 }
76
77private:
78 std::unique_ptr<LIBRARY_TABLE_ROW> m_row;
79};
80
81
83wxString getResistorLibPath()
84{
85 // qa/data/pcbnew/.. -> qa/data/libraries/Resistor_SMD.pretty
86 wxFileName fn( wxString::FromUTF8( KI_TEST::GetPcbnewTestDataDir() ), wxEmptyString );
87 fn.RemoveLastDir();
88 fn.AppendDir( wxS( "libraries" ) );
89 fn.AppendDir( wxS( "Resistor_SMD.pretty" ) );
90 return fn.GetPath();
91}
92
93} // namespace
94
95
96BOOST_AUTO_TEST_SUITE( FootprintLibraryAdapter )
97
98
99
107BOOST_AUTO_TEST_CASE( IsFootprintLibWritableHandlesFailedLoad )
108{
109 LIBRARY_MANAGER manager;
110 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
111
112 adapter.SeedLoadError( wxS( "BadLib" ) );
113
114 BOOST_CHECK_EQUAL( adapter.IsFootprintLibWritable( wxS( "BadLib" ) ), false );
115
116 // A library that was never even attempted must also be safe.
117 BOOST_CHECK_EQUAL( adapter.IsFootprintLibWritable( wxS( "NeverSeen" ) ), false );
118}
119
120
128BOOST_AUTO_TEST_CASE( SaveFootprintReadOnlyFilePropagatesError )
129{
130#ifdef __unix__
131 // The superuser ignores mode bits, so a read-only file stays writable and this path
132 // cannot be exercised.
133 if( ::geteuid() == 0 )
134 {
135 BOOST_TEST_MESSAGE( "Skipping read-only footprint save test when running as root." );
136 return;
137 }
138#endif
139
140 // FootprintSave validates the whole containing directory as a library, so it needs a
141 // private directory no unrelated .kicad_mod can pollute.
142 KI_TEST::TEMPORARY_DIRECTORY tmpLib( "kicad_qa_adapter_save_readonly", ".pretty" );
143
144 LIBRARY_MANAGER manager;
145 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
146
147 const wxString nickname = wxS( "scratch" );
148 adapter.SeedLoadedLibrary( nickname, tmpLib.GetPath().string() );
149
150 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
151
152 FOOTPRINT* fp = new FOOTPRINT( board.get() );
153 board->Add( fp );
154 fp->SetFPID( LIB_ID( nickname, wxS( "readonly_fp" ) ) );
155
156 BOOST_REQUIRE( adapter.SaveFootprint( nickname, fp ) == FOOTPRINT_LIBRARY_ADAPTER::SAVE_OK );
157
158 auto savedFile = tmpLib.GetPath() / "readonly_fp.kicad_mod";
159 BOOST_REQUIRE( std::filesystem::exists( savedFile ) );
160
161 // Mark only the file read-only, mirroring the issue; the directory stays writable so the
162 // writability gate still passes and TEMPORARY_DIRECTORY can unlink it.
163 std::filesystem::permissions( savedFile,
164 std::filesystem::perms::owner_write | std::filesystem::perms::group_write
165 | std::filesystem::perms::others_write,
166 std::filesystem::perm_options::remove );
167
168 BOOST_CHECK_THROW( adapter.SaveFootprint( nickname, fp ), IO_ERROR );
169}
170
171
172// Concurrency regression for the FP_CACHE heap corruption (Sentry 6819786130 / 6322230945 /
173// 7271165487): a writer churns the library while readers rebuild the cache, which races unserialized.
174BOOST_AUTO_TEST_CASE( ConcurrentPluginAccessIsSerialized )
175{
176 // Writable copy so the writer can churn the library and force concurrent cache rebuilds.
177 KI_TEST::TEMPORARY_DIRECTORY tmpLib( "kicad_qa_adapter_concurrent", ".pretty" );
178
179 for( const auto& entry : std::filesystem::directory_iterator(
180 std::filesystem::path( getResistorLibPath().ToStdString() ) ) )
181 {
182 if( entry.is_regular_file() )
183 std::filesystem::copy_file( entry.path(), tmpLib.GetPath() / entry.path().filename() );
184 }
185
186 LIBRARY_MANAGER manager;
187 TEST_FOOTPRINT_LIBRARY_ADAPTER adapter( manager );
188
189 const wxString nickname = wxS( "Resistor_SMD" );
190 adapter.SeedLoadedLibrary( nickname, tmpLib.GetPath().string() );
191
192 // Readers probe this; the writer only writes scratch names, so it always resolves.
193 const wxString stableFp = wxS( "R_0603_1608Metric" );
194
195 BOOST_REQUIRE( adapter.FootprintExists( nickname, stableFp ) );
196
197 constexpr int readerCount = 6;
198 constexpr int iterations = 40;
199
200 std::atomic<bool> sawMissing{ false };
201 std::atomic<bool> sawNullLoad{ false };
202 std::atomic<int> savedCount{ 0 };
203
204 // Release all threads together so readers and writer overlap deterministically.
205 std::atomic<int> ready{ 0 };
206 std::atomic<bool> go{ false };
207
208 auto waitForStart = [&]()
209 {
210 ready.fetch_add( 1 );
211
212 while( !go.load() )
213 std::this_thread::yield();
214 };
215
216 std::vector<std::thread> workers;
217 workers.reserve( readerCount + 1 );
218
219 for( int t = 0; t < readerCount; ++t )
220 {
221 workers.emplace_back(
222 [&]()
223 {
224 waitForStart();
225
226 for( int i = 0; i < iterations; ++i )
227 {
228 // Base-class path whose guard this change adds.
229 adapter.IsWritable( nickname );
230
231 if( !adapter.FootprintExists( nickname, stableFp ) )
232 sawMissing = true;
233
234 std::unique_ptr<FOOTPRINT> fp{ adapter.LoadFootprint( nickname, stableFp, false ) };
235
236 if( !fp )
237 sawNullLoad = true;
238 }
239 } );
240 }
241
242 workers.emplace_back(
243 [&]()
244 {
245 std::unique_ptr<FOOTPRINT> seed{ adapter.LoadFootprint( nickname, stableFp, false ) };
246
247 if( !seed )
248 {
249 sawNullLoad = true;
250 return;
251 }
252
253 waitForStart();
254
255 for( int i = 0; i < iterations; ++i )
256 {
257 // Each save bumps the library timestamp, forcing the next validateCache() to rebuild.
258 seed->SetFPID( LIB_ID( nickname, wxString::Format( wxS( "scratch_%d" ), i % 4 ) ) );
259
260 try
261 {
262 if( adapter.SaveFootprint( nickname, seed.get(), true ) == FOOTPRINT_LIBRARY_ADAPTER::SAVE_OK )
263 savedCount.fetch_add( 1 );
264 }
265 catch( const IO_ERROR& )
266 {
267 // Transient write failures are fine; a total failure trips savedCount below.
268 }
269 }
270 } );
271
272 while( ready.load() < readerCount + 1 )
273 std::this_thread::yield();
274
275 go.store( true );
276
277 for( std::thread& worker : workers )
278 worker.join();
279
280 BOOST_CHECK( !sawMissing.load() );
281 BOOST_CHECK( !sawNullLoad.load() );
282
283 // Confirm the writer churned the cache, else the readers never raced a rebuild.
284 BOOST_CHECK( savedCount.load() > 0 );
285}
286
287
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:445
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
A temporary directory that will be deleted when it goes out of scope.
const std::filesystem::path & GetPath() const
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")