andygrove commented on code in PR #6642:
URL: https://github.com/apache/datafusion-comet/pull/6642#discussion_r4261000571


##########
spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala:
##########
@@ -564,56 +596,41 @@ abstract class CometTestBase
   }
 
   /**
-   * A helper function for comparing Comet DataFrame with Spark result using 
absolute tolerance.
+   * Orders rows for a tolerance comparison: field by field, with `float` and 
`double` fields
+   * compared by value and everything else by its string form. Sorting whole 
rows by `toString`,
+   * as the exact comparison does, can put two values that match within 
tolerance at different
+   * positions: `1.2246467991473532E-16` sorts after `0.5`, but `0.0` sorts 
before it. The two
+   * zeros are equal here because the comparison matches them, and NaN sorts 
last.
    */
-  private def checkAnswerWithTolerance(
-      dataFrame: DataFrame,
-      expectedAnswer: Seq[Row],
-      absTol: Double): Unit = {
-    val actualAnswer = dataFrame.collect()
-    require(
-      actualAnswer.length == expectedAnswer.length,
-      s"actual num rows ${actualAnswer.length} != expected num of rows 
${expectedAnswer.length}")
-
-    actualAnswer.zip(expectedAnswer).foreach { case (actualRow, expectedRow) =>
-      checkAnswerWithTolerance(actualRow, expectedRow, absTol)
+  private val toleranceOrdering: Ordering[Row] = new Ordering[Row] {
+    override def compare(x: Row, y: Row): Int =
+      x.toSeq.iterator
+        .zip(y.toSeq.iterator)
+        .map { case (l, r) => compareField(l, r) }
+        .find(_ != 0)
+        .getOrElse(0)
+
+    private def compareField(left: Any, right: Any): Int = (left, right) match 
{
+      case (null, null) => 0
+      case (null, _) => -1
+      case (_, null) => 1
+      case (l: Float, r: Float) => if (l == r) 0 else 
java.lang.Float.compare(l, r)
+      case (l: Double, r: Double) => if (l == r) 0 else 
java.lang.Double.compare(l, r)
+      case _ => left.toString.compareTo(right.toString)
     }
   }
 
-  /**
-   * Compares two answers and makes sure the answer is within absTol of the 
expected result.
-   */
-  private def checkAnswerWithTolerance(
-      actualAnswer: Row,
-      expectedAnswer: Row,
-      absTol: Double): Unit = {
-    require(
-      actualAnswer.length == expectedAnswer.length,
-      s"actual answer length ${actualAnswer.length} != " +
-        s"expected answer length ${expectedAnswer.length}")
-    require(absTol > 0 && absTol <= 1e-6, s"absTol $absTol is out of range (0, 
1e-6]")
-
-    actualAnswer.toSeq.zip(expectedAnswer.toSeq).foreach {
-      case (actual: Float, expected: Float) =>
-        if (actual.isInfinity || expected.isInfinity) {
-          assert(actual.isInfinity == expected.isInfinity, s"actual answer 
$actual != $expected")
-        } else if (!actual.isNaN && !expected.isNaN) {
-          assert(
-            math.abs(actual - expected) < absTol,
-            s"actual answer $actual not within $absTol of correct answer 
$expected")
-        }
-      case (actual: Double, expected: Double) =>
-        if (actual.isInfinity || expected.isInfinity) {
-          assert(actual.isInfinity == expected.isInfinity, s"actual answer 
$actual != $expected")
-        } else if (!actual.isNaN && !expected.isNaN) {
-          assert(
-            math.abs(actual - expected) < absTol,
-            s"actual answer $actual not within $absTol of correct answer 
$expected")
-        }
-      case (actual, expected) =>
-        assert(actual == expected, s"$actualAnswer did not equal 
$expectedAnswer")
+  private def rowsMatch(spark: Row, comet: Row, absTol: Double): Boolean =
+    spark.length == comet.length && spark.toSeq.zip(comet.toSeq).forall {
+      case (s: Float, c: Float) => valuesMatch(s, c, absTol)

Review Comment:
   Fixed in 054ae3978d. Both values are now widened with `.toDouble`. The test 
sources compile with `-Pspark-3.5 -Pstrict-warnings` locally, and putting the 
old call back reproduces the two `implicit numeric widening` errors.
   



##########
spark/src/test/scala/org/apache/spark/sql/CometTestBase.scala:
##########
@@ -497,14 +517,23 @@ abstract class CometTestBase
              |${org.apache.spark.sql.catalyst.util.stackTraceToString(e)}
            """.stripMargin)
       }
-    val preparedSpark = prepareCometAnswer(sparkAnswer, isSorted)
-    val preparedComet = prepareCometAnswer(cometAnswer, isSorted)
-    if (!QueryTest.compare(preparedSpark, preparedComet)) {
+    val ordering =
+      if (absTol.isDefined) toleranceOrdering else Ordering.by[Row, 
String](_.toString)
+    val preparedSpark = prepareCometAnswer(sparkAnswer, isSorted, ordering)
+    val preparedComet = prepareCometAnswer(cometAnswer, isSorted, ordering)
+    val matches = absTol match {
+      case Some(tol) =>
+        preparedSpark.length == preparedComet.length &&
+        preparedSpark.zip(preparedComet).forall { case (s, c) => rowsMatch(s, 
c, tol) }

Review Comment:
   Fixed in 054ae3978d. Unordered rows now pair one-to-one instead of zipping 
the two sorted answers. Rows are first grouped by their fields other than the 
top-level floats, using a key that follows `QueryTest.compare` (maps compare as 
sets of entries, nested floats by their bits), and each group has to pair up on 
its own, so duplicate counts must agree. Within a group the rows are sorted by 
their float values. With a single float field that pairing succeeds whenever 
any pairing exists. With more than one, a group that does not pair in sorted 
order falls back to Kuhn's algorithm, which can reassign earlier pairs, so a 
greedy first match can no longer strand a row.
   
   `CometAnswerToleranceSuite` now covers the `(0.0, "a")` example and its 
all-double variant, the map rows from @rich7420, a case that greedy first-match 
gets wrong, and mismatched duplicate counts. Each of those checks fails if I 
put back the sorted zip, a greedy fallback, an entry-order key for maps or a 
by-value key for nested doubles.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to