KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_libeval_compiler.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
21#include <boost/test/data/test_case.hpp>
22
23#include <wx/wx.h>
24
25#include <layer_ids.h>
26#include <gal/color4d.h>
27#include <geometry/eda_angle.h>
29#include <drc/drc_rule.h>
30#include <pcbnew/board.h>
32#include <pcbnew/pcb_shape.h>
33#include <pcbnew/pcb_table.h>
34#include <pcbnew/pcb_track.h>
35#include <pcbnew/footprint.h>
36#include <pcbnew/pcb_text.h>
37#include <pcbnew/zone.h>
39#include <properties/property.h>
41
42BOOST_AUTO_TEST_SUITE( Libeval_Compiler )
43
45{
46 wxString expression;
49
50 friend std::ostream& operator<<( std::ostream& os, const EXPR_TO_TEST& expr )
51 {
52 os << expr.expression;
53 return os;
54 }
55};
56
58
59const static std::vector<EXPR_TO_TEST> simpleExpressions = {
60 { "10mm + 20 mm", false, VAL( 30e6 ) },
61 { "3*(7+8)", false, VAL( 3 * ( 7 + 8 ) ) },
62 { "3*7+8", false, VAL( 3 * 7 + 8 ) },
63 { "(3*7)+8", false, VAL( 3 * 7 + 8 ) },
64 { "10mm + 20)", true, VAL( 0 ) },
65
66 { "1", false, VAL(1) },
67 { "1.5", false, VAL(1.5) },
68 { "1,5", false, VAL(1.5) },
69 { "1mm", false, VAL(1e6) },
70 // Any White-space is OK
71 { " 1 + 2 ", false, VAL(3) },
72 // Decimals are OK in expressions
73 { "1.5 + 0.2 + 0.1", false, VAL(1.8) },
74 // Negatives are OK
75 { "3 - 10", false, VAL(-7) },
76 // Lots of operands
77 { "1 + 2 + 10 + 1000.05", false, VAL(1013.05) },
78 // Operator precedence
79 { "1 + 2 - 4 * 20 / 2", false, VAL(-37) },
80 // Parens
81 { "(1)", false, VAL(1) },
82 // Parens affect precedence
83 { "-(1 + (2 - 4)) * 20.8 / 2", false, VAL(10.4) },
84 // Unary addition is a sign, not a leading operator
85 { "+2 - 1", false, VAL(1) },
86 // A short-circuited || must yield a normalized 1, not the raw (nonzero) left operand, so a
87 // boolean feeding a further operator behaves the same as the non-short-circuited path.
88 { "(2 || 0) == 1", false, VAL(1) },
89 { "(7 || 0) + 5", false, VAL(6) }
90};
91
92
93const static std::vector<EXPR_TO_TEST> introspectionExpressions = {
94 { "A.type == 'Pad' && B.type == 'Pad' && (A.existsOnLayer('F.Cu'))", false, VAL( 0.0 ) },
95 { "A.Width > B.Width", false, VAL( 0.0 ) },
96 { "A.Width + B.Width", false, VAL( pcbIUScale.MilsToIU(10) + pcbIUScale.MilsToIU(20) ) },
97 { "A.Netclass", false, VAL( "HV_LINE" ) },
98 { "A.Net_Class == 'HV_LINE'", false, VAL( 1.0 ) },
99 { "(A.Netclass == 'HV_LINE') && (B.netclass == 'otherClass') && (B.netclass != 'F.Cu')", false, VAL( 1.0 ) },
100 { "A.Netclass + 1.0", false, VAL( 1.0 ) },
101 { "A.hasNetclass('HV_LINE')", false, VAL( 1.0 ) },
102 { "A.hasNetclass('HV_*')", false, VAL( 1.0 ) },
103 { "A.type == 'Track' && B.type == 'Track' && A.layer == 'F.Cu'", false, VAL( 1.0 ) },
104 { "(A.type == 'Track') && (B.type == 'Track') && (A.layer == 'F.Cu')", false, VAL( 1.0 ) },
105 { "A.type == 'Via' && A.isMicroVia()", false, VAL(0.0) }
106};
107
108
109static bool testEvalExpr( const wxString& expr, const LIBEVAL::VALUE& expectedResult,
110 bool expectError = false, BOARD_ITEM* itemA = nullptr,
111 BOARD_ITEM* itemB = nullptr )
112{
113 PCBEXPR_COMPILER compiler( new PCBEXPR_UNIT_RESOLVER() );
114 PCBEXPR_UCODE ucode;
117 bool ok = true;
118
119 context.SetItems( itemA, itemB );
120
121
122 BOOST_TEST_MESSAGE( "Expr: '" << expr.c_str() << "'" );
123
124 bool error = !compiler.Compile( expr, &ucode, &preflightContext );
125
126 BOOST_CHECK_EQUAL( error, expectError );
127
128 if( error != expectError )
129 {
130 BOOST_TEST_MESSAGE( "Result: FAIL: " << compiler.GetError().message.c_str() <<
131 " (code pos: " << compiler.GetError().srcPos << ")" );
132
133 return false;
134 }
135
136 if( error )
137 return true;
138
140
141 if( ok )
142 {
143 result = ucode.Run( &context );
144 ok = ( result->EqualTo( &context, &expectedResult ) );
145 }
146
147 if( expectedResult.GetType() == LIBEVAL::VT_NUMERIC )
148 {
149 BOOST_CHECK_EQUAL( result->AsDouble(), expectedResult.AsDouble() );
150 }
151 else
152 {
153 BOOST_CHECK_EQUAL( result->AsString(), expectedResult.AsString() );
154 }
155
156 return ok;
157}
158
159
160static void expectCompileError( const wxString& aExpr )
161{
162 PCBEXPR_COMPILER compiler( new PCBEXPR_UNIT_RESOLVER() );
163 PCBEXPR_UCODE ucode;
165
166 compiler.Compile( aExpr, &ucode, &preflight );
167
168 BOOST_CHECK_MESSAGE( compiler.IsErrorPending(), "Expected a compile error for: " << aExpr.mb_str() );
169}
170
171
172static void expectCompileSuccess( const wxString& aExpr )
173{
174 PCBEXPR_COMPILER compiler( new PCBEXPR_UNIT_RESOLVER() );
175 PCBEXPR_UCODE ucode;
177
178 compiler.Compile( aExpr, &ucode, &preflight );
179
180 BOOST_CHECK_MESSAGE( !compiler.IsErrorPending(), "Expected expression to compile: " << aExpr.mb_str() );
181}
182
183
184BOOST_DATA_TEST_CASE( SimpleExpressions, boost::unit_test::data::make( simpleExpressions ), expr )
185{
186 testEvalExpr( expr.expression, expr.expectedResult, expr.expectError );
187}
188
189
190BOOST_AUTO_TEST_CASE( IntrospectedProperties )
191{
193 propMgr.Rebuild();
194
195 BOARD brd;
196
197 const NETINFO_LIST& netInfo = brd.GetNetInfo();
198
199 std::shared_ptr<NETCLASS> netclass1( new NETCLASS( "HV_LINE" ) );
200 std::shared_ptr<NETCLASS> netclass2( new NETCLASS( "otherClass" ) );
201
202 auto net1info = new NETINFO_ITEM( &brd, "net1", 1 );
203 auto net2info = new NETINFO_ITEM( &brd, "net2", 2 );
204
205 net1info->SetNetClass( netclass1 );
206 net2info->SetNetClass( netclass2 );
207
208 PCB_TRACK trackA( &brd );
209 PCB_TRACK trackB( &brd );
210
211 trackA.SetNet( net1info );
212 trackB.SetNet( net2info );
213
214 trackB.SetLayer( F_Cu );
215
216 trackA.SetWidth( pcbIUScale.MilsToIU( 10 ) );
217 trackB.SetWidth( pcbIUScale.MilsToIU( 20 ) );
218
219 for( const auto& expr : introspectionExpressions )
220 {
221 testEvalExpr( expr.expression, expr.expectedResult, expr.expectError, &trackA, &trackB );
222 }
223}
224
225BOOST_AUTO_TEST_CASE( IntrospectedExtendedNumericProperties )
226{
228 propMgr.Rebuild();
229
230 BOARD brd;
231 ZONE zone( &brd );
232
233 zone.SetLayer( F_Cu );
234 zone.SetAssignedPriority( 7 );
235 zone.SetHatchOrientation( EDA_ANGLE( 45.0, DEGREES_T ) );
236 zone.SetMinIslandArea( 123456789LL );
237
238 testEvalExpr( wxT( "A.Priority == 7" ), VAL( 1.0 ), false, &zone );
239 testEvalExpr( wxT( "A.Hatch_Orientation == 45deg" ), VAL( 1.0 ), false, &zone );
240 testEvalExpr( wxT( "A.Minimum_Island_Area == 123456789" ), VAL( 1.0 ), false, &zone );
241
242 PCB_SHAPE ellipse( &brd, SHAPE_T::ELLIPSE );
243 ellipse.SetEllipseRotation( EDA_ANGLE( 30.0, DEGREES_T ) );
244
245 testEvalExpr( wxT( "A.Ellipse_Rotation == 30deg" ), VAL( 1.0 ), false, &ellipse );
246
247 FOOTPRINT footprint( &brd );
248 footprint.SetLocalSolderPasteMarginRatio( 0.125 );
249
250 testEvalExpr( wxT( "A.Solderpaste_Margin_Ratio_Override == 0.125" ), VAL( 1.0 ), false,
251 &footprint );
252
253 footprint.SetLocalSolderPasteMarginRatio( std::nullopt );
254
255 testEvalExpr( wxT( "A.Solderpaste_Margin_Ratio_Override == null" ), VAL( 1.0 ), false,
256 &footprint );
257}
258
259BOOST_AUTO_TEST_CASE( IntrospectedColorProperties )
260{
262 propMgr.Rebuild();
263
264 BOARD brd;
265 PCB_TABLE table( &brd, 0 );
266
267 const auto checkColor = [&]( const COLOR4D& aColor, const wxString& aExpectedCss )
268 {
269 table.SetBorderColor( aColor );
270 BOOST_CHECK_EQUAL( aColor.ToCSSString(), aExpectedCss );
271
272 wxString expression = wxT( "A.Border_Color == '" ) + aExpectedCss + wxT( "'" );
273 testEvalExpr( expression, VAL( 1.0 ), false, &table );
274 };
275
276 checkColor( COLOR4D( 0.25, 0.5, 0.75, 0.5 ), wxT( "rgba(64, 128, 191, 0.502)" ) );
277 checkColor( COLOR4D( wxString( wxT( "red" ) ) ), wxT( "rgb(255, 0, 0)" ) );
278}
279
280BOOST_AUTO_TEST_CASE( ExpressionPropertyTypesAreSupported )
281{
283 propMgr.Rebuild();
284
285 std::set<std::pair<TYPE_ID, PROPERTY_BASE*>> seen;
286 std::map<wxString, std::set<LIBEVAL::VAR_TYPE_T>> fieldTypes;
287
288 for( const PROPERTY_MANAGER::CLASS_INFO& cls : propMgr.GetAllClasses() )
289 {
290 if( !propMgr.IsOfType( cls.type, TYPE_HASH( BOARD_ITEM ) ) )
291 continue;
292
293 for( PROPERTY_BASE* prop : cls.properties )
294 {
295 if( !seen.emplace( cls.type, prop ).second )
296 continue;
297
299
300 BOOST_CHECK_MESSAGE( kind != PCBEXPR_PROPERTY_KIND::UNSUPPORTED,
301 "Unsupported expression property: class=" << cls.name.mb_str()
302 << ", property=" << prop->Name().mb_str()
303 << ", type_hash=" << prop->TypeHash() );
304
306 fieldTypes[prop->Name()].insert( PCBEXPR_VAR_REF::ExpressionType( kind ) );
307 }
308 }
309
310 // PCB_TARGET::Shape is numeric while EDA_SHAPE::Shape is an enum, so a global
311 // expression reference cannot have one stable public type.
312 auto shapeTypes = fieldTypes.find( wxT( "Shape" ) );
313 BOOST_REQUIRE( shapeTypes != fieldTypes.end() );
314 BOOST_CHECK_EQUAL( shapeTypes->second.size(), 2u );
315 fieldTypes.erase( shapeTypes );
316
317 for( const auto& [field, types] : fieldTypes )
318 {
319 BOOST_CHECK_MESSAGE( types.size() == 1,
320 "Incompatible expression types for property: " << field.mb_str() );
321 }
322
323 expectCompileError( wxT( "A.Shape" ) );
324}
325
326BOOST_AUTO_TEST_CASE( IntrospectedPropertyAvailability )
327{
329 propMgr.Rebuild();
330
331 BOARD brd;
332 ZONE zone( &brd );
333
334 zone.SetLayer( F_Cu );
335 zone.SetAssignedPriority( 7 );
336 zone.SetIsRuleArea( true );
337
338 testEvalExpr( wxT( "A.Priority == 7" ), VAL( 0.0 ), false, &zone );
339
340 zone.SetIsRuleArea( false );
341
342 testEvalExpr( wxT( "A.Priority == 7" ), VAL( 1.0 ), false, &zone );
343}
344
345BOOST_AUTO_TEST_CASE( InNetChainClassWildcard )
346{
348 propMgr.Rebuild();
349
350 BOARD brd;
351
352 std::shared_ptr<NET_SETTINGS> netSettings = brd.GetDesignSettings().m_NetSettings;
353 netSettings->SetNetChainClass( wxT( "ChainHS" ), wxT( "HighSpeed" ) );
354
355 auto netUnclassified = new NETINFO_ITEM( &brd, "netA", 1 );
356 auto netClassified = new NETINFO_ITEM( &brd, "netB", 2 );
357 auto netNoChain = new NETINFO_ITEM( &brd, "netC", 3 );
358
359 netUnclassified->SetNetChain( wxT( "ChainOrphan" ) );
360 netClassified->SetNetChain( wxT( "ChainHS" ) );
361
362 PCB_TRACK trackUnclassified( &brd );
363 PCB_TRACK trackClassified( &brd );
364 PCB_TRACK trackNoChain( &brd );
365
366 trackUnclassified.SetNet( netUnclassified );
367 trackClassified.SetNet( netClassified );
368 trackNoChain.SetNet( netNoChain );
369
370 // A chain with no class assignment must not match any inNetChainClass() pattern,
371 // including the '*' wildcard.
372 testEvalExpr( wxT( "A.inNetChainClass('*')" ), VAL( 0.0 ), false, &trackUnclassified,
373 &trackUnclassified );
374 testEvalExpr( wxT( "A.inNetChainClass('HighSpeed')" ), VAL( 0.0 ), false, &trackUnclassified,
375 &trackUnclassified );
376
377 // Net with no chain at all must not match either.
378 testEvalExpr( wxT( "A.inNetChainClass('*')" ), VAL( 0.0 ), false, &trackNoChain,
379 &trackNoChain );
380
381 // Properly classified chain must match both wildcard and exact patterns.
382 testEvalExpr( wxT( "A.inNetChainClass('*')" ), VAL( 1.0 ), false, &trackClassified,
383 &trackClassified );
384 testEvalExpr( wxT( "A.inNetChainClass('HighSpeed')" ), VAL( 1.0 ), false, &trackClassified,
385 &trackClassified );
386 testEvalExpr( wxT( "A.inNetChainClass('High*')" ), VAL( 1.0 ), false, &trackClassified,
387 &trackClassified );
388 testEvalExpr( wxT( "A.inNetChainClass('LowSpeed')" ), VAL( 0.0 ), false, &trackClassified,
389 &trackClassified );
390}
391
392BOOST_AUTO_TEST_CASE( ParentNavigation )
393{
395 propMgr.Rebuild();
396
397 BOARD brd;
398
399 FOOTPRINT fp( &brd );
400 fp.SetReference( wxT( "J1" ) );
401
402 // A text item living inside the footprint. Its direct parent is the footprint, so
403 // "A.Parent" navigates to J1.
404 PCB_TEXT* text = new PCB_TEXT( &fp );
405 text->SetText( wxT( "J1" ) );
406 fp.Add( text );
407
408 // The headline capability: getField() only returns a value when its receiver is a
409 // footprint, so this passes solely because navigation steps from the text to its parent.
410 testEvalExpr( wxT( "A.Parent.getField('Reference') == 'J1'" ), VAL( 1.0 ), false, text, text );
411
412 // The exact pattern from the issue (parent field compared to the text's own value).
413 testEvalExpr( wxT( "A.Parent.getField('Reference') == A.Text" ), VAL( 1.0 ), false, text, text );
414
415 // The parent resolves to a footprint object that can be type-queried.
416 testEvalExpr( wxT( "A.Parent.Type == 'Footprint'" ), VAL( 1.0 ), false, text, text );
417
418 // Regression: the terminal "Parent" string still yields the parent footprint reference,
419 // i.e. the object and the string refer to the same parent.
420 testEvalExpr( wxT( "A.Parent == 'J1'" ), VAL( 1.0 ), false, text, text );
421
422 // Without navigation getField() has a non-footprint receiver and returns "", so this must
423 // NOT match - it guards against the navigation silently applying to the base item.
424 testEvalExpr( wxT( "A.getField('Reference') == 'J1'" ), VAL( 0.0 ), false, text, text );
425
426 // Error cases. These are code-generation errors (not parse errors), which the shared
427 // testEvalExpr does not observe through Compile()'s return value, so check the pending-error
428 // status directly. None of these expressions contain a bare number, so the only error that
429 // can be raised is the unrecognized item/property we are testing for.
430 // An unknown property on the parent, an unknown navigation step, and navigation on the
431 // layer pseudo-item (which has no parent).
432 expectCompileError( wxT( "A.Parent.bogusProperty" ) );
433 expectCompileError( wxT( "A.bogus.Reference == 'J1'" ) );
434 expectCompileError( wxT( "L.Parent.Type == 'Footprint'" ) );
435}
436
437
438BOOST_AUTO_TEST_CASE( ReceiverValidation )
439{
440 expectCompileSuccess( wxT( "L == 'F.Cu'" ) );
441 expectCompileSuccess( wxT( "AB.isCoupledDiffPair()" ) );
442 expectCompileSuccess( wxT( "A.Parent.Reference == 'J1'" ) );
443 expectCompileSuccess( wxT( "A.Width == 1mm" ) );
444
445 expectCompileError( wxT( "L.Width == 1mm" ) );
446 expectCompileError( wxT( "AB.Width == A.Width" ) );
447 expectCompileError( wxT( "AB.Parent.Reference == 'J1'" ) );
448}
449
450
451BOOST_AUTO_TEST_CASE( PairItemDependency )
452{
453 auto requiresPairItems =
454 []( const wxString& aExpr )
455 {
456 PCBEXPR_COMPILER compiler( new PCBEXPR_UNIT_RESOLVER() );
457 PCBEXPR_UCODE ucode;
459
460 BOOST_REQUIRE( compiler.Compile( aExpr, &ucode, &preflight ) );
461 return ucode.RequiresPairItems();
462 };
463
464 BOOST_CHECK( !requiresPairItems( wxT( "A.Width == 1mm" ) ) );
465 BOOST_CHECK( requiresPairItems( wxT( "B.Width == 1mm" ) ) );
466 BOOST_CHECK( requiresPairItems( wxT( "AB.isCoupledDiffPair()" ) ) );
467 BOOST_CHECK( !requiresPairItems( wxT( "A.Reference == 'B.Width'" ) ) );
468}
469
470
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
virtual void SetNet(NETINFO_ITEM *aNetInfo)
Set a NET_INFO object for the item.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
std::shared_ptr< NET_SETTINGS > m_NetSettings
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1098
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1158
void SetLocalSolderPasteMarginRatio(std::optional< double > aRatio)
Definition footprint.h:489
void SetReference(const wxString &aReference)
Definition footprint.h:863
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
wxString ToCSSString() const
Definition color4d.cpp:146
bool IsErrorPending() const
const ERROR_STATUS & GetError() const
bool Compile(const wxString &aString, UCODE *aCode, CONTEXT *aPreflightContext)
VALUE * Run(CONTEXT *ctx)
virtual const wxString & AsString() const
virtual double AsDouble() const
VAR_TYPE_T GetType() const
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:38
Handle the data for a net.
Definition netinfo.h:46
Container for NETINFO_ITEM elements, which are the nets.
Definition netinfo.h:221
void SetItems(BOARD_ITEM *a, BOARD_ITEM *b=nullptr)
bool RequiresPairItems() const
static PCBEXPR_PROPERTY_KIND ClassifyProperty(const PROPERTY_BASE *aProperty)
static LIBEVAL::VAR_TYPE_T ExpressionType(PCBEXPR_PROPERTY_KIND aKind)
void SetEllipseRotation(const EDA_ANGLE &aA) override
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
virtual size_t TypeHash() const =0
Return type-id of the property type.
const wxString & Name() const
Definition property.h:220
Provide class metadata.Helper macro to map type hashes to names.
CLASSES_INFO GetAllClasses()
static PROPERTY_MANAGER & Instance()
void Rebuild()
Rebuild the list of all registered properties.
bool IsOfType(TYPE_ID aDerived, TYPE_ID aBase) const
Return true if aDerived is inherited from aBase.
Handle a list of polygons defining a copper zone.
Definition zone.h:70
void SetHatchOrientation(const EDA_ANGLE &aStep)
Definition zone.h:332
virtual void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Definition zone.cpp:619
void SetIsRuleArea(bool aEnable)
Definition zone.h:814
void SetAssignedPriority(unsigned aPriority)
Definition zone.h:117
void SetMinIslandArea(long long int aArea)
Definition zone.h:839
@ NULL_CONSTRAINT
Definition drc_rule.h:50
@ DEGREES_T
Definition eda_angle.h:31
@ ELLIPSE
Definition eda_shape.h:52
@ UNDEFINED_LAYER
Definition layer_ids.h:57
@ F_Cu
Definition layer_ids.h:60
PCBEXPR_PROPERTY_KIND
#define TYPE_HASH(x)
Definition property.h:74
friend std::ostream & operator<<(std::ostream &os, const EXPR_TO_TEST &expr)
LIBEVAL::VALUE expectedResult
BOOST_DATA_TEST_CASE(ConvertToKicadUnit, boost::unit_test::data::make(altium_to_kicad_unit), input_value, expected_result)
Test conversation from Altium internal units into KiCad internal units.
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
static void expectCompileError(const wxString &aExpr)
static void expectCompileSuccess(const wxString &aExpr)
static const std::vector< EXPR_TO_TEST > simpleExpressions
static bool testEvalExpr(const wxString &expr, const LIBEVAL::VALUE &expectedResult, bool expectError=false, BOARD_ITEM *itemA=nullptr, BOARD_ITEM *itemB=nullptr)
LIBEVAL::VALUE VAL
static const std::vector< EXPR_TO_TEST > introspectionExpressions
BOOST_AUTO_TEST_CASE(IntrospectedProperties)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")