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


##########
amber/src/test/scala/org/apache/texera/web/ComputingUnitMasterSpec.scala:
##########
@@ -318,6 +357,171 @@ class ComputingUnitMasterSpec
     }
   }
 
+  /**
+    * The context every client below is built for. Deliberately NOT `new 
WorkflowContext()`:
+    * the default ids are exactly what a factory that dropped its 
`workflowContext` argument
+    * would fall back to, and a fixture that hands production the mutant's own 
literal cannot
+    * tell the two apart.
+    */
+  private def specWorkflowContext: WorkflowContext =
+    new WorkflowContext(
+      workflowId = WorkflowIdentity(90210L),
+      executionId = ExecutionIdentity(90211L)
+    )
+
+  /**
+    * Builds a client the way `WorkflowExecutionService` does. The empty plan 
plus an
+    * all-None config is the recipe AmberClientSpec and three `web.service` 
specs already
+    * use: the AmberClient constructor blocks on an InitializeRequest that 
spawns a real
+    * Coordinator child, and an empty plan lets that finish with no engine 
behind it. The
+    * client is always shut down afterwards -- amber's suites share one 
serially-run JVM, so
+    * a leaked client would leave a live actor tree behind for every later 
suite.
+    */
+  private def withAmberRuntime(
+      workflowContext: WorkflowContext = specWorkflowContext,
+      physicalPlan: PhysicalPlan = PhysicalPlan(Set.empty, Set.empty),
+      conf: CoordinatorConfig = CoordinatorConfig(None, None, None, None),
+      errorHandler: Throwable => Unit = _ => ()
+  )(body: AmberClient => Unit): Unit = {
+    val client = ComputingUnitMaster.createAmberRuntime(
+      workflowContext,
+      physicalPlan,
+      conf,
+      errorHandler
+    )
+    try body(client)
+    finally client.shutdown()
+  }
+
+  /**
+    * Registers one SessionState against a stubbed websocket for the duration 
of `body` and
+    * hands it the JSON payloads pushed to that session. `SessionState` keeps 
a JVM-global
+    * registry, so the entry is always removed again -- a leaked one would 
receive events
+    * from every later suite that builds a coordinator.
+    */
+  private def withRegisteredSession(body: (() => Seq[String]) => Unit): Unit = 
{
+    val pushed = new ConcurrentLinkedQueue[String]()
+    val remote = stub(classOf[RemoteEndpoint.Async]) {
+      case ("sendText", args) => pushed.add(args.head.asInstanceOf[String]); 
null
+    }
+    val session = stub(classOf[Session]) {
+      case ("getAsyncRemote", _) => remote.asInstanceOf[AnyRef]
+    }
+    val sessionId = "computing-unit-master-spec-" + java.util.UUID.randomUUID()
+    SessionState.setState(sessionId, new SessionState(session))
+    try body(() => pushed.asScala.toList)
+    finally SessionState.removeState(sessionId)
+  }
+
+  /** File names directly inside `folder`. */
+  private def entriesIn(folder: Path): Seq[String] = {
+    val listing = Files.list(folder)
+    try listing.iterator().asScala.map(_.getFileName.toString).toList
+    finally listing.close()
+  }
+
+  /**
+    * Reads the only field of `fieldType` off an AmberClient. Every field 
there is
+    * class-private, and the client actor's is additionally name-mangled, so 
they are looked
+    * up by TYPE rather than by a brittle spelling of the name.
+    */
+  private def amberClientField[T](client: AmberClient, fieldType: Class[T]): T 
= {
+    val field = classOf[AmberClient].getDeclaredFields
+      .find(candidate => fieldType.isAssignableFrom(candidate.getType))
+      .getOrElse(fail(s"AmberClient no longer holds a 
${fieldType.getSimpleName} field"))
+    field.setAccessible(true)
+    field.get(client).asInstanceOf[T]
+  }
+
+  "createAmberRuntime" should "build the client on the process-wide actor 
system" in {
+    withAmberRuntime() { client =>
+      val clientActor = amberClientField(client, classOf[ActorRef])
+
+      // The factory has to hand AmberClient the process-wide 
AmberRuntime.actorSystem and
+      // not one of its own: in production that is the system startActorMaster 
bound to
+      // artery and joined to the cluster, and a workflow run on a private 
system would be
+      // unreachable from the rest of the runtime. Identity is asserted as 
well as the name
+      // -- resolving the path inside this suite's own system fails if the 
actor was created
+      // anywhere else, even in a second system that happened to carry the 
same name.
+      clientActor.path.address.system shouldBe testSystem.name
+      val resolved = Await.result(
+        testSystem
+          .actorSelection("/" + clientActor.path.elements.mkString("/"))
+          .resolveOne(10.seconds),
+        10.seconds
+      )
+      resolved shouldBe clientActor
+    }
+  }
+
+  it should "hand the client the caller's error handler" in {
+    val errorHandler: Throwable => Unit = _ => ()
+
+    withAmberRuntime(errorHandler = errorHandler) { client =>
+      // AmberClient routes everything a registered callback throws to this 
function and
+      // nowhere else, so a factory that quietly substituted a handler of its 
own would
+      // swallow callback failures at the single place production builds a 
client.
+      amberClientField(client, classOf[Function1[_, _]]) should be 
theSameInstanceAs errorHandler
+    }
+  }
+
+  it should "forward the workflow context, physical plan and coordinator 
config it is handed" in {
+    // The two assertions above pin arguments 1 and 5. Arguments 2, 3 and 4 
are consumed
+    // inside the AmberClient constructor and retained on no field of it 
(`javap -p` keeps
+    // only errorHandler, clientActor, timeout, registeredObservables, 
isActive and
+    // coordinatorInterface), so they can only be observed through what the 
coordinator the
+    // constructor spawns then does with them. Production passes 
`workflow.context`,
+    // `workflow.physicalPlan` and the service's own config at 
WorkflowExecutionService:124;
+    // a factory that substituted defaults for any of the three would run 
every workflow with
+    // an empty plan, no workflow/execution identity and fault tolerance 
disabled.
+    val scanOp = TestOperators.headerlessSmallCsvScanOpDesc()
+    val context = specWorkflowContext
+    val physicalPlan = buildWorkflow(List(scanOp), List.empty, 
context).physicalPlan
+    val logFolder = 
Files.createTempDirectory("computing-unit-master-spec-fault-tolerance")
+    val conf =
+      CoordinatorConfig(None, None, None, Some(FaultToleranceConfig(writeTo = 
logFolder.toUri)))
+    // s"${toShorterString(COORDINATOR)}] [<simple name>" is AmberLogging's 
naming scheme.
+    val scheduleGeneratorLogger =
+      s"${VirtualIdentityUtils.toShorterString(COORDINATOR)}] 
[CostBasedScheduleGenerator"
+

Review Comment:
   The logger name here hard-codes "CostBasedScheduleGenerator". This is easy 
to drift from the actual class name used by AmberLogging (and won't fail fast 
if it changes). Prefer deriving it from the class so renames either keep 
working or fail at compile time.



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