Increase the fp precision in dumped spec.

By default, C++ will output 6 fractional digits of floating point
values. This is insufficient for some of the RGG tests that requires
precision < 1e-6.

This CL uses hex representation for fp values in the generated spec
file, while human-readable values are also dumped as a comment.

Fixes: 144788409
Test: NNT_static_fuzzing and inspected the dumped spec
Change-Id: I43ed11e9f73cb387ed8894713fe79361322315ba
diff --git a/tools/test_generator/test_harness/TestHarness.cpp b/tools/test_generator/test_harness/TestHarness.cpp
index f373327..56e1414 100644
--- a/tools/test_generator/test_harness/TestHarness.cpp
+++ b/tools/test_generator/test_harness/TestHarness.cpp
@@ -327,6 +327,21 @@
     return kQuantizedTypes.count(type) > 0;
 }
 
+bool isFloatType(TestOperandType type) {
+    static const std::set<TestOperandType> kFloatTypes = {
+            TestOperandType::TENSOR_FLOAT32,
+            TestOperandType::TENSOR_FLOAT16,
+            TestOperandType::FLOAT32,
+            TestOperandType::FLOAT16,
+    };
+    return kFloatTypes.count(type) > 0;
+}
+
+bool isConstant(TestOperandLifeTime lifetime) {
+    return lifetime == TestOperandLifeTime::CONSTANT_COPY ||
+           lifetime == TestOperandLifeTime::CONSTANT_REFERENCE;
+}
+
 namespace {
 
 const char* kOperationTypeNames[] = {
@@ -492,11 +507,27 @@
 }
 
 template <typename T>
-const auto defaultToStringFunc = [](const T& value) { return std::to_string(value); };
-
+std::string defaultToStringFunc(const T& value) {
+    return std::to_string(value);
+};
 template <>
-const auto defaultToStringFunc<_Float16> =
-        [](const _Float16& value) { return std::to_string(static_cast<float>(value)); };
+std::string defaultToStringFunc<_Float16>(const _Float16& value) {
+    return defaultToStringFunc(static_cast<float>(value));
+};
+
+// Dump floating point values in hex representation.
+template <typename T>
+std::string toHexFloatString(const T& value);
+template <>
+std::string toHexFloatString<float>(const float& value) {
+    std::stringstream ss;
+    ss << "\"" << std::hexfloat << value << "\"";
+    return ss.str();
+};
+template <>
+std::string toHexFloatString<_Float16>(const _Float16& value) {
+    return toHexFloatString(static_cast<float>(value));
+};
 
 template <typename Iterator, class ToStringFunc>
 std::string join(const std::string& joint, Iterator begin, Iterator end, ToStringFunc func) {
@@ -513,9 +544,15 @@
 }
 
 template <typename T>
-void dumpTestBufferToSpecFileHelper(const TestBuffer& buffer, std::ostream& os) {
+void dumpTestBufferToSpecFileHelper(const TestBuffer& buffer, bool useHexFloat, std::ostream& os) {
     const T* data = buffer.get<T>();
     const uint32_t length = buffer.size() / sizeof(T);
+    if constexpr (nnIsFloat<T>) {
+        if (useHexFloat) {
+            os << "from_hex([" << join(", ", data, data + length, toHexFloatString<T>) << "])";
+            return;
+        }
+    }
     os << "[" << join(", ", data, data + length, defaultToStringFunc<T>) << "]";
 }
 
@@ -530,36 +567,36 @@
 }
 
 // Dump a test buffer.
-void SpecDumper::dumpTestBuffer(TestOperandType type, const TestBuffer& buffer) {
+void SpecDumper::dumpTestBuffer(TestOperandType type, const TestBuffer& buffer, bool useHexFloat) {
     switch (type) {
         case TestOperandType::FLOAT32:
         case TestOperandType::TENSOR_FLOAT32:
-            dumpTestBufferToSpecFileHelper<float>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<float>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::INT32:
         case TestOperandType::TENSOR_INT32:
-            dumpTestBufferToSpecFileHelper<int32_t>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<int32_t>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::TENSOR_QUANT8_ASYMM:
-            dumpTestBufferToSpecFileHelper<uint8_t>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<uint8_t>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::TENSOR_QUANT8_SYMM:
         case TestOperandType::TENSOR_QUANT8_ASYMM_SIGNED:
-            dumpTestBufferToSpecFileHelper<int8_t>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<int8_t>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::TENSOR_QUANT16_ASYMM:
-            dumpTestBufferToSpecFileHelper<uint16_t>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<uint16_t>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::TENSOR_QUANT16_SYMM:
-            dumpTestBufferToSpecFileHelper<int16_t>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<int16_t>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::BOOL:
         case TestOperandType::TENSOR_BOOL8:
-            dumpTestBufferToSpecFileHelper<bool8>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<bool8>(buffer, useHexFloat, mOs);
             break;
         case TestOperandType::FLOAT16:
         case TestOperandType::TENSOR_FLOAT16:
-            dumpTestBufferToSpecFileHelper<_Float16>(buffer, mOs);
+            dumpTestBufferToSpecFileHelper<_Float16>(buffer, useHexFloat, mOs);
             break;
         default:
             CHECK(false) << "Unknown type when dumping the buffer";
@@ -571,17 +608,29 @@
         << "\", [\"" << toString(operand.type) << "\", ["
         << join(", ", operand.dimensions, defaultToStringFunc<uint32_t>) << "]";
     if (operand.scale != 0.0f || operand.zeroPoint != 0) {
-        mOs << ", " << operand.scale << ", " << operand.zeroPoint;
+        mOs << ", float.fromhex(" << toHexFloatString(operand.scale) << "), " << operand.zeroPoint;
     }
     mOs << "]";
     if (operand.lifetime == TestOperandLifeTime::CONSTANT_COPY ||
         operand.lifetime == TestOperandLifeTime::CONSTANT_REFERENCE) {
         mOs << ", ";
-        dumpTestBuffer(operand.type, operand.data);
+        dumpTestBuffer(operand.type, operand.data, /*useHexFloat=*/true);
     } else if (operand.lifetime == TestOperandLifeTime::NO_VALUE) {
         mOs << ", value=None";
     }
-    mOs << ")\n";
+    mOs << ")";
+    // For quantized data types, append a human-readable scale at the end.
+    if (operand.scale != 0.0f) {
+        mOs << "  # scale = " << operand.scale;
+    }
+    // For float buffers, append human-readable values at the end.
+    if (isFloatType(operand.type) &&
+        (operand.lifetime == TestOperandLifeTime::CONSTANT_COPY ||
+         operand.lifetime == TestOperandLifeTime::CONSTANT_REFERENCE)) {
+        mOs << "  # ";
+        dumpTestBuffer(operand.type, operand.data, /*useHexFloat=*/false);
+    }
+    mOs << "\n";
 }
 
 void SpecDumper::dumpTestOperation(const TestOperation& operation) {
@@ -593,6 +642,7 @@
 
 void SpecDumper::dumpTestModel() {
     CHECK_EQ(kTestModel.referenced.size(), 0u) << "Subgraphs not supported";
+    mOs << "from_hex = lambda l: [float.fromhex(i) for i in l]\n\n";
 
     // Dump model operands.
     mOs << "# Model operands\n";
@@ -614,8 +664,14 @@
             operand.lifetime != TestOperandLifeTime::SUBGRAPH_OUTPUT) {
             continue;
         }
+        // For float buffers, dump human-readable values as a comment.
+        if (isFloatType(operand.type)) {
+            mOs << "    # op" << i << ": ";
+            dumpTestBuffer(operand.type, operand.data, /*useHexFloat=*/false);
+            mOs << "\n";
+        }
         mOs << "    op" << i << ": ";
-        dumpTestBuffer(operand.type, operand.data);
+        dumpTestBuffer(operand.type, operand.data, /*useHexFloat=*/true);
         mOs << ",\n";
     }
     mOs << "}).DisableLifeTimeVariation()\n";
@@ -627,8 +683,14 @@
     for (uint32_t i = 0; i < results.size(); i++) {
         const uint32_t outputIndex = kTestModel.main.outputIndexes[i];
         const auto& operand = kTestModel.main.operands[outputIndex];
+        // For float buffers, dump human-readable values as a comment.
+        if (isFloatType(operand.type)) {
+            mOs << "    # op" << outputIndex << ": ";
+            dumpTestBuffer(operand.type, results[i], /*useHexFloat=*/false);
+            mOs << "\n";
+        }
         mOs << "    op" << outputIndex << ": ";
-        dumpTestBuffer(operand.type, results[i]);
+        dumpTestBuffer(operand.type, results[i], /*useHexFloat=*/true);
         mOs << ",\n";
     }
     mOs << "}\n";
diff --git a/tools/test_generator/test_harness/include/TestHarness.h b/tools/test_generator/test_harness/include/TestHarness.h
index c67200c..ef2dbc8 100644
--- a/tools/test_generator/test_harness/include/TestHarness.h
+++ b/tools/test_generator/test_harness/include/TestHarness.h
@@ -531,7 +531,10 @@
 
     // Dump a test buffer as a python 1D list.
     // e.g. [1, 2, 3, 4, 5]
-    void dumpTestBuffer(TestOperandType type, const TestBuffer& buffer);
+    //
+    // If useHexFloat is set to true and the operand type is float, the buffer values will be
+    // dumped in hex representation.
+    void dumpTestBuffer(TestOperandType type, const TestBuffer& buffer, bool useHexFloat);
 
     const TestModel& kTestModel;
     std::ostream& mOs;