This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 4722a42c01 test(workflow-core): cover PortIdentity key serde (#5799)
4722a42c01 is described below

commit 4722a42c019a35790134e660d9becc6ea069205f
Author: Tanishq Gandhi <[email protected]>
AuthorDate: Fri Jun 19 23:24:42 2026 -0700

    test(workflow-core): cover PortIdentity key serde (#5799)
    
    ### What changes were proposed in this PR?
    
    Add dedicated unit specs for `PortIdentity` JSON map-key serde:
    
    | Spec | Coverage |
    | --- | --- |
    | `PortIdentityKeySerializerSpec` | Pins the exact `id_internal` key
    format and Jackson map-key serialization |
    | `PortIdentityKeyDeserializerSpec` | Pins valid key parsing,
    serializer/deserializer round-trips, Jackson map round-trip, and current
    malformed-key failure behavior |
    
    ### Any related issues, documentation, discussions?
    
    Closes #5776
    
    ### How was this PR tested?
    
    Added unit tests and ran:
    
    ```bash
    sbt "project WorkflowCore" "testOnly 
org.apache.texera.amber.util.serde.PortIdentityKeySerializerSpec 
org.apache.texera.amber.util.serde.PortIdentityKeyDeserializerSpec"
    ```
    Result: all 12 tests passed.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-5)
---
 .../serde/PortIdentityKeyDeserializerSpec.scala    | 129 ++++++++++++++++++++
 .../util/serde/PortIdentityKeySerializerSpec.scala |  63 ++++++++++
 .../amber/util/serde/PortIdentitySerdeSpec.scala   | 132 ---------------------
 3 files changed, 192 insertions(+), 132 deletions(-)

diff --git 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeyDeserializerSpec.scala
 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeyDeserializerSpec.scala
new file mode 100644
index 0000000000..427239aa2c
--- /dev/null
+++ 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeyDeserializerSpec.scala
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.util.serde
+
+import org.apache.texera.amber.core.workflow.PortIdentity
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import scala.jdk.CollectionConverters._
+
+class PortIdentityKeyDeserializerSpec extends AnyFlatSpec with Matchers {
+
+  private val deserializer = new PortIdentityKeyDeserializer
+
+  "PortIdentityKeyDeserializer.deserializeKey" should "parse id_internal keys 
into PortIdentity" in {
+    deserializer.deserializeKey("3_false", null) shouldBe PortIdentity(3, 
internal = false)
+    deserializer.deserializeKey("0_true", null) shouldBe PortIdentity(0, 
internal = true)
+  }
+
+  it should "round-trip serializer output for representative PortIdentity 
values" in {
+    val cases = Seq(
+      PortIdentity(0, internal = false),
+      PortIdentity(0, internal = true),
+      PortIdentity(999999, internal = false),
+      PortIdentity(999999, internal = true),
+      PortIdentity(-1, internal = false),
+      PortIdentity(-1, internal = true)
+    )
+
+    cases.foreach { portIdentity =>
+      deserializer.deserializeKey(
+        PortIdentityKeySerializer.portIdToString(portIdentity),
+        null
+      ) shouldBe portIdentity
+    }
+  }
+
+  it should "round-trip Map[PortIdentity, String] keys through 
JSONUtils.objectMapper" in {
+    val original = Map(
+      PortIdentity(0, internal = false) -> "zero-external",
+      PortIdentity(3, internal = true) -> "three-internal",
+      PortIdentity(999999, internal = false) -> "large-external",
+      PortIdentity(-1, internal = true) -> "negative-internal"
+    )
+    val json = objectMapper.writeValueAsString(original)
+    val mapType = objectMapper.getTypeFactory
+      .constructMapType(classOf[java.util.HashMap[_, _]], 
classOf[PortIdentity], classOf[String])
+
+    val restored: java.util.Map[PortIdentity, String] = 
objectMapper.readValue(json, mapType)
+
+    restored.asScala.toMap shouldBe original
+  }
+
+  it should "round-trip an empty Map[PortIdentity, String] through 
JSONUtils.objectMapper" in {
+    val original = Map.empty[PortIdentity, String]
+    val json = objectMapper.writeValueAsString(original)
+    val mapType = objectMapper.getTypeFactory
+      .constructMapType(classOf[java.util.HashMap[_, _]], 
classOf[PortIdentity], classOf[String])
+
+    val restored: java.util.Map[PortIdentity, String] = 
objectMapper.readValue(json, mapType)
+
+    restored.asScala.toMap shouldBe original
+  }
+
+  it should "throw NumberFormatException for a non-integer id" in {
+    intercept[NumberFormatException] {
+      deserializer.deserializeKey("notAnInt_false", null)
+    }
+  }
+
+  it should "throw IllegalArgumentException for a non-boolean internal flag" 
in {
+    intercept[IllegalArgumentException] {
+      deserializer.deserializeKey("0_notABool", null)
+    }
+  }
+
+  it should "throw NumberFormatException when a separator-less key is 
non-numeric" in {
+    intercept[NumberFormatException] {
+      deserializer.deserializeKey("missingSeparator", null)
+    }
+  }
+
+  it should "throw NumberFormatException for an empty key" in {
+    intercept[NumberFormatException] {
+      deserializer.deserializeKey("", null)
+    }
+  }
+
+  it should "throw ArrayIndexOutOfBoundsException when only the id is 
provided" in {
+    intercept[ArrayIndexOutOfBoundsException] {
+      deserializer.deserializeKey("5", null)
+    }
+  }
+
+  it should "ignore extra trailing underscore-separated segments" in {
+    deserializer.deserializeKey("1_true_extra", null) shouldBe PortIdentity(1, 
internal = true)
+  }
+
+  it should "eventually reject keys with extra trailing segments 
(pendingUntilFixed)" in pendingUntilFixed {
+    // Documented contract: a `PortIdentityKeySerializer` output is exactly
+    // `id_internal` — two underscore-separated segments. Anything else is
+    // corrupt JSON and should be rejected, not silently truncated. The
+    // current implementation is lenient (see characterization test above);
+    // this pendingUntilFixed flips to passing once the parser is hardened,
+    // then `pendingUntilFixed` inverts that into a deliberate failure forcing
+    // the marker to be removed.
+    intercept[IllegalArgumentException] {
+      deserializer.deserializeKey("1_true_extra", null)
+    }
+  }
+}
diff --git 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeySerializerSpec.scala
 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeySerializerSpec.scala
