KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_constraint_serialization.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
20#include <filesystem>
21#include <fstream>
22#include <memory>
23#include <regex>
24#include <sstream>
25
27
28#include <richio.h>
29
30#include <board.h>
31#include <pcb_shape.h>
35
37
38using namespace KI_TEST;
39
40
41namespace
42{
43PCB_CONSTRAINT* findConstraint( BOARD& aBoard, PCB_CONSTRAINT_TYPE aType )
44{
45 for( PCB_CONSTRAINT* c : aBoard.Constraints() )
46 {
47 if( c->GetConstraintType() == aType )
48 return c;
49 }
50
51 return nullptr;
52}
53}
54
55
56BOOST_AUTO_TEST_SUITE( ConstraintSolverSerialization )
57
58
59// A board with two segments and a parallel + fixed-length constraint round-trips: the
60// constraints, their members (uuid + anchor), value and driving flag survive write/reload.
62{
63 auto board1 = std::make_unique<BOARD>();
64
65 PCB_SHAPE* a = addSegment( *board1, { 0, 0 }, { 10 * MM, 0 } );
66 PCB_SHAPE* b = addSegment( *board1, { 0, 5 * MM }, { 8 * MM, 6 * MM } );
67
68 PCB_CONSTRAINT* parallel = new PCB_CONSTRAINT( board1.get(), PCB_CONSTRAINT_TYPE::PARALLEL );
69 parallel->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
70 parallel->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::WHOLE );
71 board1->Add( parallel );
72
73 // A fixed-length constraint carries a length value (in IU, written in mm) and the reference
74 // (non-driving) flag; its two START/END members exercise non-WHOLE anchor round-trip.
78 dim->SetValue( 10.0 * MM );
79 dim->SetDriving( false );
80 board1->Add( dim );
81
82 KIID aId = a->m_Uuid;
83 KIID bId = b->m_Uuid;
84
85 auto path = std::filesystem::temp_directory_path() / "constraint_roundtrip_tst.kicad_pcb";
86 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
87
88 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
89
90 BOOST_REQUIRE_EQUAL( board2->Constraints().size(), 2 );
91
92 PCB_CONSTRAINT* par2 = findConstraint( *board2, PCB_CONSTRAINT_TYPE::PARALLEL );
93 BOOST_REQUIRE( par2 );
94 BOOST_REQUIRE_EQUAL( par2->GetMembers().size(), 2 );
95 BOOST_CHECK( par2->GetMembers()[0].m_item == aId );
96 BOOST_CHECK( par2->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::WHOLE );
97 BOOST_CHECK( par2->GetMembers()[1].m_item == bId );
98 BOOST_CHECK( par2->IsDriving() );
99 BOOST_CHECK( !par2->HasValue() );
100
101 PCB_CONSTRAINT* dim2 = findConstraint( *board2, PCB_CONSTRAINT_TYPE::FIXED_LENGTH );
102 BOOST_REQUIRE( dim2 );
103 BOOST_REQUIRE_EQUAL( dim2->GetMembers().size(), 2 );
104 BOOST_CHECK( dim2->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::START );
105 BOOST_CHECK( dim2->GetMembers()[1].m_anchor == CONSTRAINT_ANCHOR::END );
106 BOOST_CHECK( !dim2->IsDriving() );
107 BOOST_REQUIRE( dim2->HasValue() );
108 BOOST_CHECK_CLOSE( *dim2->GetValue(), 10.0 * MM, 1e-6 ); // length survives the mm round-trip
109}
110
111
112// VERTEX members carry an ordinal that survives the round trip non VERTEX members keep the
113// shorter index less form and reload with member index negative one
114BOOST_AUTO_TEST_CASE( VertexIndexRoundTrip )
115{
116 auto board1 = std::make_unique<BOARD>();
117
118 PCB_SHAPE* a = addSegment( *board1, { 0, 0 }, { 10 * MM, 0 } );
119 PCB_SHAPE* b = addSegment( *board1, { 0, 5 * MM }, { 8 * MM, 6 * MM } );
120
122 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
123 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 7 );
124 board1->Add( c );
125
128 board1->Add( d );
129
130 KIID aId = a->m_Uuid;
131 KIID bId = b->m_Uuid;
132
133 auto path = std::filesystem::temp_directory_path() / "constraint_vertex_tst.kicad_pcb";
134 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
135
136 // Written text carries the index only for VERTEX members a START member keeps the two token
137 // form so the common format stays unchanged
138 std::ifstream in( path );
139 std::stringstream buf;
140 buf << in.rdbuf();
141 std::string text = buf.str();
142
143 std::regex vertexRe( "\\(member\\s+\"" + std::string( aId.AsString().ToUTF8() )
144 + "\"\\s+vertex\\s+1\\s*\\)" );
145 std::regex startWithIndexRe( "\\(member\\s+\"[^\"]+\"\\s+start\\s+[-0-9]" );
146
147 BOOST_CHECK( std::regex_search( text, vertexRe ) );
148 BOOST_CHECK( !std::regex_search( text, startWithIndexRe ) );
149
150 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
151
152 BOOST_REQUIRE_EQUAL( board2->Constraints().size(), 2 );
153
154 PCB_CONSTRAINT* c2 = findConstraint( *board2, PCB_CONSTRAINT_TYPE::COINCIDENT );
155 BOOST_REQUIRE( c2 );
156 BOOST_REQUIRE_EQUAL( c2->GetMembers().size(), 2 );
157 BOOST_CHECK( c2->GetMembers()[0].m_item == aId );
158 BOOST_CHECK( c2->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::VERTEX );
159 BOOST_CHECK_EQUAL( c2->GetMembers()[0].m_index, 1 );
160 BOOST_CHECK( c2->GetMembers()[1].m_item == bId );
161 BOOST_CHECK( c2->GetMembers()[1].m_anchor == CONSTRAINT_ANCHOR::VERTEX );
162 BOOST_CHECK_EQUAL( c2->GetMembers()[1].m_index, 7 );
163
164 PCB_CONSTRAINT* d2 = findConstraint( *board2, PCB_CONSTRAINT_TYPE::HORIZONTAL );
165 BOOST_REQUIRE( d2 );
166 BOOST_REQUIRE_EQUAL( d2->GetMembers().size(), 1 );
167 BOOST_CHECK( d2->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::START );
168 BOOST_CHECK_EQUAL( d2->GetMembers()[0].m_index, -1 ); // absent index token maps back to -1
169}
170
171
172// Index token is legal only on a VERTEX anchor and must be numeric a bad index breaks equality
173// silently on the next save so both are rejected at parse time
174BOOST_AUTO_TEST_CASE( VertexIndexValidation )
175{
176 auto board1 = std::make_unique<BOARD>();
177
178 PCB_SHAPE* a = addSegment( *board1, { 0, 0 }, { 10 * MM, 0 } );
179
181 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::VERTEX, 1 );
182 board1->Add( c );
183
184 auto path = std::filesystem::temp_directory_path() / "constraint_vertex_validation_tst.kicad_pcb";
185 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
186
187 std::ifstream in( path );
188 std::stringstream buf;
189 buf << in.rdbuf();
190 std::string text = buf.str();
191
192 // Rewrite the known-good "vertex 1" member into each variant under test and parse the result.
193 auto parseVariant =
194 [&]( const std::string& aReplacement ) -> std::unique_ptr<BOARD>
195 {
196 std::string variant = std::regex_replace( text, std::regex( "vertex\\s+1" ), aReplacement );
197
198 BOOST_REQUIRE( variant != text ); // guard against the pattern silently not matching
199
200 auto board = std::make_unique<BOARD>();
201
202 STRING_LINE_READER reader( variant, wxT( "constraint_vertex_validation" ) );
203 PCB_IO_KICAD_SEXPR_PARSER parser( &reader, board.get(),
204 []( wxString, int, wxString, wxString ) { return true; } );
205 parser.Parse();
206 return board;
207 };
208
209 // A bare vertex anchor still loads, mapping the absent index back to -1.
210 std::unique_ptr<BOARD> board2 = parseVariant( "vertex" );
211 BOOST_REQUIRE_EQUAL( board2->Constraints().size(), 1 );
212 BOOST_REQUIRE_EQUAL( board2->Constraints().front()->GetMembers().size(), 1 );
213 BOOST_CHECK( board2->Constraints().front()->GetMembers()[0].m_anchor == CONSTRAINT_ANCHOR::VERTEX );
214 BOOST_CHECK_EQUAL( board2->Constraints().front()->GetMembers()[0].m_index, -1 );
215
216 // An index on a non-VERTEX anchor is rejected rather than loaded asymmetrically.
217 BOOST_CHECK_THROW( parseVariant( "start 4" ), IO_ERROR );
218
219 // A non-numeric index is rejected rather than silently becoming 0.
220 BOOST_CHECK_THROW( parseVariant( "vertex garbage" ), IO_ERROR );
221}
222
223
224// A constraint member referencing a missing shape is preserved through a round-trip as a dangling
225// reference (error state), not silently dropped (Zulip "Geometry Constraint Solver", 2026-06-18:
226// a constraint persists in an error state rather than losing the reference).
227BOOST_AUTO_TEST_CASE( MissingShapeMemberPreservedAsError )
228{
229 auto board1 = std::make_unique<BOARD>();
230
231 PCB_SHAPE* a = addSegment( *board1, { 0, 0 }, { 10 * MM, 0 } );
232 KIID danglingId; // a reference to an item that is not on the board
233
236 c->AddMember( danglingId, CONSTRAINT_ANCHOR::START );
237 board1->Add( c );
238
239 auto path = std::filesystem::temp_directory_path() / "constraint_missing_tst.kicad_pcb";
240 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
241
242 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
243
244 BOOST_REQUIRE_EQUAL( board2->Constraints().size(), 1 );
245
246 // Both members survive: the resolvable one and the dangling one (error state).
247 PCB_CONSTRAINT* c2 = board2->Constraints().front();
248 BOOST_REQUIRE_EQUAL( c2->GetMembers().size(), 2 );
249
250 bool hasValid = false, hasDangling = false;
251
252 for( const CONSTRAINT_MEMBER& m : c2->GetMembers() )
253 {
254 if( m.m_item == a->m_Uuid )
255 hasValid = true;
256 else if( m.m_item == danglingId )
257 hasDangling = true;
258 }
259
260 BOOST_CHECK( hasValid );
261 BOOST_CHECK( hasDangling );
262}
263
264
265// Appending a board file into an existing board remaps every uuid; a constraint's members must
266// follow the remap and resolve to the appended copies, not dangle on the originals' old uuids.
267BOOST_AUTO_TEST_CASE( AppendRemapsConstraintMembers )
268{
269 auto board1 = std::make_unique<BOARD>();
270
271 PCB_SHAPE* a = addSegment( *board1, { 0, 0 }, { 10 * MM, 0 } );
272 PCB_SHAPE* b = addSegment( *board1, { 10 * MM, 0 }, { 10 * MM, 10 * MM } );
273
275 c->AddMember( a->m_Uuid, CONSTRAINT_ANCHOR::END );
276 c->AddMember( b->m_Uuid, CONSTRAINT_ANCHOR::START );
277 board1->Add( c );
278
279 KIID aOrigId = a->m_Uuid;
280
281 auto path = std::filesystem::temp_directory_path() / "constraint_append_tst.kicad_pcb";
282 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
283
284 // Append the file into a fresh board, which forces uuid remapping.
285 auto board2 = std::make_unique<BOARD>();
286
287 FILE_LINE_READER reader( path.string() );
288 PCB_IO_KICAD_SEXPR_PARSER parser( &reader, board2.get(),
289 []( wxString, int, wxString, wxString ) { return true; } );
290 parser.Parse();
291
292 BOOST_REQUIRE_EQUAL( board2->Constraints().size(), 1 );
293
294 PCB_CONSTRAINT* c2 = board2->Constraints().front();
295 BOOST_REQUIRE_EQUAL( c2->GetMembers().size(), 2 );
296
297 // Every member resolves to an item that actually lives in the appended board...
298 for( const CONSTRAINT_MEMBER& member : c2->GetMembers() )
299 BOOST_CHECK( board2->ResolveItem( member.m_item, true ) != nullptr );
300
301 // ...and the references were remapped off the originals' uuids.
302 BOOST_CHECK( c2->GetMembers()[0].m_item != aOrigId );
303}
304
305
General utilities for PCB file IO for QA programs.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:465
const KIID m_Uuid
Definition eda_item.h:531
A LINE_READER that reads from an open file.
Definition richio.h:154
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
Definition kiid.h:44
wxString AsString() const
Definition kiid.cpp:256
A geometric constraint between board items (issue #2329).
const std::vector< CONSTRAINT_MEMBER > & GetMembers() const
std::optional< double > GetValue() const
bool IsDriving() const
A driving constraint forces its value; a reference (non-driving) one only measures it.
void AddMember(const KIID &aItem, CONSTRAINT_ANCHOR aAnchor=CONSTRAINT_ANCHOR::WHOLE, int aIndex=-1)
bool HasValue() const
void SetValue(std::optional< double > aValue)
void SetDriving(bool aDriving)
Read a Pcbnew s-expression formatted LINE_READER object and returns the appropriate BOARD_ITEM object...
Is a LINE_READER that reads from a multiline 8 bit wide std::string.
Definition richio.h:222
PCB_SHAPE * addSegment(BOARD &aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
void DumpBoardToFile(BOARD &board, const std::string &aFilename)
Utility function to simply write a Board out to a file.
constexpr int MM
@ VERTEX
An indexed rectangle corner or polygon outline vertex; pairs with CONSTRAINT_MEMBER::m_index.
@ WHOLE
The item as a whole (a segment as a line, a circle).
@ START
First endpoint of a segment or arc.
@ END
Second endpoint of a segment or arc.
PCB_CONSTRAINT_TYPE
The geometric relationship a PCB_CONSTRAINT enforces between its members.
@ COINCIDENT
Two points are made to coincide.
@ HORIZONTAL
A segment (or two points) is horizontal.
@ FIXED_LENGTH
A segment has a driving length value.
@ PARALLEL
Two segments are parallel.
Pcbnew s-expression file format parser definition.
One participant in a constraint: a referenced board item plus the feature of that item that participa...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(RoundTrip)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
BOOST_CHECK_EQUAL(result, "25.4")