KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_history_autosave.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 3
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
21#include <qa_utils/file_utils.h>
22
23#include <board.h>
25#include <local_history.h>
26#include <pgm_base.h>
27#include <project.h>
29#include <scoped_set_reset.h>
32
33#include <git2.h>
34
35#include <algorithm>
36#include <memory>
37#include <vector>
38
39#include <wx/datetime.h>
40#include <wx/dir.h>
41#include <wx/ffile.h>
42#include <wx/filefn.h>
43#include <wx/filename.h>
44#include <wx/stdpaths.h>
45
46
47namespace
48{
49// single_top handles libgit2 init in production; the QA harness does not, so tests driving
50// LOCAL_HISTORY must manage it themselves.
51struct LIBGIT2_SCOPE
52{
53 LIBGIT2_SCOPE() { git_libgit2_init(); }
54 ~LIBGIT2_SCOPE() { git_libgit2_shutdown(); }
55};
56
57
58struct SCOPED_PGM_BACKUP_SETTINGS
59{
67 explicit SCOPED_PGM_BACKUP_SETTINGS() :
68 m_backupReset( Pgm().GetCommonSettings()->m_Backup )
69 {
70 // Zero-init, with some useful defaults.
71 static const COMMON_SETTINGS::AUTO_BACKUP s_defaultState = {
72 .enabled = true,
76 };
77
78 Pgm().GetCommonSettings()->m_Backup = s_defaultState;
79 }
80
81 SCOPED_SET_RESET<COMMON_SETTINGS::AUTO_BACKUP> m_backupReset;
82};
83
84
85// Load a project into the settings manager and unload it on destruction, keeping the global
86// active-project state isolated even when a test aborts partway through.
87struct SCOPED_PROJECT_LOAD
88{
89 SCOPED_PROJECT_LOAD( SETTINGS_MANAGER& aMgr, const wxString& aProjectFile ) : m_mgr( aMgr )
90 {
91 m_mgr.LoadProject( aProjectFile.ToStdString() );
92 }
93
94 ~SCOPED_PROJECT_LOAD() { m_mgr.UnloadProject( &m_mgr.Prj(), false ); }
95
96 SETTINGS_MANAGER& m_mgr;
97};
98
99
100void writeTextFile( const wxString& aPath, const wxString& aContents )
101{
102 wxFFile f( aPath, wxT( "wb" ) );
103 BOOST_REQUIRE( f.IsOpened() );
104 f.Write( aContents );
105 f.Close();
106}
107
108
109std::string readTextFile( const wxString& aPath )
110{
111 wxFFile f( aPath, wxT( "rb" ) );
112 BOOST_REQUIRE( f.IsOpened() );
113
114 wxString text;
115 BOOST_REQUIRE( f.ReadAll( &text ) );
116
117 return text.ToStdString();
118}
119
120
121std::string historyEntryBytes( const HISTORY_FILE_DATA& aEntry )
122{
123 if( !aEntry.content.empty() )
124 return aEntry.content;
125
126 if( !aEntry.sourcePath.IsEmpty() )
127 return readTextFile( aEntry.sourcePath );
128
129 return std::string();
130}
131
132
133// One project seeded on disk and loaded, shared by the snapshot tests.
134struct SNAPSHOT_PROJECT
135{
136 SNAPSHOT_PROJECT( const wxString& aDirPrefix, const wxString& aFileName ) :
137 m_tempDir( aDirPrefix ),
138 m_proPath( m_tempDir.ChildPathStr( aFileName ) )
139 {
140 writeTextFile( m_proPath, wxS( "{ \"meta\": { \"version\": 3 } }\n" ) );
141 m_project = std::make_unique<SCOPED_PROJECT_LOAD>( m_mgr, m_proPath );
142 }
143
144 PROJECT& Project() { return m_mgr.Prj(); }
145 PROJECT_FILE& ProjectFile() { return m_mgr.Prj().GetProjectFile(); }
146
147 SETTINGS_MANAGER m_mgr;
148 KI_TEST::SCOPED_TEMP_DIR m_tempDir;
149 wxString m_proPath;
150 std::unique_ptr<SCOPED_PROJECT_LOAD> m_project;
151};
152
153
154void addViaStackPreset( BOARD_DESIGN_SETTINGS& aSettings, const wxString& aName )
155{
156 VIA_STACK_PRESET preset;
157 preset.m_Name = aName;
158 aSettings.m_ViaStackPresets.push_back( preset );
159}
160
161
162// Snapshot the project and return the bytes recorded for the .kicad_pro.
163std::string snapshotProjectBytes( PROJECT& aProject )
164{
165 std::vector<HISTORY_FILE_DATA> fileData;
166 aProject.SaveToHistory( aProject.GetProjectFullName(), fileData );
167
168 auto it = std::find_if( fileData.begin(), fileData.end(),
169 []( const HISTORY_FILE_DATA& aEntry )
170 {
171 return aEntry.relativePath.EndsWith( wxS( ".kicad_pro" ) );
172 } );
173
174 BOOST_REQUIRE( it != fileData.end() );
175
176 return historyEntryBytes( *it );
177}
178} // namespace
179
180
181BOOST_AUTO_TEST_SUITE( PcbHistoryAutosave )
182
183
184
193BOOST_AUTO_TEST_CASE( SaveToHistoryWithNullProjectDoesNotCrash )
194{
195 BOARD board;
196 std::vector<HISTORY_FILE_DATA> fileData;
197
198 BOOST_REQUIRE( board.GetProject() == nullptr );
199
200 BOOST_CHECK_NO_THROW( board.SaveToHistory( wxS( "/tmp/anywhere" ), fileData ) );
201 BOOST_CHECK( fileData.empty() );
202}
203
204
209BOOST_AUTO_TEST_CASE( SaveToHistoryUnsavedBoardProducesNothing )
210{
212
213 wxString tempDir = wxStandardPaths::Get().GetTempDir();
214 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxS( "pcb_autosave.kicad_pro" );
215
216 mgr.LoadProject( projectPath.ToStdString() );
217
218 BOARD board;
219 board.SetProject( &mgr.Prj() );
220
221 std::vector<HISTORY_FILE_DATA> fileData;
222
223 BOOST_REQUIRE( board.GetFileName().IsEmpty() );
224 BOOST_CHECK_NO_THROW( board.SaveToHistory( mgr.Prj().GetProjectPath(), fileData ) );
225 BOOST_CHECK( fileData.empty() );
226
227 // Detach project before BOARD destruction so design settings ownership unwinds cleanly
228 board.ClearProject();
229 mgr.UnloadProject( &mgr.Prj(), false );
230}
231
232
240BOOST_AUTO_TEST_CASE( NoSnapshotWithoutProjectFile )
241{
242 LIBGIT2_SCOPE libgit;
243
244 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
245
246 KI_TEST::SCOPED_TEMP_DIR notAProject( wxS( "kicad_qa_no_project" ) );
247 const wxString& path = notAProject.PathStr();
248
249 // Drop a board file in but no .kicad_pro - this mirrors saving a board to /tmp
250 // from standalone pcbnew.
251 wxString boardPath = path + wxFileName::GetPathSeparator() + wxS( "stray.kicad_pcb" );
252 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
253
254 LOCAL_HISTORY history;
255
256 BOOST_CHECK( !history.Init( path ) );
257 BOOST_CHECK( !history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
258 BOOST_CHECK( !history.TagSave( path, wxS( "pcb" ) ) );
259
260 // No .history directory should have been created.
261 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
262 BOOST_CHECK( !wxDirExists( historyDir ) );
263}
264
265
273BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotHandlesSubdirectories )
274{
275 LIBGIT2_SCOPE libgit;
276
277 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
278
279 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_subdirs" ) );
280 const wxString& path = project.PathStr();
281
282 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pro" ), wxS( "{}\n" ) );
283 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pcb" ),
284 wxS( "(kicad_pcb (version 20240108))\n" ) );
285
286 // Create a subdirectory whose contents will likely be collected before files at the
287 // project root, exercising the subdirectory-first iteration order.
288 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "libs" );
289 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
290 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
291 wxS( "(footprint test)\n" ) );
292
293 LOCAL_HISTORY history;
294 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Initial" ) ) );
295
296 // History must have been created at the project root, not at the subdirectory.
297 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
298 BOOST_CHECK( wxDirExists( historyDir ) );
299 BOOST_CHECK( !wxDirExists( subDir + wxFileName::GetPathSeparator() + wxS( ".history" ) ) );
300
301 wxString headBefore = history.GetHeadHash( path );
302 BOOST_REQUIRE( !headBefore.IsEmpty() );
303
304 // Mutate a file in the subdirectory to ensure the next snapshot has work to do.
305 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
306 wxS( "(footprint test (modified))\n" ) );
307
308 // CommitFullProjectSnapshot must commit using the project root, not derive a wrong
309 // root from the first file in the recursive collection.
310 BOOST_CHECK( history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
311
312 wxString headAfter = history.GetHeadHash( path );
313 BOOST_REQUIRE( !headAfter.IsEmpty() );
314 BOOST_CHECK( headBefore != headAfter );
315}
316
317
318// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24016
319BOOST_AUTO_TEST_CASE( RestoreCommitPreservesZipBackupsDirectory )
320{
321 LIBGIT2_SCOPE libgit;
322
323 // LOCAL_HISTORY early-exits when backups are disabled.
324 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
325
326 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_issue24016" ) );
327 const wxString& projectPath = tempProject.PathStr();
328
329 wxString boardPath =
330 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pcb" );
331 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
332
333 // LOCAL_HISTORY refuses to operate on directories without a project file, so seed
334 // the fixture with a minimal one to mirror a real KiCad project layout.
335 wxString projectFile =
336 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pro" );
337 writeTextFile( projectFile, wxS( "{}\n" ) );
338
339 LOCAL_HISTORY history;
340 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
341
342 wxString headHash = history.GetHeadHash( projectPath );
343 BOOST_REQUIRE( !headHash.IsEmpty() );
344
345 // Mirror SETTINGS_MANAGER::BackupProject output: a sibling "<name>-backups" directory
346 // containing one or more .zip archives.
347 wxString backupsDir =
348 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016-backups" );
349 BOOST_REQUIRE( wxFileName::Mkdir( backupsDir, 0777, wxPATH_MKDIR_FULL ) );
350
351 wxString zipPath = backupsDir + wxFileName::GetPathSeparator()
352 + wxS( "issue24016-2026-04-22_120000.zip" );
353 writeTextFile( zipPath, wxS( "pretend-zip-contents" ) );
354
355 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
356
357 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
358
359 BOOST_CHECK_MESSAGE( wxDirExists( backupsDir ),
360 "zip backups directory must survive RestoreCommit" );
361 BOOST_CHECK_MESSAGE( wxFileExists( zipPath ),
362 ".zip archive inside the backups directory must survive RestoreCommit" );
363}
364
365
379BOOST_AUTO_TEST_CASE( RestoreCommitPreservesNestedProject )
380{
381 LIBGIT2_SCOPE libgit;
382
383 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
384
385 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_nested_project" ) );
386 const wxString& projectPath = tempProject.PathStr();
387
388 // Parent project (projectA): minimal .kicad_pro plus a board file.
389 wxString parentPro = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pro" );
390 wxString parentPcb = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pcb" );
391 writeTextFile( parentPro, wxS( "{}\n" ) );
392 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108))\n" ) );
393
394 LOCAL_HISTORY history;
395 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
396
397 wxString headHash = history.GetHeadHash( projectPath );
398 BOOST_REQUIRE( !headHash.IsEmpty() );
399
400 // Now drop a nested project under projectA/. The user's scenario was that this nested
401 // project was added AFTER the parent's snapshot was committed - so its files are not in
402 // the restored commit, and a naive restore would propose them for deletion.
403 wxString nestedDir = projectPath + wxFileName::GetPathSeparator() + wxS( "projectB" );
404 BOOST_REQUIRE( wxFileName::Mkdir( nestedDir, 0777, wxPATH_MKDIR_FULL ) );
405
406 wxString nestedPro = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pro" );
407 wxString nestedPcb = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pcb" );
408 wxString nestedSch = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_sch" );
409 writeTextFile( nestedPro, wxS( "{ \"nested\": true }\n" ) );
410 writeTextFile( nestedPcb, wxS( "(kicad_pcb (version 20240108) (nested yes))\n" ) );
411 writeTextFile( nestedSch, wxS( "(kicad_sch (version 20240108))\n" ) );
412
413 // Modify the parent board so the restore actually has work to do.
414 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
415
416 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
417
418 // The nested project's directory and every one of its files MUST survive the restore.
419 BOOST_CHECK_MESSAGE( wxDirExists( nestedDir ),
420 "nested project directory must survive RestoreCommit" );
421 BOOST_CHECK_MESSAGE( wxFileExists( nestedPro ),
422 "nested .kicad_pro must survive RestoreCommit" );
423 BOOST_CHECK_MESSAGE( wxFileExists( nestedPcb ),
424 "nested .kicad_pcb must survive RestoreCommit" );
425 BOOST_CHECK_MESSAGE( wxFileExists( nestedSch ),
426 "nested .kicad_sch must survive RestoreCommit" );
427
428 // Parent project files were correctly restored.
429 BOOST_CHECK( wxFileExists( parentPro ) );
430 BOOST_CHECK( wxFileExists( parentPcb ) );
431}
432
433
439BOOST_AUTO_TEST_CASE( RestoreCommitRetainsTimestampedBackup )
440{
441 LIBGIT2_SCOPE libgit;
442
443 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
444
445 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_retained_backup" ) );
446 const wxString& projectPath = tempProject.PathStr();
447
448 wxString boardPath = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pcb" );
449 wxString projectFile = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pro" );
450 writeTextFile( projectFile, wxS( "{}\n" ) );
451 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
452
453 LOCAL_HISTORY history;
454 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
455
456 wxString headHash = history.GetHeadHash( projectPath );
457 BOOST_REQUIRE( !headHash.IsEmpty() );
458
459 // Mutate the board so restore has work to do (and produces a backup).
460 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
461
462 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
463
464 // Backups land at a SIBLING path (aProjectPath + "_restore_backup_<ts>"), so look in
465 // the parent directory of the project. Same convention as the legacy "_restore_backup".
466 wxString parentDir = wxFileName( projectPath ).GetPath();
467 wxString leafPrefix = wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup_" );
468
469 wxDir dir( parentDir );
470 BOOST_REQUIRE( dir.IsOpened() );
471
472 wxString retainedBackup;
473 bool foundLegacyBackup = false;
474
475 wxString name;
476 for( bool cont = dir.GetFirst( &name, wxEmptyString, wxDIR_DIRS ); cont;
477 cont = dir.GetNext( &name ) )
478 {
479 if( name.StartsWith( leafPrefix ) )
480 {
481 retainedBackup = parentDir + wxFileName::GetPathSeparator() + name;
482
483 // No colons - Windows path-safe.
484 BOOST_CHECK_MESSAGE( name.Find( ':' ) == wxNOT_FOUND,
485 "retained backup directory name must not contain ':' "
486 "(Windows-illegal in path components)" );
487 }
488 else if( name == wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup" ) )
489 {
490 foundLegacyBackup = true;
491 }
492 }
493
494 BOOST_CHECK_MESSAGE(
495 !retainedBackup.IsEmpty(),
496 "RestoreCommit must retain a timestamped _restore_backup_<ts>/ sibling directory" );
497 BOOST_CHECK_MESSAGE(
498 !foundLegacyBackup,
499 "RestoreCommit must not leave the legacy non-timestamped _restore_backup/ behind" );
500
501 // Clean up the retained backup so the test does not leak files into /tmp.
502 if( !retainedBackup.IsEmpty() && wxDirExists( retainedBackup ) )
503 wxFileName::Rmdir( retainedBackup, wxPATH_RMDIR_RECURSIVE );
504}
505
506
510BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotExcludesNonKiCadFiles )
511{
512 LIBGIT2_SCOPE libgit;
513
514 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
515
516 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_privacy" ) );
517 const wxString& path = project.PathStr();
518
519 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
520 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
521 wxS( "(kicad_pcb (version 20240108))\n" ) );
522 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_sch" ),
523 wxS( "(kicad_sch (version 20240108))\n" ) );
524
525 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "passwords.txt" ), wxS( "secret\n" ) );
526 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "datasheet.pdf" ), wxS( "fake pdf bytes\n" ) );
527 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "notes.md" ), wxS( "personal notes\n" ) );
528
529 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "docs" );
530 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
531 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "manual.txt" ), wxS( "irrelevant\n" ) );
532
533 LOCAL_HISTORY history;
534 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Close" ) ) );
535
536 wxString hist = path + wxFileName::GetPathSeparator() + wxS( ".history" );
537 git_repository* repo = nullptr;
538 BOOST_REQUIRE_EQUAL( git_repository_open( &repo, hist.mb_str().data() ), 0 );
539
540 git_oid head_oid;
541 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head_oid, repo, "HEAD" ), 0 );
542
543 git_commit* head = nullptr;
544 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &head_oid ), 0 );
545
546 git_tree* tree = nullptr;
547 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), 0 );
548
549 std::vector<std::string> committedPaths;
550 git_tree_walk(
551 tree, GIT_TREEWALK_PRE,
552 []( const char* root, const git_tree_entry* entry, void* payload ) -> int
553 {
554 auto* paths = static_cast<std::vector<std::string>*>( payload );
555
556 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
557 paths->push_back( std::string( root ) + git_tree_entry_name( entry ) );
558
559 return 0;
560 },
561 &committedPaths );
562
563 git_tree_free( tree );
564 git_commit_free( head );
565 git_repository_free( repo );
566
567 auto contains = [&]( const std::string& s )
568 {
569 return std::find( committedPaths.begin(), committedPaths.end(), s ) != committedPaths.end();
570 };
571
572 BOOST_CHECK_MESSAGE( contains( "p.kicad_pro" ), "kicad_pro must be committed" );
573 BOOST_CHECK_MESSAGE( contains( "p.kicad_pcb" ), "kicad_pcb must be committed" );
574 BOOST_CHECK_MESSAGE( contains( "p.kicad_sch" ), "kicad_sch must be committed" );
575
576 BOOST_CHECK_MESSAGE( !contains( "passwords.txt" ), "passwords.txt must NOT appear in history" );
577 BOOST_CHECK_MESSAGE( !contains( "datasheet.pdf" ), "datasheet.pdf must NOT appear in history" );
578 BOOST_CHECK_MESSAGE( !contains( "notes.md" ), "notes.md must NOT appear in history" );
579 BOOST_CHECK_MESSAGE( !contains( "docs/manual.txt" ), "subdirectory user content must NOT appear in history" );
580}
581
582
587BOOST_AUTO_TEST_CASE( FirstAutosaveSkipsCommitWhenStagedMatchesDisk )
588{
589 LIBGIT2_SCOPE libgit;
590
591 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
592
593 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_idle_autosave" ) );
594 const wxString& path = project.PathStr();
595
596 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
597 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
598 wxS( "(kicad_pcb (version 20240108))\n" ) );
599
600 LOCAL_HISTORY history;
601
602 // Mutating this between calls simulates the user editing the in-memory document.
603 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
604 BOOST_REQUIRE_EQUAL( readTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ) ),
605 inMemoryContent );
606
607 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
608 {
609 HISTORY_FILE_DATA entry;
610 entry.relativePath = wxS( "p.kicad_pcb" );
611 entry.content = inMemoryContent;
612 aFileData.push_back( std::move( entry ) );
613 };
614
615 history.RegisterSaver( &history, saver );
616
617 // Saver output matches disk, must skip (autosave path: empty tagFileType).
618 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
619 history.WaitForPendingSave();
620
621 wxString histDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
622 BOOST_CHECK( wxDirExists( histDir ) );
623
624 wxString head = history.GetHeadHash( path );
625 BOOST_CHECK_MESSAGE( head.IsEmpty(), "no untagged HEAD should exist after an idle first save" );
626
627 // Real edit, must commit.
628 inMemoryContent = "(kicad_pcb (version 20240108) (edited yes))\n";
629
630 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
631 history.WaitForPendingSave();
632
633 head = history.GetHeadHash( path );
634 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "in-memory edits diverging from disk must produce a commit" );
635
636 history.UnregisterSaver( &history );
637}
638
639
646BOOST_AUTO_TEST_CASE( SaverSkippedAfterOwningDocumentDestroyed )
647{
648 LIBGIT2_SCOPE libgit;
649
650 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
651
652 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_saver_lifetime" ) );
653 const wxString& path = project.PathStr();
654 const wxString sep = wxFileName::GetPathSeparator();
655
656 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
657 writeTextFile( path + sep + wxS( "p.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108))\n" ) );
658
659 LOCAL_HISTORY history;
660
661 // The saver only touches this heap counter, so it stays safe to invoke after the board is gone;
662 // gating it on the board's token is the behaviour under test.
663 auto runCount = std::make_shared<int>( 0 );
664 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
665
666 history.RegisterSaver( board.get(),
667 [runCount]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
668 {
669 ++( *runCount );
670
671 HISTORY_FILE_DATA entry;
672 entry.relativePath = wxS( "p.kicad_pcb" );
673 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
674 aFileData.push_back( std::move( entry ) );
675 },
676 board->GetHistoryLifetimeToken() );
677
678 // Positive control: while the board is alive the saver runs.
679 history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString );
680 history.WaitForPendingSave();
681 BOOST_CHECK_EQUAL( *runCount, 1 );
682
683 // Destroy the document; its expired token must make both runners skip and drop the saver.
684 board.reset();
685
686 history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString );
687 history.WaitForPendingSave();
688 BOOST_CHECK_MESSAGE( *runCount == 1, "commit runner invoked a saver whose board was destroyed" );
689
691 BOOST_CHECK_MESSAGE( *runCount == 1, "autosave-file runner invoked a saver whose board was destroyed" );
692}
693
694
700BOOST_AUTO_TEST_CASE( FirstManualSaveAlwaysCommitsOnFreshProject )
701{
702 LIBGIT2_SCOPE libgit;
703
704 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
705
706 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_manual_save" ) );
707 const wxString& path = project.PathStr();
708
709 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
710 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
711 wxS( "(kicad_pcb (version 20240108))\n" ) );
712
713 LOCAL_HISTORY history;
714
715 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
716
717 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
718 {
719 HISTORY_FILE_DATA entry;
720 entry.relativePath = wxS( "p.kicad_pcb" );
721 entry.content = inMemoryContent;
722 aFileData.push_back( std::move( entry ) );
723 };
724
725 history.RegisterSaver( &history, saver );
726
727 // Manual save (non-empty tagFileType) on fresh project: commit even when staged matches disk.
728 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Manual Save" ), wxS( "pcb" ) ) );
729
730 wxString head = history.GetHeadHash( path );
731 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "manual save on a fresh project must commit even when staged matches disk" );
732
733 history.UnregisterSaver( &history );
734}
735
736
737// With backups enabled but the format set to Zip, autosave uses legacy recovery files, so the
738// incremental git-commit path must be a no-op rather than extending a history the user switched
739// off (issue 24773).
740BOOST_AUTO_TEST_CASE( ZipFormatSkipsIncrementalAutosave )
741{
742 LIBGIT2_SCOPE libgit;
743
744 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
747
748 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_zip_skips_incremental" ) );
749 const wxString& path = project.PathStr();
750
751 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
752 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
753 wxS( "(kicad_pcb (version 20240108))\n" ) );
754
755 LOCAL_HISTORY history;
756
757 auto saver = []( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
758 {
759 HISTORY_FILE_DATA entry;
760 entry.relativePath = wxS( "p.kicad_pcb" );
761 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
762 aFileData.push_back( std::move( entry ) );
763 };
764
765 history.RegisterSaver( &history, saver );
766
767 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
768 history.WaitForPendingSave();
769
770 BOOST_CHECK_MESSAGE( history.GetHeadHash( path ).IsEmpty(),
771 "zip backup format must not create incremental autosave commits" );
772
773 history.UnregisterSaver( &history );
774}
775
776
777// The Zip backup format must reliably write legacy recovery files on autosave so a crash does
778// not lose work between manual saves (issue 24773).
779BOOST_AUTO_TEST_CASE( ZipFormatWritesRecoveryFiles )
780{
781 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
784
785 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_zip_recovery_files" ) );
786 const wxString& path = project.PathStr();
787 const wxString sep = wxFileName::GetPathSeparator();
788
789 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
790
792 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
793
794 LOCAL_HISTORY history;
795
796 auto saver = []( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
797 {
798 HISTORY_FILE_DATA entry;
799 entry.relativePath = wxS( "p.kicad_pcb" );
800 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
801 aFileData.push_back( std::move( entry ) );
802 };
803
804 history.RegisterSaver( &history, saver );
805
807
808 wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
809 BOOST_CHECK_MESSAGE( wxFileExists( autosavePath ),
810 "zip backup format must write autosave recovery files" );
811
812 history.UnregisterSaver( &history );
813}
814
815
816// A content-identical autosave with a newer mtime (cloud-sync touch) must not be flagged
817// stale, or recovery prompts fire on every open with nothing to restore (issue 24126).
818BOOST_AUTO_TEST_CASE( CloudSyncTouchedAutosaveFalselyFlaggedStale )
819{
820 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
822
823 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_cloudsync_autosave" ) );
824 const wxString& path = project.PathStr();
825 const wxString sep = wxFileName::GetPathSeparator();
826
827 // FindStaleAutosaveFiles resolves the autosave root through the active project, so it must
828 // be loaded; the .kicad_pro must exist on disk first for LoadProject to succeed.
829 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
830
832 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
833
834 const wxString sourcePath = path + sep + wxS( "p.kicad_pcb" );
835 const wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
836 const wxString boardContent = wxS( "(kicad_pcb (version 20240108))\n" );
837
838 // Byte-identical source and "_autosave-" companion, mirroring a clean save.
839 writeTextFile( sourcePath, boardContent );
840 writeTextFile( autosavePath, boardContent );
841
842 LOCAL_HISTORY history;
843
844 // Cloud-sync touch gives the identical autosave a strictly newer mtime.
845 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
846 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
847 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime, nullptr ) );
848
849 std::vector<wxString> exts{ wxS( "kicad_pcb" ) };
850 auto stale = history.FindStaleAutosaveFiles( path, exts );
851
852 BOOST_CHECK_MESSAGE( stale.empty(),
853 "Content-identical autosave with newer mtime was flagged stale, "
854 "triggering a spurious recovery prompt (issue 24126)" );
855}
856
857
858// Counterpart to the above: an autosave whose content genuinely differs (a real unsaved edit)
859// must still be flagged stale so recovery continues to fire.
860BOOST_AUTO_TEST_CASE( DivergentAutosaveStillFlaggedStale )
861{
862 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
864
865 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_divergent_autosave" ) );
866 const wxString& path = project.PathStr();
867 const wxString sep = wxFileName::GetPathSeparator();
868
869 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
870
872 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
873
874 const wxString sourcePath = path + sep + wxS( "p.kicad_pcb" );
875 const wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
876
877 // Autosave content diverges from the source, a genuine unsaved edit to recover.
878 writeTextFile( sourcePath, wxS( "(kicad_pcb (version 20240108))\n" ) );
879 writeTextFile( autosavePath, wxS( "(kicad_pcb (version 20240108) (edited yes))\n" ) );
880
881 LOCAL_HISTORY history;
882
883 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
884 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
885 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime, nullptr ) );
886
887 std::vector<wxString> exts{ wxS( "kicad_pcb" ) };
888 auto stale = history.FindStaleAutosaveFiles( path, exts );
889
890 BOOST_CHECK_MESSAGE( stale.size() == 1,
891 "Genuinely divergent autosave must still be flagged stale for recovery" );
892}
893
894
895BOOST_AUTO_TEST_CASE( EnforceSizeLimitKeepsUnstagedFilesInNextSnapshot )
896{
897 LIBGIT2_SCOPE libgit;
898
899 SCOPED_PGM_BACKUP_SETTINGS restorePgmBackup;
900
901 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_trim_partial_save" ) );
902 const wxString& path = project.PathStr();
903 const wxString sep = wxFileName::GetPathSeparator();
904
905 writeTextFile( path + sep + wxS( "trim.kicad_pro" ), wxS( "{}\n" ) );
906 writeTextFile( path + sep + wxS( "trim.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108))\n" ) );
907 writeTextFile( path + sep + wxS( "trim.kicad_sch" ), wxS( "(kicad_sch (version 20240108))\n" ) );
908
909 LOCAL_HISTORY history;
910 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Initial" ) ) );
911
912 writeTextFile( path + sep + wxS( "trim.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108) (net 1))\n" ) );
913 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Second" ) ) );
914
915 BOOST_REQUIRE( history.EnforceSizeLimit( path, 1 ) );
916
917 writeTextFile( path + sep + wxS( "trim.kicad_sch" ), wxS( "(kicad_sch (version 20240108) (mod))\n" ) );
918 BOOST_REQUIRE( history.CommitSnapshot( { path + sep + wxS( "trim.kicad_sch" ) }, wxS( "Sch Save" ) ) );
919
920 wxString historyDir = path + sep + wxS( ".history" );
921 git_repository* repo = nullptr;
922 BOOST_REQUIRE( git_repository_open( &repo, historyDir.mb_str().data() ) == 0 );
923
924 git_oid headOid;
925 git_commit* head = nullptr;
926 git_tree* tree = nullptr;
927 BOOST_REQUIRE( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 );
928 BOOST_REQUIRE( git_commit_lookup( &head, repo, &headOid ) == 0 );
929 BOOST_REQUIRE( git_commit_tree( &tree, head ) == 0 );
930
931 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_sch" ),
932 "schematic missing from snapshot after trim" );
933 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_pcb" ), "board dropped from snapshot after trim" );
934 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_pro" ),
935 "project file dropped from snapshot after trim" );
936
937 git_tree_free( tree );
938 git_commit_free( head );
939 git_repository_free( repo );
940}
941
942
943// A snapshot must serialize the live project settings, not copy the stale file from disk.
944BOOST_AUTO_TEST_CASE( ProjectSnapshotCarriesUnsavedSettings )
945{
946 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot" ), wxS( "snap.kicad_pro" ) );
947 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
948
949 BOARD_DESIGN_SETTINGS bds( &tc.ProjectFile(), "board.design_settings" );
950 addViaStackPreset( bds, wxS( "History Preset" ) );
951
952 std::string bytes = snapshotProjectBytes( tc.Project() );
953
954 BOOST_CHECK_MESSAGE( bytes.find( "History Preset" ) != std::string::npos,
955 "project file snapshot must carry the unsaved via stack preset" );
956}
957
958
959// The snapshot flush must not swallow the next real save.
960BOOST_AUTO_TEST_CASE( ProjectSaveStillWritesAfterSnapshot )
961{
962 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot_save" ), wxS( "snapsave.kicad_pro" ) );
963 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
964
965 PROJECT_FILE& projectFile = tc.ProjectFile();
966 BOARD_DESIGN_SETTINGS bds( &projectFile, "board.design_settings" );
967 addViaStackPreset( bds, wxS( "History Preset" ) );
968
969 projectFile.m_TextVars[wxS( "HISTORY_VAR" )] = wxS( "kept" );
970
971 snapshotProjectBytes( tc.Project() );
972
973 BOOST_CHECK_MESSAGE( projectFile.SaveToFile( tc.m_tempDir.PathStr() ),
974 "a real save after the snapshot must still write the file" );
975
976 std::string onDisk = readTextFile( tc.m_proPath );
977
978 BOOST_CHECK_MESSAGE( onDisk.find( "History Preset" ) != std::string::npos,
979 "the via stack preset must reach the file after the snapshot" );
980 BOOST_CHECK_MESSAGE( onDisk.find( "HISTORY_VAR" ) != std::string::npos,
981 "the text variable must reach the file after the snapshot" );
982}
983
984
985// A snapshot of a clean project must not make the next save rewrite an unchanged file (#24402).
986BOOST_AUTO_TEST_CASE( ProjectSnapshotDoesNotDirtyACleanProject )
987{
988 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot_clean" ), wxS( "snapclean.kicad_pro" ) );
989
990 PROJECT_FILE& projectFile = tc.ProjectFile();
991 BOARD_DESIGN_SETTINGS bds( &projectFile, "board.design_settings" );
992
993 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
994
995 std::string before = readTextFile( tc.m_proPath );
996
997 snapshotProjectBytes( tc.Project() );
998
999 BOOST_CHECK_MESSAGE( !projectFile.SaveToFile( tc.m_tempDir.PathStr() ),
1000 "an unchanged project must not be rewritten after a snapshot" );
1001 BOOST_CHECK_EQUAL( before, readTextFile( tc.m_proPath ) );
1002}
1003
1004
const char * name
Container for design settings for a BOARD object.
std::vector< VIA_STACK_PRESET > m_ViaStackPresets
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
void SaveToHistory(const wxString &aProjectPath, std::vector< HISTORY_FILE_DATA > &aFileData)
Serialize board into HISTORY_FILE_DATA for non-blocking history commit.
Definition board.cpp:4347
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
Definition board.cpp:379
const wxString & GetFileName() const
Definition board.h:454
void ClearProject()
Definition board.cpp:420
PROJECT * GetProject() const
Definition board.h:774
AUTO_BACKUP m_Backup
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:64
Simple local history manager built on libgit2.
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
void WaitForPendingSave()
Block until any pending background save completes.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr, bool aConfirm=true)
Restore the project files to the state recorded by the given commit hash.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
bool RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver, const std::weak_ptr< void > &aLifetime={})
Register a saver callback invoked during autosave history commits.
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:562
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
The backing store for a PROJECT, in JSON format.
std::map< wxString, wxString > m_TextVars
bool SaveToFile(const wxString &aDirectory="", bool aForce=false) override
Calls Store() and then writes the contents of the JSON document to a file.
Container for project specific data.
Definition project.h:63
virtual const wxString GetProjectFullName() const
Return the full path and name of the project.
Definition project.cpp:270
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:276
void SaveToHistory(const wxString &aProjectPath, std::vector< HISTORY_FILE_DATA > &aFileData)
Produce HISTORY_FILE_DATA entries for project files (.kicad_pro and .kicad_prl).
Definition project.cpp:560
bool SaveProject(const wxString &aFullPath=wxEmptyString, PROJECT *aProject=nullptr)
Save a loaded project.
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
@ ZIP
Zip archive snapshots; autosave uses recovery files.
@ INCREMENTAL
Git-based local history (default)
@ PROJECT_DIR
Inside the project directory (default)
constexpr char PROJECT_FILE[]
Source project file, present when the importer is handed a project's sheets rather than one file.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
BACKUP_LOCATION location
Where backups, history, and autosave files live.
static constexpr unsigned long long UNLIMITED_SIZE
Value representing unlimited total size.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
wxString sourcePath
For file-copy savers (small files like .kicad_pro)
std::string content
Serialized content (mutually exclusive with sourcePath)
wxString relativePath
Destination path relative to the project root.
A named microvia stack definition, chosen while routing instead of entering the values each time.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
static std::vector< uint8_t > bytes(const std::string &aText)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(SaveToHistoryWithNullProjectDoesNotCrash)
Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23737.
std::string path
BOOST_CHECK_EQUAL(result, "25.4")