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