KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_project_file_patch.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 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25
29
30#include <json_common.h>
31
32#include <wx/file.h>
33#include <wx/filename.h>
34#include <wx/stdpaths.h>
35#include <wx/utils.h>
36
37
38using namespace KICAD_DIFF;
39using nlohmann::json;
40
41
42namespace
43{
44wxString patchTempDir()
45{
46 static unsigned counter = 0;
47 return wxStandardPaths::Get().GetTempDir() + wxFILE_SEP_PATH
48 + wxString::Format( wxS( "kicad_patch_%ld_%u" ), static_cast<long>( wxGetProcessId() ),
49 ++counter );
50}
51}
52
53
54BOOST_AUTO_TEST_SUITE( ProjectFilePatch )
55
56
57// Each DOC_PROP that lives in the project file must map to a json pointer.
58BOOST_AUTO_TEST_CASE( DocPropPointerMappingDrcSeverities )
59{
61 BOOST_REQUIRE( p.has_value() );
62 BOOST_CHECK_EQUAL( *p, "/board/design_settings/rule_severities" );
63}
64
65
66BOOST_AUTO_TEST_CASE( DocPropPointerMappingErcSeverities )
67{
69 BOOST_REQUIRE( p.has_value() );
70 BOOST_CHECK_EQUAL( *p, "/erc" );
71}
72
73
74BOOST_AUTO_TEST_CASE( DocPropPointerMappingDrawingSheet )
75{
77 BOOST_REQUIRE( p.has_value() );
78 BOOST_CHECK_EQUAL( *p, "/pcbnew/page_layout_descr_file" );
79}
80
81
82BOOST_AUTO_TEST_CASE( DocPropPointerMappingDrawingSheetSchematic )
83{
85 BOOST_REQUIRE( p.has_value() );
86 BOOST_CHECK_EQUAL( *p, "/schematic/page_layout_descr_file" );
87}
88
89
90BOOST_AUTO_TEST_CASE( DocPropPointerMappingNetClasses )
91{
93 BOOST_REQUIRE( p.has_value() );
94 BOOST_CHECK_EQUAL( *p, "/net_settings" );
95}
96
97
98// DOC_PROPs whose content lives in a sibling file (not .kicad_pro) must NOT
99// have a pointer mapping -- the patch code would silently misroute them
100// otherwise.
101BOOST_AUTO_TEST_CASE( DocPropPointerMappingCustomRulesIsAbsent )
102{
103 BOOST_CHECK( !DocPropJsonPointer( DOC_PROP_CUSTOM_RULES ).has_value() );
104}
105
106
107BOOST_AUTO_TEST_CASE( DocPropPointerMappingFpLibTableIsAbsent )
108{
109 BOOST_CHECK( !DocPropJsonPointer( DOC_PROP_FP_LIB_TABLE ).has_value() );
110}
111
112
113BOOST_AUTO_TEST_CASE( DocPropPointerMappingSymLibTableIsAbsent )
114{
115 BOOST_CHECK( !DocPropJsonPointer( DOC_PROP_SYM_LIB_TABLE ).has_value() );
116}
117
118
119// Patching DRC severities copies just that sub-tree; other fields in target
120// are preserved.
121BOOST_AUTO_TEST_CASE( ApplyPatchPreservesNonTargetFields )
122{
123 json target = {
124 { "board", { { "design_settings", { { "rule_severities", { { "old_key", "warning" } } } } } } },
125 { "text_variables", { { "USER_VAR", "user value" } } },
126 { "schematic", { { "legacy_lib_dir", "/old/path" } } }
127 };
128
129 json source = {
130 { "board", { { "design_settings", { { "rule_severities", { { "new_key", "error" } } } } } } }
131 };
132
134
135 // Patched field: replaced.
136 BOOST_CHECK_EQUAL( target["board"]["design_settings"]["rule_severities"]["new_key"], "error" );
137 BOOST_CHECK( !target["board"]["design_settings"]["rule_severities"].contains( "old_key" ) );
138
139 // Non-patched fields: preserved.
140 BOOST_CHECK_EQUAL( target["text_variables"]["USER_VAR"], "user value" );
141 BOOST_CHECK_EQUAL( target["schematic"]["legacy_lib_dir"], "/old/path" );
142}
143
144
145// Patching a DOC_PROP whose pointer is missing in the source returns false
146// and leaves target untouched.
147BOOST_AUTO_TEST_CASE( ApplyPatchNoOpWhenSourceMissingField )
148{
149 json target = { { "board", { { "design_settings", { { "rule_severities", { { "k", "warning" } } } } } } } };
150 json source = { { "unrelated", "value" } };
151
152 BOOST_CHECK( !ApplyProjectFilePatch( target, source, DOC_PROP_DRC_SEVERITIES ) );
153 BOOST_CHECK_EQUAL( target["board"]["design_settings"]["rule_severities"]["k"], "warning" );
154}
155
156
157// A DOC_PROP not in the mapping table returns false even when the JSON
158// contains a similarly-named field.
159BOOST_AUTO_TEST_CASE( ApplyPatchUnmappedPropertyReturnsFalse )
160{
161 json target = json::object();
162 json source = { { "anything", 1 } };
163
164 BOOST_CHECK( !ApplyProjectFilePatch( target, source, DOC_PROP_CUSTOM_RULES ) );
165}
166
167
168// Patching a nested object (net_settings) copies the whole sub-tree.
169BOOST_AUTO_TEST_CASE( ApplyPatchNetClassesCopiesSubTree )
170{
171 json target = {
172 { "net_settings", { { "classes", json::array() } } },
173 { "schematic", { { "preserved", true } } }
174 };
175
176 json source = {
177 { "net_settings", {
178 { "classes", { { { "name", "HighSpeed" }, { "priority", 1 } } } },
179 { "netclass_patterns", { { { "pattern", "DDR_*" }, { "netclass", "HighSpeed" } } } }
180 } }
181 };
182
184
185 BOOST_CHECK_EQUAL( target["net_settings"]["classes"].size(), 1u );
186 BOOST_CHECK_EQUAL( target["net_settings"]["netclass_patterns"].size(), 1u );
187 BOOST_CHECK_EQUAL( target["schematic"]["preserved"], true );
188}
189
190
191BOOST_AUTO_TEST_CASE( ApplyPatchDrawingSheetSchematicDoesNotTouchPcbnew )
192{
193 json target = {
194 { "pcbnew", { { "page_layout_descr_file", "target-pcb.kicad_wks" } } },
195 { "schematic", { { "page_layout_descr_file", "target-sch.kicad_wks" } } }
196 };
197
198 json source = {
199 { "pcbnew", { { "page_layout_descr_file", "source-pcb.kicad_wks" } } },
200 { "schematic", { { "page_layout_descr_file", "source-sch.kicad_wks" } } }
201 };
202
204
205 BOOST_CHECK_EQUAL( target["schematic"]["page_layout_descr_file"], "source-sch.kicad_wks" );
206 BOOST_CHECK_EQUAL( target["pcbnew"]["page_layout_descr_file"], "target-pcb.kicad_wks" );
207}
208
209
210// File orchestrator: missing output file -> writes source verbatim (with the
211// requested fields treated as identity copies).
212BOOST_AUTO_TEST_CASE( ApplyPatchesNoExistingFileWritesSource )
213{
214 wxString tmpDir = patchTempDir();
215 BOOST_REQUIRE( wxFileName::Mkdir( tmpDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
216
217 const wxString outputPath = tmpDir + wxFILE_SEP_PATH + wxS( "test.kicad_pro" );
218 BOOST_REQUIRE( !wxFileExists( outputPath ) );
219
220 json source = { { "board", { { "design_settings", { { "rule_severities", { { "k", "error" } } } } } } } };
221
222 std::set<wxString> docProps = { DOC_PROP_DRC_SEVERITIES };
223 BOOST_REQUIRE( ApplyProjectFilePatches( outputPath, source, docProps ) );
224
225 BOOST_REQUIRE( wxFileExists( outputPath ) );
226
227 wxFile in( outputPath );
228 wxString content;
229 in.ReadAll( &content );
230 json parsed = json::parse( content.ToStdString() );
231 BOOST_CHECK_EQUAL( parsed["board"]["design_settings"]["rule_severities"]["k"], "error" );
232
233 in.Close();
234 wxRemoveFile( outputPath );
235 wxFileName::Rmdir( tmpDir );
236}
237
238
239BOOST_AUTO_TEST_CASE( ApplyPatchesEmptyPropertySetDoesNotCreateOutput )
240{
241 wxString tmpDir = patchTempDir();
242 BOOST_REQUIRE( wxFileName::Mkdir( tmpDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
243
244 const wxString outputPath = tmpDir + wxFILE_SEP_PATH + wxS( "test.kicad_pro" );
245 BOOST_REQUIRE( !wxFileExists( outputPath ) );
246
247 json source = { { "text_variables", { { "USER_VAR", "source value" } } } };
248 std::set<wxString> docProps;
249
250 BOOST_REQUIRE( ApplyProjectFilePatches( outputPath, source, docProps ) );
251 BOOST_CHECK( !wxFileExists( outputPath ) );
252
253 if( wxFileExists( outputPath ) )
254 wxRemoveFile( outputPath );
255
256 wxFileName::Rmdir( tmpDir );
257}
258
259
260// File orchestrator: existing output file is patched -- only the touched
261// fields are updated, all others preserved verbatim.
262BOOST_AUTO_TEST_CASE( ApplyPatchesPreservesExistingNonTargetFields )
263{
264 wxString tmpDir = patchTempDir();
265 BOOST_REQUIRE( wxFileName::Mkdir( tmpDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
266
267 const wxString outputPath = tmpDir + wxFILE_SEP_PATH + wxS( "test.kicad_pro" );
268
269 json existing = {
270 { "text_variables", { { "USER_VAR", "user value" } } },
271 { "board", { { "design_settings", { { "rule_severities", { { "old", "warning" } } } } } } }
272 };
273
274 {
275 wxFile out;
276 BOOST_REQUIRE( out.Open( outputPath, wxFile::write ) );
277 BOOST_REQUIRE( out.Write( wxString::FromUTF8( existing.dump( 2 ) ) ) );
278 }
279
280 json source = { { "board", { { "design_settings", { { "rule_severities", { { "new", "error" } } } } } } } };
281
282 std::set<wxString> docProps = { DOC_PROP_DRC_SEVERITIES };
283 BOOST_REQUIRE( ApplyProjectFilePatches( outputPath, source, docProps ) );
284
285 wxFile in( outputPath );
286 wxString content;
287 in.ReadAll( &content );
288 json parsed = json::parse( content.ToStdString() );
289
290 BOOST_CHECK_EQUAL( parsed["board"]["design_settings"]["rule_severities"]["new"], "error" );
291 BOOST_CHECK( !parsed["board"]["design_settings"]["rule_severities"].contains( "old" ) );
292 BOOST_CHECK_EQUAL( parsed["text_variables"]["USER_VAR"], "user value" );
293
294 in.Close();
295 wxRemoveFile( outputPath );
296 wxFileName::Rmdir( tmpDir );
297}
298
299
300BOOST_AUTO_TEST_CASE( ApplyPatchesMissingRequestedSourceFieldFails )
301{
302 wxString tmpDir = patchTempDir();
303 BOOST_REQUIRE( wxFileName::Mkdir( tmpDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
304
305 const wxString outputPath = tmpDir + wxFILE_SEP_PATH + wxS( "test.kicad_pro" );
306
307 json existing = { { "text_variables", { { "USER_VAR", "user value" } } } };
308
309 {
310 wxFile out;
311 BOOST_REQUIRE( out.Open( outputPath, wxFile::write ) );
312 BOOST_REQUIRE( out.Write( wxString::FromUTF8( existing.dump( 2 ) ) ) );
313 }
314
315 json source = { { "board", json::object() } };
316 std::set<wxString> docProps = { DOC_PROP_DRC_SEVERITIES };
317
318 BOOST_CHECK( !ApplyProjectFilePatches( outputPath, source, docProps ) );
319
320 wxFile in( outputPath );
321 wxString content;
322 in.ReadAll( &content );
323 json parsed = json::parse( content.ToStdString() );
324 BOOST_CHECK_EQUAL( parsed["text_variables"]["USER_VAR"], "user value" );
325
326 in.Close();
327 wxRemoveFile( outputPath );
328 wxFileName::Rmdir( tmpDir );
329}
330
331
332// Realistically-shaped .kicad_pro: rule_severities is a sibling of other
333// design_settings params under board.design_settings. The surgical patch must
334// touch only rule_severities and leave its siblings byte-identical -- a full
335// SaveProjectCopy fallback (the symptom of a wrong pointer) would clobber them.
336BOOST_AUTO_TEST_CASE( ApplyPatchRealisticProjectShapeIsSurgical )
337{
338 json existing = {
339 { "board", {
340 { "design_settings", {
341 { "rule_severities", { { "copper_edge_clearance", "error" } } },
342 { "rules", { { "min_copper_edge_clearance", 0.5 } } },
343 { "track_widths", json::array( { 0.2, 0.25 } ) }
344 } }
345 } },
346 { "meta", { { "filename", "board.kicad_pro" } } }
347 };
348
349 json source = {
350 { "board", {
351 { "design_settings", {
352 { "rule_severities", { { "copper_edge_clearance", "warning" } } },
353 { "rules", { { "min_copper_edge_clearance", 99.0 } } },
354 { "track_widths", json::array( { 9.0 } ) }
355 } }
356 } }
357 };
358
360
361 // Targeted severity took the source value.
362 BOOST_CHECK_EQUAL( existing["board"]["design_settings"]["rule_severities"]["copper_edge_clearance"],
363 "warning" );
364
365 // Unrelated design_settings siblings are byte-identical to the original,
366 // proving the patch did not fall back to a full copy of the source subtree.
367 BOOST_CHECK_EQUAL( existing["board"]["design_settings"]["rules"]["min_copper_edge_clearance"], 0.5 );
368 BOOST_CHECK_EQUAL( existing["board"]["design_settings"]["track_widths"].dump(),
369 json::array( { 0.2, 0.25 } ).dump() );
370 BOOST_CHECK_EQUAL( existing["meta"]["filename"], "board.kicad_pro" );
371}
372
373
nlohmann::json json
Definition gerbview.cpp:50
std::string source
const wxString DOC_PROP_ERC_SEVERITIES
const wxString DOC_PROP_SYM_LIB_TABLE
const wxString DOC_PROP_NET_CLASSES
bool ApplyProjectFilePatch(nlohmann::json &aTarget, const nlohmann::json &aSource, const wxString &aDocProp, DOC_KIND aKind)
Copy the JSON sub-tree located at the pointer for aDocProp from aSource into aTarget.
const wxString DOC_PROP_CUSTOM_RULES
const wxString DOC_PROP_FP_LIB_TABLE
const wxString DOC_PROP_DRAWING_SHEET
const wxString DOC_PROP_DRC_SEVERITIES
std::optional< std::string > DocPropJsonPointer(const wxString &aDocProp, DOC_KIND aKind)
Return the JSON pointer (RFC 6901, slash-separated) under which aDocProp is persisted in a ....
bool ApplyProjectFilePatches(const wxString &aOutputProPath, const nlohmann::json &aSource, const std::set< wxString > &aDocProps, DOC_KIND aKind)
Higher-level orchestrator: load the existing aOutputProPath as JSON (or start from aSource if the fil...
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(DocPropPointerMappingDrcSeverities)
BOOST_CHECK_EQUAL(result, "25.4")
wxString dump(const wxArrayString &aArray)
Debug helper for printing wxArrayString contents.