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, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 */
20
22
23#include <board.h>
24#include <local_history.h>
25#include <pgm_base.h>
26#include <project.h>
29
30#include <git2.h>
31
32#include <vector>
33
34#include <wx/dir.h>
35#include <wx/ffile.h>
36#include <wx/filefn.h>
37#include <wx/filename.h>
38#include <wx/stdpaths.h>
39
40
41namespace
42{
43// single_top handles libgit2 init in production; the QA harness does not, so tests driving
44// LOCAL_HISTORY must manage it themselves.
45struct LIBGIT2_SCOPE
46{
47 LIBGIT2_SCOPE() { git_libgit2_init(); }
48 ~LIBGIT2_SCOPE() { git_libgit2_shutdown(); }
49};
50
51
52struct SCOPED_BOOL_OVERRIDE
53{
54 explicit SCOPED_BOOL_OVERRIDE( bool& aFlag ) : m_flag( aFlag ), m_original( aFlag ) {}
55 ~SCOPED_BOOL_OVERRIDE() { m_flag = m_original; }
56
57 bool& m_flag;
58 bool m_original;
59};
60
61
62// Recursively remove the directory on destruction so test failures (which throw out of
63// BOOST_REQUIRE) do not leak temp directories.
64struct SCOPED_TEMP_DIR
65{
66 explicit SCOPED_TEMP_DIR( const wxString& aPrefix )
67 {
68 wxString base = wxStandardPaths::Get().GetTempDir();
69 m_path = base + wxFileName::GetPathSeparator() + aPrefix
70 + wxString::Format( wxS( "_%lu_%ld" ),
71 static_cast<unsigned long>( ::wxGetProcessId() ),
72 static_cast<long>( wxDateTime::UNow().GetTicks() ) );
73 wxFileName::Mkdir( m_path, 0777, wxPATH_MKDIR_FULL );
74 }
75
76 ~SCOPED_TEMP_DIR()
77 {
78 if( !m_path.IsEmpty() && wxDirExists( m_path ) )
79 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
80 }
81
82 const wxString& Path() const { return m_path; }
83
84 wxString m_path;
85};
86
87
88void writeTextFile( const wxString& aPath, const wxString& aContents )
89{
90 wxFFile f( aPath, wxT( "w" ) );
91 BOOST_REQUIRE( f.IsOpened() );
92 f.Write( aContents );
93 f.Close();
94}
95} // namespace
96
97
98BOOST_AUTO_TEST_SUITE( PcbHistoryAutosave )
99
100
101
110BOOST_AUTO_TEST_CASE( SaveToHistoryWithNullProjectDoesNotCrash )
111{
112 BOARD board;
113 std::vector<HISTORY_FILE_DATA> fileData;
114
115 BOOST_REQUIRE( board.GetProject() == nullptr );
116
117 BOOST_CHECK_NO_THROW( board.SaveToHistory( wxS( "/tmp/anywhere" ), fileData ) );
118 BOOST_CHECK( fileData.empty() );
119}
120
121
126BOOST_AUTO_TEST_CASE( SaveToHistoryUnsavedBoardProducesNothing )
127{
129
130 wxString tempDir = wxStandardPaths::Get().GetTempDir();
131 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxS( "pcb_autosave.kicad_pro" );
132
133 mgr.LoadProject( projectPath.ToStdString() );
134
135 BOARD board;
136 board.SetProject( &mgr.Prj() );
137
138 std::vector<HISTORY_FILE_DATA> fileData;
139
140 BOOST_REQUIRE( board.GetFileName().IsEmpty() );
141 BOOST_CHECK_NO_THROW( board.SaveToHistory( mgr.Prj().GetProjectPath(), fileData ) );
142 BOOST_CHECK( fileData.empty() );
143
144 // Detach project before BOARD destruction so design settings ownership unwinds cleanly
145 board.ClearProject();
146 mgr.UnloadProject( &mgr.Prj(), false );
147}
148
149
157BOOST_AUTO_TEST_CASE( NoSnapshotWithoutProjectFile )
158{
159 LIBGIT2_SCOPE libgit;
160
161 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
162 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
163 backupEnabled = true;
164
165 SCOPED_TEMP_DIR notAProject( wxS( "kicad_qa_no_project" ) );
166 const wxString& path = notAProject.Path();
167
168 // Drop a board file in but no .kicad_pro - this mirrors saving a board to /tmp
169 // from standalone pcbnew.
170 wxString boardPath = path + wxFileName::GetPathSeparator() + wxS( "stray.kicad_pcb" );
171 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
172
173 LOCAL_HISTORY history;
174
175 BOOST_CHECK( !history.Init( path ) );
176 BOOST_CHECK( !history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
177 BOOST_CHECK( !history.TagSave( path, wxS( "pcb" ) ) );
178
179 // No .history directory should have been created.
180 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
181 BOOST_CHECK( !wxDirExists( historyDir ) );
182}
183
184
192BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotHandlesSubdirectories )
193{
194 LIBGIT2_SCOPE libgit;
195
196 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
197 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
198 backupEnabled = true;
199
200 SCOPED_TEMP_DIR project( wxS( "kicad_qa_subdirs" ) );
201 const wxString& path = project.Path();
202
203 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pro" ), wxS( "{}\n" ) );
204 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pcb" ),
205 wxS( "(kicad_pcb (version 20240108))\n" ) );
206
207 // Create a subdirectory whose contents will likely be collected before files at the
208 // project root, exercising the subdirectory-first iteration order.
209 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "libs" );
210 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
211 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
212 wxS( "(footprint test)\n" ) );
213
214 LOCAL_HISTORY history;
215 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Initial" ) ) );
216
217 // History must have been created at the project root, not at the subdirectory.
218 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
219 BOOST_CHECK( wxDirExists( historyDir ) );
220 BOOST_CHECK( !wxDirExists( subDir + wxFileName::GetPathSeparator() + wxS( ".history" ) ) );
221
222 wxString headBefore = history.GetHeadHash( path );
223 BOOST_REQUIRE( !headBefore.IsEmpty() );
224
225 // Mutate a file in the subdirectory to ensure the next snapshot has work to do.
226 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
227 wxS( "(footprint test (modified))\n" ) );
228
229 // CommitFullProjectSnapshot must commit using the project root, not derive a wrong
230 // root from the first file in the recursive collection.
231 BOOST_CHECK( history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
232
233 wxString headAfter = history.GetHeadHash( path );
234 BOOST_REQUIRE( !headAfter.IsEmpty() );
235 BOOST_CHECK( headBefore != headAfter );
236}
237
238
239// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24016
240BOOST_AUTO_TEST_CASE( RestoreCommitPreservesZipBackupsDirectory )
241{
242 LIBGIT2_SCOPE libgit;
243
244 // LOCAL_HISTORY early-exits when backups are disabled.
245 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
246 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
247 backupEnabled = true;
248
249 SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_issue24016" ) );
250 const wxString& projectPath = tempProject.Path();
251
252 wxString boardPath =
253 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pcb" );
254 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
255
256 // LOCAL_HISTORY refuses to operate on directories without a project file, so seed
257 // the fixture with a minimal one to mirror a real KiCad project layout.
258 wxString projectFile =
259 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pro" );
260 writeTextFile( projectFile, wxS( "{}\n" ) );
261
262 LOCAL_HISTORY history;
263 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
264
265 wxString headHash = history.GetHeadHash( projectPath );
266 BOOST_REQUIRE( !headHash.IsEmpty() );
267
268 // Mirror SETTINGS_MANAGER::BackupProject output: a sibling "<name>-backups" directory
269 // containing one or more .zip archives.
270 wxString backupsDir =
271 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016-backups" );
272 BOOST_REQUIRE( wxFileName::Mkdir( backupsDir, 0777, wxPATH_MKDIR_FULL ) );
273
274 wxString zipPath = backupsDir + wxFileName::GetPathSeparator()
275 + wxS( "issue24016-2026-04-22_120000.zip" );
276 writeTextFile( zipPath, wxS( "pretend-zip-contents" ) );
277
278 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
279
280 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
281
282 BOOST_CHECK_MESSAGE( wxDirExists( backupsDir ),
283 "zip backups directory must survive RestoreCommit" );
284 BOOST_CHECK_MESSAGE( wxFileExists( zipPath ),
285 ".zip archive inside the backups directory must survive RestoreCommit" );
286}
287
288
302BOOST_AUTO_TEST_CASE( RestoreCommitPreservesNestedProject )
303{
304 LIBGIT2_SCOPE libgit;
305
306 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
307 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
308 backupEnabled = true;
309
310 SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_nested_project" ) );
311 const wxString& projectPath = tempProject.Path();
312
313 // Parent project (projectA): minimal .kicad_pro plus a board file.
314 wxString parentPro = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pro" );
315 wxString parentPcb = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pcb" );
316 writeTextFile( parentPro, wxS( "{}\n" ) );
317 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108))\n" ) );
318
319 LOCAL_HISTORY history;
320 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
321
322 wxString headHash = history.GetHeadHash( projectPath );
323 BOOST_REQUIRE( !headHash.IsEmpty() );
324
325 // Now drop a nested project under projectA/. The user's scenario was that this nested
326 // project was added AFTER the parent's snapshot was committed - so its files are not in
327 // the restored commit, and a naive restore would propose them for deletion.
328 wxString nestedDir = projectPath + wxFileName::GetPathSeparator() + wxS( "projectB" );
329 BOOST_REQUIRE( wxFileName::Mkdir( nestedDir, 0777, wxPATH_MKDIR_FULL ) );
330
331 wxString nestedPro = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pro" );
332 wxString nestedPcb = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pcb" );
333 wxString nestedSch = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_sch" );
334 writeTextFile( nestedPro, wxS( "{ \"nested\": true }\n" ) );
335 writeTextFile( nestedPcb, wxS( "(kicad_pcb (version 20240108) (nested yes))\n" ) );
336 writeTextFile( nestedSch, wxS( "(kicad_sch (version 20240108))\n" ) );
337
338 // Modify the parent board so the restore actually has work to do.
339 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
340
341 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
342
343 // The nested project's directory and every one of its files MUST survive the restore.
344 BOOST_CHECK_MESSAGE( wxDirExists( nestedDir ),
345 "nested project directory must survive RestoreCommit" );
346 BOOST_CHECK_MESSAGE( wxFileExists( nestedPro ),
347 "nested .kicad_pro must survive RestoreCommit" );
348 BOOST_CHECK_MESSAGE( wxFileExists( nestedPcb ),
349 "nested .kicad_pcb must survive RestoreCommit" );
350 BOOST_CHECK_MESSAGE( wxFileExists( nestedSch ),
351 "nested .kicad_sch must survive RestoreCommit" );
352
353 // Parent project files were correctly restored.
354 BOOST_CHECK( wxFileExists( parentPro ) );
355 BOOST_CHECK( wxFileExists( parentPcb ) );
356}
357
358
364BOOST_AUTO_TEST_CASE( RestoreCommitRetainsTimestampedBackup )
365{
366 LIBGIT2_SCOPE libgit;
367
368 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
369 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
370 backupEnabled = true;
371
372 SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_retained_backup" ) );
373 const wxString& projectPath = tempProject.Path();
374
375 wxString boardPath = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pcb" );
376 wxString projectFile = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pro" );
377 writeTextFile( projectFile, wxS( "{}\n" ) );
378 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
379
380 LOCAL_HISTORY history;
381 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
382
383 wxString headHash = history.GetHeadHash( projectPath );
384 BOOST_REQUIRE( !headHash.IsEmpty() );
385
386 // Mutate the board so restore has work to do (and produces a backup).
387 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
388
389 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
390
391 // Backups land at a SIBLING path (aProjectPath + "_restore_backup_<ts>"), so look in
392 // the parent directory of the project. Same convention as the legacy "_restore_backup".
393 wxString parentDir = wxFileName( projectPath ).GetPath();
394 wxString leafPrefix = wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup_" );
395
396 wxDir dir( parentDir );
397 BOOST_REQUIRE( dir.IsOpened() );
398
399 wxString retainedBackup;
400 bool foundLegacyBackup = false;
401
402 wxString name;
403 for( bool cont = dir.GetFirst( &name, wxEmptyString, wxDIR_DIRS ); cont;
404 cont = dir.GetNext( &name ) )
405 {
406 if( name.StartsWith( leafPrefix ) )
407 {
408 retainedBackup = parentDir + wxFileName::GetPathSeparator() + name;
409
410 // No colons - Windows path-safe.
411 BOOST_CHECK_MESSAGE( name.Find( ':' ) == wxNOT_FOUND,
412 "retained backup directory name must not contain ':' "
413 "(Windows-illegal in path components)" );
414 }
415 else if( name == wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup" ) )
416 {
417 foundLegacyBackup = true;
418 }
419 }
420
422 !retainedBackup.IsEmpty(),
423 "RestoreCommit must retain a timestamped _restore_backup_<ts>/ sibling directory" );
425 !foundLegacyBackup,
426 "RestoreCommit must not leave the legacy non-timestamped _restore_backup/ behind" );
427
428 // Clean up the retained backup so the test does not leak files into /tmp.
429 if( !retainedBackup.IsEmpty() && wxDirExists( retainedBackup ) )
430 wxFileName::Rmdir( retainedBackup, wxPATH_RMDIR_RECURSIVE );
431}
432
433
437BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotExcludesNonKiCadFiles )
438{
439 LIBGIT2_SCOPE libgit;
440
441 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
442 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
443 backupEnabled = true;
444
445 SCOPED_TEMP_DIR project( wxS( "kicad_qa_privacy" ) );
446 const wxString& path = project.Path();
447
448 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
449 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
450 wxS( "(kicad_pcb (version 20240108))\n" ) );
451 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_sch" ),
452 wxS( "(kicad_sch (version 20240108))\n" ) );
453
454 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "passwords.txt" ), wxS( "secret\n" ) );
455 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "datasheet.pdf" ), wxS( "fake pdf bytes\n" ) );
456 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "notes.md" ), wxS( "personal notes\n" ) );
457
458 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "docs" );
459 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
460 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "manual.txt" ), wxS( "irrelevant\n" ) );
461
462 LOCAL_HISTORY history;
463 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Close" ) ) );
464
465 wxString hist = path + wxFileName::GetPathSeparator() + wxS( ".history" );
466 git_repository* repo = nullptr;
467 BOOST_REQUIRE_EQUAL( git_repository_open( &repo, hist.mb_str().data() ), 0 );
468
469 git_oid head_oid;
470 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head_oid, repo, "HEAD" ), 0 );
471
472 git_commit* head = nullptr;
473 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &head_oid ), 0 );
474
475 git_tree* tree = nullptr;
476 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), 0 );
477
478 std::vector<std::string> committedPaths;
479 git_tree_walk(
480 tree, GIT_TREEWALK_PRE,
481 []( const char* root, const git_tree_entry* entry, void* payload ) -> int
482 {
483 auto* paths = static_cast<std::vector<std::string>*>( payload );
484
485 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
486 paths->push_back( std::string( root ) + git_tree_entry_name( entry ) );
487
488 return 0;
489 },
490 &committedPaths );
491
492 git_tree_free( tree );
493 git_commit_free( head );
494 git_repository_free( repo );
495
496 auto contains = [&]( const std::string& s )
497 {
498 return std::find( committedPaths.begin(), committedPaths.end(), s ) != committedPaths.end();
499 };
500
501 BOOST_CHECK_MESSAGE( contains( "p.kicad_pro" ), "kicad_pro must be committed" );
502 BOOST_CHECK_MESSAGE( contains( "p.kicad_pcb" ), "kicad_pcb must be committed" );
503 BOOST_CHECK_MESSAGE( contains( "p.kicad_sch" ), "kicad_sch must be committed" );
504
505 BOOST_CHECK_MESSAGE( !contains( "passwords.txt" ), "passwords.txt must NOT appear in history" );
506 BOOST_CHECK_MESSAGE( !contains( "datasheet.pdf" ), "datasheet.pdf must NOT appear in history" );
507 BOOST_CHECK_MESSAGE( !contains( "notes.md" ), "notes.md must NOT appear in history" );
508 BOOST_CHECK_MESSAGE( !contains( "docs/manual.txt" ), "subdirectory user content must NOT appear in history" );
509}
510
511
516BOOST_AUTO_TEST_CASE( FirstAutosaveSkipsCommitWhenStagedMatchesDisk )
517{
518 LIBGIT2_SCOPE libgit;
519
520 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
521 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
522 backupEnabled = true;
523
524 SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_idle_autosave" ) );
525 const wxString& path = project.Path();
526
527 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
528 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
529 wxS( "(kicad_pcb (version 20240108))\n" ) );
530
531 LOCAL_HISTORY history;
532
533 // Mutating this between calls simulates the user editing the in-memory document.
534 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
535
536 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
537 {
538 HISTORY_FILE_DATA entry;
539 entry.relativePath = wxS( "p.kicad_pcb" );
540 entry.content = inMemoryContent;
541 aFileData.push_back( std::move( entry ) );
542 };
543
544 history.RegisterSaver( &history, saver );
545
546 // Saver output matches disk, must skip (autosave path: empty tagFileType).
547 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
548 history.WaitForPendingSave();
549
550 wxString histDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
551 BOOST_CHECK( wxDirExists( histDir ) );
552
553 wxString head = history.GetHeadHash( path );
554 BOOST_CHECK_MESSAGE( head.IsEmpty(), "no untagged HEAD should exist after an idle first save" );
555
556 // Real edit, must commit.
557 inMemoryContent = "(kicad_pcb (version 20240108) (edited yes))\n";
558
559 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
560 history.WaitForPendingSave();
561
562 head = history.GetHeadHash( path );
563 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "in-memory edits diverging from disk must produce a commit" );
564
565 history.UnregisterSaver( &history );
566}
567
568
574BOOST_AUTO_TEST_CASE( FirstManualSaveAlwaysCommitsOnFreshProject )
575{
576 LIBGIT2_SCOPE libgit;
577
578 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
579 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
580 backupEnabled = true;
581
582 SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_manual_save" ) );
583 const wxString& path = project.Path();
584
585 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
586 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
587 wxS( "(kicad_pcb (version 20240108))\n" ) );
588
589 LOCAL_HISTORY history;
590
591 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
592
593 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
594 {
595 HISTORY_FILE_DATA entry;
596 entry.relativePath = wxS( "p.kicad_pcb" );
597 entry.content = inMemoryContent;
598 aFileData.push_back( std::move( entry ) );
599 };
600
601 history.RegisterSaver( &history, saver );
602
603 // Manual save (non-empty tagFileType) on fresh project: commit even when staged matches disk.
604 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Manual Save" ), wxS( "pcb" ) ) );
605
606 wxString head = history.GetHeadHash( path );
607 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "manual save on a fresh project must commit even when staged matches disk" );
608
609 history.UnregisterSaver( &history );
610}
611
612
const char * name
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
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:3967
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
Definition board.cpp:213
const wxString & GetFileName() const
Definition board.h:360
void ClearProject()
Definition board.cpp:254
PROJECT * GetProject() const
Definition board.h:587
AUTO_BACKUP m_Backup
Simple local history manager built on libgit2.
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.
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr)
Restore the project files to the state recorded by the given commit hash.
void WaitForPendingSave()
Block until any pending background save completes.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:541
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:187
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.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
bool enabled
Automatically back up the project when files are saved.
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
std::string content
Serialized content (mutually exclusive with sourcePath)
wxString relativePath
Destination path relative to the project root.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
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_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")