This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5699-debfc0935f8a58985defbf7a8c4fdf34a2a5431e in repository https://gitbox.apache.org/repos/asf/texera.git
commit 85d76de8cfb0a0957175ab26dc95d4d6d01bf7f9 Author: Xinyuan Lin <[email protected]> AuthorDate: Tue Jun 16 13:42:17 2026 -0700 test(workflow-core): add unit test coverage for storage result + dataset models (#5699) ### What changes were proposed in this PR? Pin behavior of two previously-uncovered storage-layer model classes in `common/workflow-core`. No production-code changes. | Spec | Source class | Tests | | --- | --- | --- | | `OperatorResultMetadataSpec` | `OperatorResultMetadata` | 7 | | `WorkflowResultStoreSpec` | `WorkflowResultStore` | 7 | Both spec files follow the `<srcClassName>Spec.scala` one-to-one convention. > **Note:** an earlier revision also added `OnDatasetSpec`. Per review, it was dropped — `OnDataset` is a pure abstract trait (three abstract accessors, no behavior), so the spec only exercised test-only stub subclasses rather than any production code. Only real implementations warrant tests. **Behavior pinned — `OperatorResultMetadata`** | Surface | Contract | | --- | --- | | `OperatorResultMetadata()` | defaults to `tupleCount == 0` and `changeDetector == ""` | | Custom-constructor values | preserved on both fields | | Equality | case-class equality compares both fields (differing values break it) | | `hashCode` | matches across equal instances | | `copy` | replaces only the field supplied, preserves the rest | **Behavior pinned — `WorkflowResultStore`** | Surface | Contract | | --- | --- | | `WorkflowResultStore()` | defaults `resultInfo` to `Map.empty` | | Custom map | every `(OperatorIdentity, OperatorResultMetadata)` entry preserved; missing keys read as `None` | | Equality | value-based on the inner Map; differing inner metadata breaks equality | | `copy(resultInfo = …)` | replaces the map without mutating the original (immutable case-class contract) | | `WorkflowResultStore()` vs `WorkflowResultStore(Map.empty)` | equal | ### Any related issues, documentation, discussions? Closes #5696. ### How was this PR tested? Pure unit-test additions; verified locally with: - `sbt "WorkflowCore/testOnly org.apache.texera.amber.core.storage.result.OperatorResultMetadataSpec org.apache.texera.amber.core.storage.result.WorkflowResultStoreSpec"` — 14 tests, all green - `sbt scalafmtCheckAll` — clean - CI to confirm ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.7 [1M context]) --- .../result/OperatorResultMetadataSpec.scala | 81 ++++++++++++++++ .../storage/result/WorkflowResultStoreSpec.scala | 102 +++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/OperatorResultMetadataSpec.scala b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/OperatorResultMetadataSpec.scala new file mode 100644 index 0000000000..abb6100157 --- /dev/null +++ b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/OperatorResultMetadataSpec.scala @@ -0,0 +1,81 @@ +/* + * 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.core.storage.result + +import org.scalatest.flatspec.AnyFlatSpec + +class OperatorResultMetadataSpec extends AnyFlatSpec { + + // --------------------------------------------------------------------------- + // Defaults + // --------------------------------------------------------------------------- + + "OperatorResultMetadata()" should "default tupleCount to 0" in { + assert(OperatorResultMetadata().tupleCount == 0) + } + + it should "default changeDetector to the empty string" in { + assert(OperatorResultMetadata().changeDetector == "") + } + + // --------------------------------------------------------------------------- + // Custom constructor values + // --------------------------------------------------------------------------- + + "OperatorResultMetadata(...)" should "preserve a custom tupleCount" in { + assert(OperatorResultMetadata(tupleCount = 42).tupleCount == 42) + } + + it should "preserve a custom changeDetector" in { + assert(OperatorResultMetadata(changeDetector = "abc").changeDetector == "abc") + } + + it should "preserve both fields together" in { + val m = OperatorResultMetadata(tupleCount = 7, changeDetector = "hash-x") + assert(m.tupleCount == 7) + assert(m.changeDetector == "hash-x") + } + + // --------------------------------------------------------------------------- + // Equality / hashCode (case class semantics) + // --------------------------------------------------------------------------- + + "OperatorResultMetadata equality" should "compare both fields" in { + val a = OperatorResultMetadata(1, "x") + val b = OperatorResultMetadata(1, "x") + val c = OperatorResultMetadata(1, "y") + val d = OperatorResultMetadata(2, "x") + assert(a == b) + assert(a.hashCode == b.hashCode) + assert(a != c, "differing changeDetector must break equality") + assert(a != d, "differing tupleCount must break equality") + } + + // --------------------------------------------------------------------------- + // copy semantics + // --------------------------------------------------------------------------- + + "OperatorResultMetadata.copy" should + "replace only the field that was supplied, preserving the rest" in { + val base = OperatorResultMetadata(5, "old-hash") + assert(base.copy(tupleCount = 10) == OperatorResultMetadata(10, "old-hash")) + assert(base.copy(changeDetector = "new-hash") == OperatorResultMetadata(5, "new-hash")) + } +} diff --git a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/WorkflowResultStoreSpec.scala b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/WorkflowResultStoreSpec.scala new file mode 100644 index 0000000000..55fb990544 --- /dev/null +++ b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/result/WorkflowResultStoreSpec.scala @@ -0,0 +1,102 @@ +/* + * 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.core.storage.result + +import org.apache.texera.amber.core.virtualidentity.OperatorIdentity +import org.scalatest.flatspec.AnyFlatSpec + +class WorkflowResultStoreSpec extends AnyFlatSpec { + + // --------------------------------------------------------------------------- + // Fixtures + // --------------------------------------------------------------------------- + + private val opA = OperatorIdentity("op-a") + private val opB = OperatorIdentity("op-b") + + // --------------------------------------------------------------------------- + // Default state + // --------------------------------------------------------------------------- + + "WorkflowResultStore()" should "default resultInfo to Map.empty" in { + assert(WorkflowResultStore().resultInfo.isEmpty) + } + + // --------------------------------------------------------------------------- + // Custom map preserves entries + // --------------------------------------------------------------------------- + + "WorkflowResultStore(...)" should + "preserve a Map carrying metadata entries for multiple operators" in { + val store = WorkflowResultStore( + Map( + opA -> OperatorResultMetadata(10, "h-a"), + opB -> OperatorResultMetadata(20, "h-b") + ) + ) + assert(store.resultInfo.size == 2) + assert(store.resultInfo(opA) == OperatorResultMetadata(10, "h-a")) + assert(store.resultInfo(opB) == OperatorResultMetadata(20, "h-b")) + } + + it should "preserve key identity (a missing key reads as None via .get)" in { + val store = WorkflowResultStore(Map(opA -> OperatorResultMetadata(1, "x"))) + assert(store.resultInfo.get(opA).contains(OperatorResultMetadata(1, "x"))) + assert(store.resultInfo.get(opB).isEmpty) + } + + // --------------------------------------------------------------------------- + // Equality / hashCode (case class semantics) + // --------------------------------------------------------------------------- + + "WorkflowResultStore equality" should + "compare resultInfo by value (two stores with the same Map are equal)" in { + val s1 = WorkflowResultStore(Map(opA -> OperatorResultMetadata(1, "x"))) + val s2 = WorkflowResultStore(Map(opA -> OperatorResultMetadata(1, "x"))) + val s3 = WorkflowResultStore(Map(opA -> OperatorResultMetadata(2, "x"))) + val s4 = WorkflowResultStore(Map.empty) + assert(s1 == s2) + assert(s1.hashCode == s2.hashCode) + assert(s1 != s3, "differing inner metadata must break equality") + assert(s1 != s4, "differing map size must break equality") + } + + // --------------------------------------------------------------------------- + // copy semantics + // --------------------------------------------------------------------------- + + "WorkflowResultStore.copy" should "replace the resultInfo map" in { + val base = WorkflowResultStore(Map(opA -> OperatorResultMetadata(1, "x"))) + val updated = base.copy(resultInfo = Map(opB -> OperatorResultMetadata(2, "y"))) + assert(updated.resultInfo.keySet == Set(opB)) + assert(updated.resultInfo(opB) == OperatorResultMetadata(2, "y")) + // Original is unchanged (immutable case-class semantics). + assert(base.resultInfo.keySet == Set(opA)) + } + + // --------------------------------------------------------------------------- + // Default-arg construction + // --------------------------------------------------------------------------- + + "WorkflowResultStore (default-arg construction)" should + "equal WorkflowResultStore(Map.empty) — the default arg is `Map.empty`" in { + assert(WorkflowResultStore() == WorkflowResultStore(Map.empty)) + } +}
