Skip to content

Commit ee2d7b4

Browse files
authored
--[Bugfix] - Compare Configuration scalar doubles properly (#2478)
* --remove unnecessary magnum decorator * --add and test support for scalar ConfigValues requiring fuzzy compare This uses the same mechanism that Magnum constructs use, so results are consistent * --separate Configuration fuzzy compare tests; minor updates * --make sure comparisons only involve the number of non-hidden values There is a possibility, albeit remote, that 2 otherwise identical configurations might have a different number of internal-use/hidden values and would appear to be different when they were, in fact, the same. Not anymore. Note, these are only accessible internally (in c++ source) and so the likelihood of this happening is vanishingly small.
1 parent d924c81 commit ee2d7b4

File tree

3 files changed

+147
-29
lines changed

3 files changed

+147
-29
lines changed

Diff for: src/esp/core/Configuration.cpp

+39-21
Original file line numberDiff line numberDiff line change
@@ -207,18 +207,35 @@ bool operator==(const ConfigValue& a, const ConfigValue& b) {
207207
if (a._typeAndFlags != b._typeAndFlags) {
208208
return false;
209209
}
210+
const auto dataType = a.getType();
210211
// Pointer-backed data types need to have _data dereffed
211-
if (isConfigValTypePointerBased(a.getType())) {
212-
return pointerBasedConfigTypeHandlerFor(a.getType())
213-
.comparator(a._data, b._data);
212+
if (isConfigValTypePointerBased(dataType)) {
213+
return pointerBasedConfigTypeHandlerFor(dataType).comparator(a._data,
214+
b._data);
215+
}
216+
217+
// By here we know the type is a trivial type and that the types for both
218+
// values are equal
219+
if (a.reqsFuzzyCompare()) {
220+
// Type is specified to require fuzzy comparison
221+
switch (dataType) {
222+
case ConfigValType::Double: {
223+
return Mn::Math::equal(a.get<double>(), b.get<double>());
224+
}
225+
default: {
226+
CORRADE_ASSERT_UNREACHABLE(
227+
"Unknown/unsupported Type in ConfigValue::operator==()", "");
228+
}
229+
}
214230
}
215231

216-
// Trivial type : a._data holds the actual value
217-
// _data array will always hold only legal data, since a ConfigValue should
218-
// never change type.
232+
// Trivial non-fuzzy-comparison-requiring type : a._data holds the actual
233+
// value _data array will always hold only legal data, since a ConfigValue
234+
// should never change type.
219235
return std::equal(std::begin(a._data), std::end(a._data),
220236
std::begin(b._data));
221-
}
237+
238+
} // ConfigValue::operator==
222239

223240
bool operator!=(const ConfigValue& a, const ConfigValue& b) {
224241
return !(a == b);
@@ -236,7 +253,7 @@ std::string ConfigValue::getAsString() const {
236253
return std::to_string(get<int>());
237254
}
238255
case ConfigValType::Double: {
239-
return std::to_string(get<double>());
256+
return Cr::Utility::formatString("{}", get<double>());
240257
}
241258
case ConfigValType::String: {
242259
return get<std::string>();
@@ -295,7 +312,8 @@ std::string ConfigValue::getAsString() const {
295312

296313
io::JsonGenericValue ConfigValue::writeToJsonObject(
297314
io::JsonAllocator& allocator) const {
298-
// unknown is checked before this function is called, so does not need support
315+
// unknown is checked before this function is called, so does not need
316+
// support
299317
switch (getType()) {
300318
case ConfigValType::Boolean: {
301319
return io::toJsonValue(get<bool>(), allocator);
@@ -487,17 +505,17 @@ int Configuration::loadOneConfigFromJson(int numConfigSettings,
487505
} else {
488506
// The array does not match any currently supported magnum
489507
// objects, so place in indexed subconfig of values.
490-
// decrement count by 1 - the recursive subgroup load will count all the
491-
// values.
508+
// decrement count by 1 - the recursive subgroup load will count all
509+
// the values.
492510
--numConfigSettings;
493511
// create a new subgroup
494512
std::shared_ptr<core::config::Configuration> subGroupPtr =
495513
editSubconfig<core::config::Configuration>(key);
496514
// load array into subconfig
497515
numConfigSettings += subGroupPtr->loadFromJsonArray(jsonObj);
498516
}
499-
// value in array is a number of specified length, else it is a string, an
500-
// object or a nested array
517+
// value in array is a number of specified length, else it is a string,
518+
// an object or a nested array
501519
} else {
502520
// decrement count by 1 - the recursive subgroup load will count all the
503521
// values.
@@ -584,8 +602,8 @@ void Configuration::writeValuesToJson(io::JsonGenericValue& jsonObj,
584602
<< "`, so nothing will be written to JSON for this key.";
585603

586604
} else if (valIter->second.shouldWriteToFile()) {
587-
// Create Generic value for key, using allocator, to make sure its a copy
588-
// and lives long enough
605+
// Create Generic value for key, using allocator, to make sure its a
606+
// copy and lives long enough
589607
writeValueToJsonInternal(valIter->second, valIter->first.c_str(), jsonObj,
590608
allocator);
591609
} else {
@@ -602,8 +620,8 @@ void Configuration::writeSubconfigsToJson(io::JsonGenericValue& jsonObj,
602620
++cfgIter) {
603621
// only save if subconfig tree has value entries
604622
if (cfgIter->second->getConfigTreeNumValues() > 0) {
605-
// Create Generic value for key, using allocator, to make sure its a copy
606-
// and lives long enough
623+
// Create Generic value for key, using allocator, to make sure its a
624+
// copy and lives long enough
607625
io::JsonGenericValue name{cfgIter->first.c_str(), allocator};
608626
io::JsonGenericValue subObj =
609627
cfgIter->second->writeToJsonObject(allocator);
@@ -663,9 +681,9 @@ void Configuration::setSubconfigValsOfTypeInVector(
663681
/**
664682
* @brief Retrieves a shared pointer to a copy of the subConfig @ref
665683
* esp::core::config::Configuration that has the passed @p name . This will
666-
* create a pointer to a new sub-configuration if none exists already with that
667-
* name, but will not add this configuration to this Configuration's internal
668-
* storage.
684+
* create a pointer to a new sub-configuration if none exists already with
685+
* that name, but will not add this configuration to this Configuration's
686+
* internal storage.
669687
*
670688
* @param name The name of the configuration to retrieve.
671689
* @return A pointer to a copy of the configuration having the requested
@@ -866,7 +884,7 @@ Mn::Debug& operator<<(Mn::Debug& debug, const Configuration& cfg) {
866884

867885
bool operator==(const Configuration& a, const Configuration& b) {
868886
if ((a.getNumSubconfigs() != b.getNumSubconfigs()) ||
869-
(a.getNumValues() != b.getNumValues())) {
887+
(a.getNumVisibleValues() != b.getNumVisibleValues())) {
870888
return false;
871889
}
872890
for (const auto& entry : a.configMap_) {

Diff for: src/esp/core/Configuration.h

+75-8
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ enum ConfigValStatus : uint64_t {
133133
* properly read from or written to file otherwise.
134134
*/
135135
isTranslated = 1ULL << 34,
136+
137+
/**
138+
* @brief This @ref ConfigValue requires manual fuzzy comparison (i.e. floating
139+
* point scalar type) using the Magnum::Math::equal method. Magnum types
140+
* already perform fuzzy comparison.
141+
*/
142+
reqsFuzzyComparison = 1ULL << 35,
136143
}; // enum class ConfigValStatus
137144

138145
/**
@@ -157,6 +164,24 @@ constexpr bool isConfigValTypeNonTrivial(ConfigValType type) {
157164
static_cast<int>(ConfigValType::_nonTrivialTypes);
158165
}
159166

167+
/**
168+
* @brief Function template to return whether the value requires fuzzy
169+
* comparison or not.
170+
*/
171+
template <typename T>
172+
constexpr bool useFuzzyComparisonFor() {
173+
// Default for all types is no.
174+
return false;
175+
}
176+
177+
/**
178+
* @brief Specify that @ref ConfigValType::Double scalar floating point values require fuzzy comparison
179+
*/
180+
template <>
181+
constexpr bool useFuzzyComparisonFor<double>() {
182+
return true;
183+
}
184+
160185
/**
161186
* @brief Function template to return type enum for specified type. All
162187
* supported types should have a specialization of this function handling their
@@ -272,8 +297,7 @@ constexpr ConfigValType configValTypeFor<Mn::Rad>() {
272297
/**
273298
* @brief Stream operator to support display of @ref ConfigValType enum tags
274299
*/
275-
MAGNUM_EXPORT Mn::Debug& operator<<(Mn::Debug& debug,
276-
const ConfigValType& value);
300+
Mn::Debug& operator<<(Mn::Debug& debug, const ConfigValType& value);
277301

278302
/**
279303
* @brief This class uses an anonymous tagged union to store values of different
@@ -355,7 +379,8 @@ class ConfigValue {
355379
}
356380

357381
/**
358-
* @brief Get this ConfigVal's value. Type is stored as a Pointer.
382+
* @brief Get this ConfigVal's value. For Types that are stored in _data as a
383+
* Pointer.
359384
*/
360385
template <typename T>
361386
EnableIf<isConfigValTypePointerBased(configValTypeFor<T>()), const T&>
@@ -367,7 +392,8 @@ class ConfigValue {
367392
}
368393

369394
/**
370-
* @brief Get this ConfigVal's value. Type is stored directly in buffer.
395+
* @brief Get this ConfigVal's value. For Types that are stored directly in
396+
* buffer.
371397
*/
372398
template <typename T>
373399
EnableIf<!isConfigValTypePointerBased(configValTypeFor<T>()), const T&>
@@ -413,6 +439,9 @@ class ConfigValue {
413439

414440
//_data should be destructed at this point, construct a new value
415441
setInternalTyped(value);
442+
// set whether this type requires fuzzy comparison or not
443+
setReqsFuzzyCompare(useFuzzyComparisonFor<T>());
444+
416445
} // ConfigValue::setInternal
417446

418447
/**
@@ -621,6 +650,29 @@ class ConfigValue {
621650
setState(ConfigValStatus::isTranslated, isTranslated);
622651
}
623652

653+
/**
654+
* @brief Check whether this ConfigVal requires a fuzzy comparison for
655+
* equality (i.e. for a scalar double).
656+
*
657+
* The comparisons for such a type
658+
* should use Magnum::Math::equal to be consistent with similarly configured
659+
* magnum types.
660+
*/
661+
inline bool reqsFuzzyCompare() const {
662+
return getState(ConfigValStatus::reqsFuzzyComparison);
663+
}
664+
/**
665+
* @brief Check whether this ConfigVal requires a fuzzy comparison for
666+
* equality (i.e. for a scalar double).
667+
*
668+
* The comparisons for such a type
669+
* should use Magnum::Math::equal to be consistent with similarly configured
670+
* magnum types.
671+
*/
672+
inline void setReqsFuzzyCompare(bool fuzzyCompare) {
673+
setState(ConfigValStatus::reqsFuzzyComparison, fuzzyCompare);
674+
}
675+
624676
/**
625677
* @brief Whether or not this @ref ConfigValue should be written to file during
626678
* common execution. The reason we may not want to do this might be that the
@@ -657,7 +709,7 @@ class ConfigValue {
657709
/**
658710
* @brief provide debug stream support for @ref ConfigValue
659711
*/
660-
MAGNUM_EXPORT Mn::Debug& operator<<(Mn::Debug& debug, const ConfigValue& value);
712+
Mn::Debug& operator<<(Mn::Debug& debug, const ConfigValue& value);
661713

662714
/**
663715
* @brief This class holds Configuration data in a map of ConfigValues, and
@@ -1175,10 +1227,26 @@ class Configuration {
11751227
}
11761228

11771229
/**
1178-
* @brief returns number of values in this Configuration.
1230+
* @brief Returns number of values in this Configuration.
11791231
*/
11801232
int getNumValues() const { return valueMap_.size(); }
11811233

1234+
/**
1235+
* @brief Returns number of non-hidden values in this Configuration. This is
1236+
* necessary for determining whether or not configurations are "effectively"
1237+
* equal, where they contain the same data but may vary in number
1238+
* internal-use-only fields.
1239+
*/
1240+
int getNumVisibleValues() const {
1241+
int numVals = 0;
1242+
for (const auto& val : valueMap_) {
1243+
if (!val.second.isHiddenVal()) {
1244+
numVals += 1;
1245+
}
1246+
}
1247+
return numVals;
1248+
}
1249+
11821250
/**
11831251
* @brief Return total number of values held by this Configuration and all
11841252
* its subconfigs.
@@ -1734,8 +1802,7 @@ class Configuration {
17341802
/**
17351803
* @brief provide debug stream support for a @ref Configuration
17361804
*/
1737-
MAGNUM_EXPORT Mn::Debug& operator<<(Mn::Debug& debug,
1738-
const Configuration& value);
1805+
Mn::Debug& operator<<(Mn::Debug& debug, const Configuration& value);
17391806

17401807
template <>
17411808
std::vector<float> Configuration::getSubconfigValsOfTypeInVector(

Diff for: src/tests/ConfigurationTest.cpp

+33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This source code is licensed under the MIT license found in the
33
// LICENSE file in the root directory of this source tree.
44

5+
#include <Corrade/TestSuite/Compare/Numeric.h>
56
#include <Corrade/TestSuite/Tester.h>
67
#include <Corrade/Utility/FormatStl.h>
78
#include "esp/core/Configuration.h"
@@ -76,6 +77,14 @@ struct ConfigurationTest : Cr::TestSuite::Tester {
7677
*/
7778
void TestConfiguration();
7879

80+
/**
81+
* @brief Test Configuration comparisons between value types requiring fuzzy
82+
* logic (i.e. doubles). All additional numeric types added in the future that
83+
* require fuzzy comparison should have their fuzzy equality bounds tested
84+
* here.
85+
*/
86+
void TestConfigurationFuzzyVals();
87+
7988
/**
8089
* @brief Test Configuration find capability. Find returns a list of
8190
* subconfiguration keys required to find a particular key
@@ -103,6 +112,7 @@ struct ConfigurationTest : Cr::TestSuite::Tester {
103112
ConfigurationTest::ConfigurationTest() {
104113
addTests({
105114
&ConfigurationTest::TestConfiguration,
115+
&ConfigurationTest::TestConfigurationFuzzyVals,
106116
&ConfigurationTest::TestSubconfigFind,
107117
&ConfigurationTest::TestSubconfigFindAndMerge,
108118
&ConfigurationTest::TestSubconfigFilter,
@@ -269,6 +279,29 @@ void ConfigurationTest::TestConfiguration() {
269279
CORRADE_COMPARE(cfg.get<std::string>("myString"), "test");
270280
} // ConfigurationTest::TestConfiguration test
271281

282+
void ConfigurationTest::TestConfigurationFuzzyVals() {
283+
Configuration cfg;
284+
285+
// Specify values to test
286+
cfg.set("fuzzyTestVal0", 1.0);
287+
cfg.set("fuzzyTestVal1", 1.0 + Mn::Math::TypeTraits<double>::epsilon() / 2);
288+
// Scale the epsilon to be too big to be seen as the same.
289+
cfg.set("fuzzyTestVal2", 1.0 + Mn::Math::TypeTraits<double>::epsilon() * 4);
290+
291+
CORRADE_VERIFY(cfg.hasValue("fuzzyTestVal0"));
292+
CORRADE_VERIFY(cfg.hasValue("fuzzyTestVal1"));
293+
CORRADE_VERIFY(cfg.hasValue("fuzzyTestVal2"));
294+
// Verify very close doubles are considered sufficiently close by fuzzy
295+
// compare
296+
CORRADE_COMPARE(cfg.get("fuzzyTestVal0"), cfg.get("fuzzyTestVal1"));
297+
298+
// verify very close but not-quite-close enough doubles are considered
299+
// different by magnum's fuzzy compare
300+
CORRADE_COMPARE_AS(cfg.get("fuzzyTestVal0"), cfg.get("fuzzyTestVal2"),
301+
Cr::TestSuite::Compare::NotEqual);
302+
303+
} // ConfigurationTest::TestConfigurationFuzzyVals
304+
272305
// Test configuration find functionality
273306
void ConfigurationTest::TestSubconfigFind() {
274307
Configuration::ptr cfg = Configuration::create();

0 commit comments

Comments
 (0)