Copilot commented on code in PR #8132:
URL: https://github.com/apache/texera/pull/8132#discussion_r3894097081


##########
common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExecSpec.scala:
##########
@@ -779,4 +779,99 @@ class StableMergeSortOpExecSpec extends AnyFlatSpec {
     exec.close()
   }
 
+  // 
===========================================================================
+  // H. Lifecycle guards and unsupported key types
+  // 
===========================================================================
+
+  // The bucket stack is allocated by open(), so both readers of the field 
guard
+  // against being called on an executor the engine has not opened (or has 
already
+  // torn down). Every other test in this spec goes through open() first, 
which is
+  // why these paths need their own cases.
+  //
+  // Honest scope note: debugBucketSizes is itself a test hook (its only 
callers
+  // anywhere in the repo are this spec's getBucketSizes helper and the cases
+  // below), so its null guard is not shipped behavior. What is pinned here is
+  // which field that guard reads and what close() actually does to the stack.
+
+  "debugBucketSizes" should "report no buckets before open and the live stack 
after it" in {
+    val schema = schemaOf("value" -> AttributeType.INTEGER)
+    val desc = new StableMergeSortOpDesc(); desc.keys = 
sortKeysBuffer(sortKey("value"))
+    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))
+    assert(getBucketSizes(exec) == Nil)
+
+    // open() allocates the bucket stack but leaves the sort keys uncompiled —
+    // compileSortKeys runs on the first processTuple — so pushing a bucket
+    // straight through the internal hook reaches a state where the stack is
+    // non-empty while compiledSortKeys is still null. That state is what
+    // distinguishes which of the two fields the Nil guard is reading; a single
+    // size-1 push performs no merge, so no comparison is attempted.
+    exec.open()
+    exec.pushBucketAndCombine(ArrayBuffer(tupleOf(schema, "value" -> 1)))
+    assert(getBucketSizes(exec) == List(1))
+    exec.close()

Review Comment:
   If the assertion fails, `exec.close()` won’t run, which can leave executor 
state/resources allocated and potentially impact later tests in the same JVM. 
Consider structuring the test so `close()` is guaranteed (e.g., `try`/`finally` 
around the post-`open()` section).



##########
common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/sort/StableMergeSortOpExecSpec.scala:
##########
@@ -779,4 +779,99 @@ class StableMergeSortOpExecSpec extends AnyFlatSpec {
     exec.close()
   }
 
+  // 
===========================================================================
+  // H. Lifecycle guards and unsupported key types
+  // 
===========================================================================
+
+  // The bucket stack is allocated by open(), so both readers of the field 
guard
+  // against being called on an executor the engine has not opened (or has 
already
+  // torn down). Every other test in this spec goes through open() first, 
which is
+  // why these paths need their own cases.
+  //
+  // Honest scope note: debugBucketSizes is itself a test hook (its only 
callers
+  // anywhere in the repo are this spec's getBucketSizes helper and the cases
+  // below), so its null guard is not shipped behavior. What is pinned here is
+  // which field that guard reads and what close() actually does to the stack.
+
+  "debugBucketSizes" should "report no buckets before open and the live stack 
after it" in {
+    val schema = schemaOf("value" -> AttributeType.INTEGER)
+    val desc = new StableMergeSortOpDesc(); desc.keys = 
sortKeysBuffer(sortKey("value"))
+    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))
+    assert(getBucketSizes(exec) == Nil)
+
+    // open() allocates the bucket stack but leaves the sort keys uncompiled —
+    // compileSortKeys runs on the first processTuple — so pushing a bucket
+    // straight through the internal hook reaches a state where the stack is
+    // non-empty while compiledSortKeys is still null. That state is what
+    // distinguishes which of the two fields the Nil guard is reading; a single
+    // size-1 push performs no merge, so no comparison is attempted.
+    exec.open()
+    exec.pushBucketAndCombine(ArrayBuffer(tupleOf(schema, "value" -> 1)))
+    assert(getBucketSizes(exec) == List(1))
+    exec.close()
+  }
+
+  "close" should "be a no-op when it runs before open" in {
+    val desc = new StableMergeSortOpDesc(); desc.keys = 
sortKeysBuffer(sortKey("value"))
+    val exec = new StableMergeSortOpExec(objectMapper.writeValueAsString(desc))
+    // Pure "does not throw": the executor was never opened, so this case can 
only
+    // catch a crash in the guard, never a change in what close() does. The
+    // companion case below is the one that pins the effect.
+    exec.close()
+    assert(getBucketSizes(exec) == Nil)
+  }
+
+  it should "drop the buffered buckets when it runs after open" in {
+    val schema = schemaOf("value" -> AttributeType.INTEGER)
+    val desc = new StableMergeSortOpDesc(); desc.keys = 
sortKeysBuffer(sortKey("value"))
+    val exec = new 
StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()
+    List(3, 1, 2).foreach(i => exec.processTuple(tupleOf(schema, "value" -> 
i), 0))
+    assert(getBucketSizes(exec) == List(2, 1))
+    exec.close()
+    assert(getBucketSizes(exec) == Nil)
+  }
+
+  "StableMergeSortOpExec" should "reject a sort key whose attribute type it 
cannot compare" in {
+    // The comparison switch implements 7 of AttributeType's 9 constants; ANY 
and
+    // LARGE_BINARY both fall through to the catch-all, and both are offered as
+    // sort keys by the attribute picker (SortCriteriaUnit.attributeName 
carries a
+    // bare @AutofillAttributeName with no type restriction). Probing both is 
what
+    // makes the arm's breadth a contract rather than a case for one type.
+    def rejects(attrType: AttributeType, first: Any, second: Any): 
IllegalStateException = {
+      val schema = schemaOf("value" -> attrType)
+      val desc = new StableMergeSortOpDesc(); desc.keys = 
sortKeysBuffer(sortKey("value"))
+      val exec = new 
StableMergeSortOpExec(objectMapper.writeValueAsString(desc)); exec.open()
+      // The first tuple only compiles the sort keys. The second makes the 
stack's
+      // top two buckets equal-sized, and the binary carry that merges them is 
the
+      // first comparison of two values.
+      exec.processTuple(tupleOf(schema, "value" -> first), 0)
+      val thrown = intercept[IllegalStateException] {
+        exec.processTuple(tupleOf(schema, "value" -> second), 0)
+      }
+      exec.close()
+      thrown

Review Comment:
   `exec.close()` won’t run if `intercept` fails (e.g., no exception is thrown) 
or if a preceding call/assertion throws, which can leak executor resources and 
make the suite more brittle. Wrap the opened executor usage in `try`/`finally` 
(closing in `finally`) so cleanup happens even on test failure.



-- 
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]

Reply via email to