new file mode 100644
index 0000000000..31ba71c3c6
--- /dev/null
+++ 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentityKeySerializerSpec.scala
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.util.serde
+
+import org.apache.texera.amber.core.workflow.PortIdentity
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class PortIdentityKeySerializerSpec extends AnyFlatSpec with Matchers {
+
+  "PortIdentityKeySerializer.portIdToString" should "format PortIdentity as 
id_internal" in {
+    PortIdentityKeySerializer
+      .portIdToString(PortIdentity(3, internal = false)) shouldBe "3_false"
+    PortIdentityKeySerializer
+      .portIdToString(PortIdentity(0, internal = true)) shouldBe "0_true"
+  }
+
+  it should "preserve zero, large, negative, internal, and external ports in 
the key string" in {
+    val cases = Seq(
+      PortIdentity(0, internal = false) -> "0_false",
+      PortIdentity(0, internal = true) -> "0_true",
+      PortIdentity(999999, internal = false) -> "999999_false",
+      PortIdentity(999999, internal = true) -> "999999_true",
+      PortIdentity(-1, internal = false) -> "-1_false",
+      PortIdentity(-1, internal = true) -> "-1_true"
+    )
+
+    cases.foreach {
+      case (portIdentity, expected) =>
+        PortIdentityKeySerializer.portIdToString(portIdentity) shouldBe 
expected
+    }
+  }
+
+  "PortIdentityKeySerializer" should "write Jackson map keys with the 
id_internal format" in {
+    val original = Map(
+      PortIdentity(3, internal = false) -> "external",
+      PortIdentity(0, internal = true) -> "internal"
+    )
+
+    val json = objectMapper.writeValueAsString(original)
+
+    json should include(""""3_false"""")
+    json should include(""""0_true"""")
+  }
+}
diff --git 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentitySerdeSpec.scala
 
