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

Yicong-Huang 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 fc83951058 test(amber): add unit test coverage for StateManager (#4738)
fc83951058 is described below

commit fc8395105819f232cc9b82adc3ea22c92bd6d78e
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat May 2 23:41:05 2026 -0700

    test(amber): add unit test coverage for StateManager (#4738)
    
    ### What changes were proposed in this PR?
    
    Add `StateManagerSpec` covering the full public surface of
    `StateManager`
    
(`amber/src/main/scala/org/apache/texera/amber/engine/common/statetransition/StateManager.scala`):
    
    - `getCurrentState` reports the initial state
    - `transitTo` advances on a valid edge, is a no-op on self-transition,
    throws `InvalidTransitionException` for non-successor targets, and
    throws for states absent from the graph keys
    - `assertState` (single + varargs) returns normally on match and throws
    `InvalidStateException` on mismatch
    - `confirmState` (single + varargs) reports membership without side
    effects
    - `conditionalTransitTo` runs the callback only when the precondition
    holds, does nothing otherwise, and still validates the transition edge
    
    ### Any related issues, documentation, discussions?
    
    Closes #4737
    
    ### How was this PR tested?
    
    `sbt "WorkflowExecutionService/testOnly
    org.apache.texera.amber.engine.common.statetransition.StateManagerSpec"`
    — 13/13 tests pass.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.7)
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../common/statetransition/StateManagerSpec.scala  | 151 +++++++++++++++++++++
 1 file changed, 151 insertions(+)

diff --git 
a/amber/src/test/scala/org/apache/texera/amber/engine/common/statetransition/StateManagerSpec.scala
 
b/amber/src/test/scala/org/apache/texera/amber/engine/common/statetransition/StateManagerSpec.scala
new file mode 100644
index 0000000000..e140049dc6
--- /dev/null
+++ 
b/amber/src/test/scala/org/apache/texera/amber/engine/common/statetransition/StateManagerSpec.scala
@@ -0,0 +1,151 @@
+/*
+ * 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.engine.common.statetransition
+
+import org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity
+import org.apache.texera.amber.engine.common.statetransition.StateManager.{
+  InvalidStateException,
+  InvalidTransitionException
+}
+import org.scalatest.flatspec.AnyFlatSpec
+
+class StateManagerSpec extends AnyFlatSpec {
+
+  private sealed trait DummyState
+  private case object S0 extends DummyState
+  private case object S1 extends DummyState
+  private case object S2 extends DummyState
+  private case object Orphan extends DummyState
+
+  private val actorId: ActorVirtualIdentity = 
ActorVirtualIdentity("test-actor")
+
+  /** Linear graph S0 -> S1 -> S2; S2 is terminal. Orphan is unreachable. */
+  private def linear(initial: DummyState = S0): StateManager[DummyState] =
+    new StateManager[DummyState](
+      actorId,
+      Map(
+        S0 -> Set(S1),
+        S1 -> Set(S2),
+        S2 -> Set.empty
+      ),
+      initial
+    )
+
+  "StateManager" should "report the initial state via getCurrentState" in {
+    assert(linear(S1).getCurrentState == S1)
+  }
+
+  "StateManager.transitTo" should "advance to a state listed as a successor in 
the transition graph" in {
+    val sm = linear()
+    sm.transitTo(S1)
+    assert(sm.getCurrentState == S1)
+  }
+
+  it should "be a no-op when transitioning to the current state" in {
+    val sm = linear(S1)
+    sm.transitTo(S1)
+    assert(sm.getCurrentState == S1)
+  }
+
+  it should "throw InvalidTransitionException when the target is not a 
successor of the current state" in {
+    val sm = linear()
+    val ex = intercept[InvalidTransitionException] {
+      sm.transitTo(S2)
+    }
+    assert(ex.getMessage.contains(S0.toString))
+    assert(ex.getMessage.contains(S2.toString))
+  }
+
+  it should "throw InvalidTransitionException when transitioning out of a 
terminal state with no listed successors" in {
+    val sm = linear(S2) // S2 is a key, but with `Set.empty`, so no 
transitions are allowed.
+    intercept[InvalidTransitionException] {
+      sm.transitTo(S0)
+    }
+  }
+
+  it should "throw InvalidTransitionException when the current state is not a 
key in the transition graph" in {
+    // Orphan is intentionally absent from `linear()`'s key set, so
+    // `stateTransitionGraph.getOrElse(currentState, Set())` falls back to
+    // empty and any target should be rejected.
+    val sm = linear(Orphan)
+    intercept[InvalidTransitionException] {
+      sm.transitTo(S0)
+    }
+  }
+
+  "StateManager.assertState" should "succeed when the current state matches" 
in {
+    val sm = linear()
+    sm.assertState(S0) // does not throw
+    sm.assertState(S0, S1) // varargs: any-of
+  }
+
+  it should "throw InvalidStateException when the current state does not match 
the expected state" in {
+    val sm = linear()
+    intercept[InvalidStateException] {
+      sm.assertState(S1)
+    }
+  }
+
+  it should "throw InvalidStateException when none of the expected states 
match (varargs form)" in {
+    val sm = linear()
+    intercept[InvalidStateException] {
+      sm.assertState(S1, S2)
+    }
+  }
+
+  "StateManager.confirmState" should "report whether the current state 
matches" in {
+    val sm = linear()
+    assert(sm.confirmState(S0))
+    assert(!sm.confirmState(S1))
+  }
+
+  it should "report whether the current state is one of the given states 
(varargs form)" in {
+    val sm = linear()
+    assert(sm.confirmState(S0, S1))
+    assert(!sm.confirmState(S1, S2))
+  }
+
+  "StateManager.conditionalTransitTo" should "transition and run the callback 
when the precondition matches" in {
+    val sm = linear()
+    var called = false
+
+    sm.conditionalTransitTo(S0, S1, () => called = true)
+
+    assert(sm.getCurrentState == S1)
+    assert(called)
+  }
+
+  it should "do nothing and skip the callback when the precondition does not 
match" in {
+    val sm = linear()
+    var called = false
+
+    sm.conditionalTransitTo(S1, S2, () => called = true)
+
+    assert(sm.getCurrentState == S0)
+    assert(!called)
+  }
+
+  it should "still validate the transition graph and throw on an invalid 
transition" in {
+    val sm = linear()
+    intercept[InvalidTransitionException] {
+      sm.conditionalTransitTo(S0, S2, () => ())
+    }
+  }
+}

Reply via email to