KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_project_template.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU 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
24
26#include <project_template.h>
28
29#include <wx/dir.h>
30#include <wx/filename.h>
31
32#include <filesystem>
33#include <fstream>
34#include <sstream>
35
36namespace fs = std::filesystem;
37
38
40{
41public:
43 {
44 m_originalDir = fs::current_path();
45 m_tempDir = fs::temp_directory_path() / "kicad_template_test";
46 fs::remove_all( m_tempDir );
47 fs::create_directories( m_tempDir );
48 }
49
51 {
52 fs::current_path( m_originalDir );
53 fs::remove_all( m_tempDir );
54 }
55
56 void CreateTemplateStructure( const std::string& templateName,
57 const std::vector<std::string>& subdirs,
58 const std::vector<std::string>& files )
59 {
60 fs::path templatePath = m_tempDir / templateName;
61 fs::create_directories( templatePath );
62
63 fs::path metaPath = templatePath / "meta";
64 fs::create_directories( metaPath );
65
66 std::ofstream infoFile( ( metaPath / "info.html" ).string() );
67 infoFile << "<html><head><title>Test Template</title></head><body></body></html>";
68 infoFile.close();
69
70 for( const auto& subdir : subdirs )
71 {
72 fs::create_directories( templatePath / subdir );
73 }
74
75 for( const auto& file : files )
76 {
77 fs::path filePath = templatePath / file;
78 fs::create_directories( filePath.parent_path() );
79 std::ofstream f( filePath.string() );
80 f << "test content";
81 f.close();
82 }
83 }
84
85 fs::path m_originalDir;
86 fs::path m_tempDir;
87};
88
89
90BOOST_FIXTURE_TEST_SUITE( ProjectTemplate, PROJECT_TEMPLATE_TEST_FIXTURE )
91
92
93BOOST_AUTO_TEST_CASE( DirectoriesRenamedCorrectly )
94{
95 // Create a template with subdirectories that should be renamed
96 CreateTemplateStructure(
97 "issue22289",
98 { "issue22289-dir", "issue22289-backups", "other-dir" },
99 { "issue22289.kicad_pro", "issue22289.kicad_sch", "issue22289-dir/test.kicad_sym" } );
100
101 fs::path templatePath = m_tempDir / "issue22289";
102 fs::path destPath = m_tempDir / "myproject";
103 fs::create_directories( destPath );
104
105 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( templatePath.string() ) );
106
107 // GetDestinationFiles expects a wxFileName with the project file path
108 wxFileName newProjectPath;
109 newProjectPath.SetPath( wxString::FromUTF8( destPath.string() ) );
110 newProjectPath.SetName( wxS( "myproject" ) );
111 newProjectPath.SetExt( wxS( "kicad_pro" ) );
112
113 std::vector<wxFileName> destFiles;
114 tmpl.GetDestinationFiles( newProjectPath, destFiles );
115
116 bool foundRenamedDir = false;
117 bool foundRenamedFile = false;
118 bool foundOtherDir = false;
119
120 for( const wxFileName& destFile : destFiles )
121 {
122 wxString fullPath = destFile.GetFullPath();
123
124 if( fullPath.Contains( wxS( "myproject-dir" ) ) )
125 foundRenamedDir = true;
126
127 if( fullPath.Contains( wxS( "issue22289-dir" ) ) )
128 BOOST_FAIL( "Directory should have been renamed from issue22289-dir to myproject-dir" );
129
130 if( fullPath.Contains( wxS( "other-dir" ) ) )
131 foundOtherDir = true;
132
133 if( destFile.GetName() == wxS( "myproject" ) && destFile.GetExt() == wxS( "kicad_pro" ) )
134 foundRenamedFile = true;
135 }
136
137 BOOST_CHECK_MESSAGE( foundRenamedDir, "Should find myproject-dir in destination files" );
138 BOOST_CHECK_MESSAGE( foundRenamedFile, "Should find myproject.kicad_pro in destination files" );
139 BOOST_CHECK_MESSAGE( foundOtherDir, "Should preserve other-dir (not matching template name)" );
140}
141
142
143BOOST_AUTO_TEST_CASE( CreateProjectRenamesDirectories )
144{
145 CreateTemplateStructure(
146 "testtemplate",
147 { "testtemplate-lib", "testtemplate" },
148 { "testtemplate.kicad_pro", "testtemplate-lib/component.kicad_sym",
149 "testtemplate/nested.txt" } );
150
151 fs::path templatePath = m_tempDir / "testtemplate";
152 fs::path destPath = m_tempDir / "newproject";
153 fs::create_directories( destPath );
154
155 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( templatePath.string() ) );
156
157 // CreateProject expects a wxFileName with the project file path (including extension)
158 wxFileName newProjectPath;
159 newProjectPath.SetPath( wxString::FromUTF8( destPath.string() ) );
160 newProjectPath.SetName( wxS( "newproject" ) );
161 newProjectPath.SetExt( wxS( "kicad_pro" ) );
162
163 wxString errorMsg;
164 bool result = tmpl.CreateProject( newProjectPath, &errorMsg );
165
166 BOOST_CHECK_MESSAGE( result, "CreateProject should succeed: " + errorMsg.ToStdString() );
167
168 BOOST_CHECK( fs::exists( destPath / "newproject.kicad_pro" ) );
169 BOOST_CHECK( fs::exists( destPath / "newproject-lib" ) );
170 BOOST_CHECK( fs::exists( destPath / "newproject-lib" / "component.kicad_sym" ) );
171
172 BOOST_CHECK_MESSAGE( !fs::exists( destPath / "testtemplate-lib" ),
173 "Old directory name should not exist" );
174}
175
176
177BOOST_AUTO_TEST_CASE( ExactMatchDirectoryRenamed )
178{
179 // Test that a directory exactly matching the template name is renamed
180 CreateTemplateStructure( "mytemplate", { "mytemplate" },
181 { "mytemplate.kicad_pro", "mytemplate/subfile.txt" } );
182
183 fs::path templatePath = m_tempDir / "mytemplate";
184 fs::path destPath = m_tempDir / "finalproject";
185 fs::create_directories( destPath );
186
187 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( templatePath.string() ) );
188
189 // GetDestinationFiles expects a wxFileName with the project file path
190 wxFileName newProjectPath;
191 newProjectPath.SetPath( wxString::FromUTF8( destPath.string() ) );
192 newProjectPath.SetName( wxS( "finalproject" ) );
193 newProjectPath.SetExt( wxS( "kicad_pro" ) );
194
195 std::vector<wxFileName> destFiles;
196 tmpl.GetDestinationFiles( newProjectPath, destFiles );
197
198 bool foundExactRenamedDir = false;
199
200 for( const wxFileName& destFile : destFiles )
201 {
202 wxString fullPath = destFile.GetFullPath();
203
204 if( fullPath.Contains( wxS( "/finalproject/finalproject/" ) )
205 || fullPath.Contains( wxS( "\\finalproject\\finalproject\\" ) ) )
206 {
207 foundExactRenamedDir = true;
208 }
209
210 if( fullPath.Contains( wxS( "/finalproject/mytemplate/" ) )
211 || fullPath.Contains( wxS( "\\finalproject\\mytemplate\\" ) ) )
212 {
213 BOOST_FAIL( "Exact match directory should be renamed from mytemplate to finalproject" );
214 }
215 }
216
217 BOOST_CHECK_MESSAGE( foundExactRenamedDir, "Should find renamed subdirectory finalproject" );
218}
219
220
221BOOST_AUTO_TEST_CASE( TemplateWithoutMetaDirSetsErrorTitle )
222{
223 // Issue #23623: user template directories that are just regular KiCad projects
224 // (no meta/info.html) should not crash. The constructor should set an error
225 // title and GetTitle() should return that error without trying to open a
226 // nonexistent file.
227 fs::path noMetaPath = m_tempDir / "no_meta_template";
228 fs::create_directories( noMetaPath );
229
230 std::ofstream f( ( noMetaPath / "no_meta_template.kicad_pro" ).string() );
231 f << "{}";
232 f.close();
233
234 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( noMetaPath.string() ) );
235
236 wxString* title = tmpl.GetTitle();
237 BOOST_REQUIRE( title != nullptr );
238 BOOST_CHECK_MESSAGE( !title->IsEmpty(), "Template without meta dir should have an error title" );
239
240 // Calling GetTitle() again should return the same cached result without
241 // attempting to open meta/info.html
242 wxString* title2 = tmpl.GetTitle();
243 BOOST_CHECK( *title == *title2 );
244}
245
246
247BOOST_AUTO_TEST_CASE( TemplateWithMetaDirButNoInfoHtml )
248{
249 // meta directory exists but info.html is missing
250 fs::path tmplPath = m_tempDir / "meta_no_html";
251 fs::create_directories( tmplPath / "meta" );
252
253 std::ofstream f( ( tmplPath / "meta_no_html.kicad_pro" ).string() );
254 f << "{}";
255 f.close();
256
257 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( tmplPath.string() ) );
258
259 wxString* title = tmpl.GetTitle();
260 BOOST_REQUIRE( title != nullptr );
261 BOOST_CHECK_MESSAGE( !title->IsEmpty(),
262 "Template with meta dir but no info.html should have an error title" );
263}
264
265
266// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24343
267// EnsureDefaultProjectTemplate must seed the built-in "default" template under the given base
268// directory and yield a directory that PROJECT_TEMPLATE can load as a valid template.
269BOOST_AUTO_TEST_CASE( EnsureDefaultTemplateSeedsBaseDir )
270{
271 wxString baseDir = wxString::FromUTF8( ( m_tempDir / "default_seed" ).string() );
272 wxFileName seeded = EnsureDefaultProjectTemplate( baseDir );
273
274 BOOST_REQUIRE_MESSAGE( seeded.IsOk(), "Seeding the default template should succeed" );
275
276 // The default template must live directly under the requested base directory, not somewhere
277 // derived from an environment variable.
278 fs::path expected = m_tempDir / "default_seed" / "default";
279 BOOST_CHECK( fs::exists( expected ) );
280 BOOST_CHECK( fs::exists( expected / "meta" / "info.html" ) );
281 BOOST_CHECK( fs::exists( expected / "default.kicad_pro" ) );
282
283 // PROJECT_TEMPLATE must recognize the seeded directory as a valid template (meta dir and
284 // info.html present, so no error is reported).
285 PROJECT_TEMPLATE tmpl( seeded.GetPath() );
286
287 BOOST_CHECK_MESSAGE( tmpl.GetError().IsEmpty(),
288 "Seeded default template should load without error: " + tmpl.GetError() );
289
290 wxString* title = tmpl.GetTitle();
291
292 BOOST_REQUIRE( title != nullptr );
293 BOOST_CHECK( !title->IsEmpty() );
294}
295
296
297BOOST_AUTO_TEST_CASE( ProjectCreatedFromDefaultTemplateLoads )
298{
299 wxString baseDir = wxString::FromUTF8( ( m_tempDir / "default_load" ).string() );
300 wxFileName seeded = EnsureDefaultProjectTemplate( baseDir );
301
302 BOOST_REQUIRE( seeded.IsOk() );
303
304 fs::path destPath = m_tempDir / "fromdefault";
305 fs::create_directories( destPath );
306
307 PROJECT_TEMPLATE tmpl( seeded.GetPath() );
308
309 wxFileName newProjectPath;
310 newProjectPath.SetPath( wxString::FromUTF8( destPath.string() ) );
311 newProjectPath.SetName( wxS( "fromdefault" ) );
312 newProjectPath.SetExt( wxS( "kicad_pro" ) );
313
314 wxString errorMsg;
315
316 BOOST_REQUIRE_MESSAGE( tmpl.CreateProject( newProjectPath, &errorMsg ),
317 "CreateProject should succeed: " + errorMsg.ToStdString() );
318
320
321 BOOST_CHECK_MESSAGE( mgr.LoadProject( newProjectPath.GetFullPath() ),
322 "A project made from the default template must load" );
323}
324
325
326// An empty base directory must not create anything and must report failure.
327BOOST_AUTO_TEST_CASE( EnsureDefaultTemplateRejectsEmptyBaseDir )
328{
329 wxFileName seeded = EnsureDefaultProjectTemplate( wxEmptyString );
330
331 BOOST_CHECK( !seeded.IsOk() );
332}
333
334
335// Calling EnsureDefaultProjectTemplate twice must be idempotent and not clobber existing content.
336BOOST_AUTO_TEST_CASE( EnsureDefaultTemplateIsIdempotent )
337{
338 wxString baseDir = wxString::FromUTF8( ( m_tempDir / "default_idempotent" ).string() );
339
340 wxFileName first = EnsureDefaultProjectTemplate( baseDir );
341 BOOST_REQUIRE( first.IsOk() );
342
343 // Overwrite the project file with custom content to verify it survives a second call.
344 fs::path proFile = m_tempDir / "default_idempotent" / "default" / "default.kicad_pro";
345 {
346 std::ofstream f( proFile.string() );
347 f << "{\"custom\":true}";
348 }
349
350 wxFileName second = EnsureDefaultProjectTemplate( baseDir );
351 BOOST_REQUIRE( second.IsOk() );
352 BOOST_CHECK_EQUAL( first.GetPath(), second.GetPath() );
353
354 std::ifstream in( proFile.string() );
355 std::stringstream buffer;
356 buffer << in.rdbuf();
357
358 BOOST_CHECK_EQUAL( buffer.str(), std::string( "{\"custom\":true}" ) );
359}
360
361
362BOOST_AUTO_TEST_CASE( ReadOnlyTemplateProducesWritableProject )
363{
364 CreateTemplateStructure( "rotemplate", { "rotemplate-lib" },
365 { "rotemplate.kicad_pro", "rotemplate.kicad_sch", "rotemplate-lib/component.kicad_sym" } );
366
367 fs::path templatePath = m_tempDir / "rotemplate";
368 fs::path destPath = m_tempDir / "rwproject";
369 fs::create_directories( destPath );
370
371 const std::vector<fs::path> templateFiles = { templatePath / "rotemplate.kicad_pro",
372 templatePath / "rotemplate.kicad_sch",
373 templatePath / "rotemplate-lib" / "component.kicad_sym" };
374
375 for( const fs::path& file : templateFiles )
376 {
377 fs::permissions( file, fs::perms::owner_write | fs::perms::group_write | fs::perms::others_write,
378 fs::perm_options::remove );
379 }
380
381 PROJECT_TEMPLATE tmpl( wxString::FromUTF8( templatePath.string() ) );
382
383 wxFileName newProjectPath;
384 newProjectPath.SetPath( wxString::FromUTF8( destPath.string() ) );
385 newProjectPath.SetName( wxS( "rwproject" ) );
386 newProjectPath.SetExt( wxS( "kicad_pro" ) );
387
388 wxString errorMsg;
389 bool created = tmpl.CreateProject( newProjectPath, &errorMsg );
390
391 // Restore write permission before asserting, so that a failed assertion still leaves the
392 // fixture able to remove the temporary directory.
393 for( const fs::path& file : templateFiles )
394 fs::permissions( file, fs::perms::owner_write, fs::perm_options::add );
395
396 BOOST_REQUIRE_MESSAGE( created, "CreateProject failed: " + errorMsg.ToStdString() );
397
398 const std::vector<fs::path> projectFiles = { destPath / "rwproject.kicad_pro", destPath / "rwproject.kicad_sch",
399 destPath / "rwproject-lib" / "component.kicad_sym" };
400
401 for( const fs::path& file : projectFiles )
402 {
403 wxFileName fn( wxString::FromUTF8( file.string() ) );
404
405 BOOST_REQUIRE_MESSAGE( fn.FileExists(), file.string() + " was not created" );
406 BOOST_CHECK_MESSAGE( fn.IsFileWritable(), file.string() + " is read only" );
407 }
408}
409
410
void CreateTemplateStructure(const std::string &templateName, const std::vector< std::string > &subdirs, const std::vector< std::string > &files)
A class which provides project template functionality.
size_t GetDestinationFiles(const wxFileName &aNewProjectPath, std::vector< wxFileName > &aDestFiles)
Fetch the list of destination files to be copied when the new project is created.
wxString * GetTitle()
Get the title of the project (extracted from the html title tag)
bool CreateProject(wxFileName &aNewProjectPath, wxString *aErrorMsg=nullptr)
Copies and renames all template files to create a new project.
const wxString & GetError() const
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
wxFileName EnsureDefaultProjectTemplate(const wxString &aBaseDir)
Seed the built-in "default" project template under aBaseDir, creating the directory tree and minimal ...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_AUTO_TEST_CASE(DirectoriesRenamedCorrectly)
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")