b/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentitySerdeSpec.scala
deleted file mode 100644
index e7f6ad037b..0000000000
--- 
a/common/workflow-core/src/test/scala/org/apache/texera/amber/util/serde/PortIdentitySerdeSpec.scala
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.amber.util.serde
-
-import org.apache.texera.amber.core.workflow.PortIdentity
-import org.apache.texera.amber.util.JSONUtils.objectMapper
-import org.scalatest.flatspec.AnyFlatSpec
-
-class PortIdentitySerdeSpec extends AnyFlatSpec {
-
-  // 
---------------------------------------------------------------------------
-  // PortIdentityKeySerializer.portIdToString (companion, not the Jackson 
class)
-  // 
---------------------------------------------------------------------------
-
-  "PortIdentityKeySerializer.portIdToString" should "format a PortIdentity as 
`id_internal`" in {
-    assert(PortIdentityKeySerializer.portIdToString(PortIdentity(0, internal = 
false)) == "0_false")
-    assert(PortIdentityKeySerializer.portIdToString(PortIdentity(7, internal = 
true)) == "7_true")
-  }
-
-  // 
---------------------------------------------------------------------------
-  // PortIdentityKeySerializer + PortIdentityKeyDeserializer (Jackson wiring)
-  // 
---------------------------------------------------------------------------
-  //
-  // These tests use the production `JSONUtils.objectMapper` directly so a
-  // regression in the singleton wiring (e.g. the module that registers the
-  // PortIdentity key (de)serializer being removed or reordered) surfaces
-  // here, not just on a freshly-constructed mapper.
-
-  "PortIdentity Jackson key (de)serialization" should "round-trip a 
Map[PortIdentity, String] via JSONUtils.objectMapper" in {
-    val original = Map(
-      PortIdentity(0, internal = false) -> "a",
-      PortIdentity(1, internal = true) -> "b"
-    )
-    val json = objectMapper.writeValueAsString(original)
-    // Verify the JSON keys match the documented `id_internal` format.
-    assert(json.contains("\"0_false\""))
-    assert(json.contains("\"1_true\""))
-    val tref = objectMapper.getTypeFactory
-      .constructMapType(classOf[java.util.HashMap[_, _]], 
classOf[PortIdentity], classOf[String])
-    val restored: java.util.Map[PortIdentity, String] = 
objectMapper.readValue(json, tref)
-    import scala.jdk.CollectionConverters._
-    assert(restored.asScala.toMap == original)
-  }
-
-  it should "round-trip an empty Map[PortIdentity, V] without invoking the 
(de)serializer" in {
-    val original = Map.empty[PortIdentity, String]
-    val json = objectMapper.writeValueAsString(original)
-    val tref = objectMapper.getTypeFactory
-      .constructMapType(classOf[java.util.HashMap[_, _]], 
classOf[PortIdentity], classOf[String])
-    val restored: java.util.Map[PortIdentity, String] = 
objectMapper.readValue(json, tref)
-    assert(restored.isEmpty)
-  }
-
-  "PortIdentityKeyDeserializer.deserializeKey" should "throw 
NumberFormatException for a non-integer id" in {
-    val d = new PortIdentityKeyDeserializer
-    intercept[NumberFormatException] {
-      d.deserializeKey("notAnInt_false", null)
-    }
-  }
-
-  it should "throw IllegalArgumentException for a non-boolean internal flag" 
in {
-    val d = new PortIdentityKeyDeserializer
-    intercept[IllegalArgumentException] {
-      d.deserializeKey("0_notABool", null)
-    }
-  }
-
-  it should "throw NumberFormatException when the underscore separator is 
missing and the whole string is non-numeric" in {
-    // `key.split("_")` on a separator-less non-numeric string yields a
-    // single-element array, and `parts(0).toInt` fires first → NFE.
-    val d = new PortIdentityKeyDeserializer
-    intercept[NumberFormatException] {
-      d.deserializeKey("missingSeparator", null)
-    }
-  }
-
-  it should "throw ArrayIndexOutOfBoundsException when only the id is provided 
(no `_internal` suffix)" in {
-    // Different separator-missing path: `\"5\".split(\"_\")` yields
-    // [\"5\"], parts(0).toInt = 5 succeeds, then parts(1) reads past the
-    // end. Pin this failure mode explicitly so a future safer parser
-    // breaks the spec on purpose (and the safer error type is chosen
-    // consciously).
-    val d = new PortIdentityKeyDeserializer
-    intercept[ArrayIndexOutOfBoundsException] {
-      d.deserializeKey("5", null)
-    }
-  }
-
-  it should "silently accept extra trailing underscore-separated segments 
(lenient parser, current behavior)" in {
-    // Pin the current lenient behavior: `parts(0).toInt` and
-    // `parts(1).toBoolean` ignore everything past `parts(1)`, so a key
-    // like `"1_true_garbage"` deserializes to `PortIdentity(1, true)`
-    // without complaint. The strict-rejection variant lives in a
-    // pendingUntilFixed test below; characterizing today's lenient
-    // path here means a future-tightening fix would need to update
-    // both tests deliberately.
-    val d = new PortIdentityKeyDeserializer
-    val pid = d.deserializeKey("1_true_garbage", null)
-    assert(pid == PortIdentity(1, internal = true))
-  }
-
-  it should "eventually reject keys with extra trailing segments 
(pendingUntilFixed)" in pendingUntilFixed {
-    // Documented contract: a `PortIdentityKeySerializer` output is exactly
-    // `id_internal` — two underscore-separated segments. Anything else is
-    // corrupt JSON and should be rejected, not silently truncated. The
-    // current implementation is lenient (see characterization test
-    // above); this pendingUntilFixed flips to passing once the parser
-    // is hardened, then `pendingUntilFixed` inverts that into a
-    // deliberate failure forcing the marker to be removed.
-    val d = new PortIdentityKeyDeserializer
-    intercept[IllegalArgumentException] {
-      d.deserializeKey("1_true_garbage", null)
-    }
-  }
-}

Reply via email to