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-7165-c05c0fc783088fa84aa7272820646ac0a4ca869f
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 0022e353c9909d6001421a15ed3d7327ca486a0b
Author: Meng Wang <[email protected]>
AuthorDate: Fri Jul 31 11:48:43 2026 -0700

    test(amber): add unit test coverage for AdminExecutionResource (#7165)
    
    ### What changes were proposed in this PR?
    
    Adds a spec for `AdminExecutionResource`
    
    
(`amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/execution/AdminExecutionResource.scala`),
    which had no test coverage. The resource is jOOQ-backed, so the spec
    runs against
    embedded Postgres via `MockTexeraDB` (the established unit pattern,
    mirroring
    `WorkflowExecutionsResourceSpec`). No production code was changed.
    
    8 tests:
    
    - `mapToName` / `mapToStatus` — the pure status-code lookups, covering
    every known
      code and the default (`"UNKNOWN"` / `-1`); no DB needed.
    - `getTotalWorkflows` — returns 0 with no executions, and counts
    distinct
      workflows (two executions on one workflow still count as one).
    - `listWorkflows` — seeds a user, workflow, version and executions, then
    asserts:
    the returned row's fields (name, owner, mapped status, execution name);
    the
    `access` flag derived from `WORKFLOW_USER_ACCESS` (true with a grant,
    false
    without); that only the latest execution per workflow is returned; that
    the
    status filter narrows the result; and that providing a sort field still
    returns
      the row.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7160
    
    ### How was this PR tested?
    
    Unit tests, run locally against embedded Postgres (`MockTexeraDB`). All
    pass, and
    the failure path was verified by breaking an assertion to confirm the
    suite goes
    red:
    
    ```
    sbt "WorkflowExecutionService/testOnly *AdminExecutionResourceSpec"
    # Tests: succeeded 8, failed 0
    sbt "WorkflowExecutionService/scalafmtCheck"      # clean
    sbt "WorkflowExecutionService/Test/scalafix --check"   # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../execution/AdminExecutionResourceSpec.scala     | 313 +++++++++++++++++++++
 1 file changed, 313 insertions(+)

diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/execution/AdminExecutionResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/execution/AdminExecutionResourceSpec.scala
new file mode 100644
index 0000000000..16cda7c5a0
--- /dev/null
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/execution/AdminExecutionResourceSpec.scala
@@ -0,0 +1,313 @@
+/*
+ * 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.web.resource.dashboard.admin.execution
+
+import org.apache.texera.auth.SessionUser
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.Tables._
+import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.{
+  UserDao,
+  WorkflowDao,
+  WorkflowExecutionsDao,
+  WorkflowUserAccessDao,
+  WorkflowVersionDao
+}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{
+  User,
+  Workflow,
+  WorkflowExecutions,
+  WorkflowUserAccess,
+  WorkflowVersion
+}
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.BeforeAndAfterEach
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import java.sql.Timestamp
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+class AdminExecutionResourceSpec
+    extends AnyFlatSpec
+    with Matchers
+    with BeforeAndAfterAll
+    with BeforeAndAfterEach
+    with MockTexeraDB {
+
+  // MockTexeraDB's embedded Postgres is a JVM singleton shared by all suites, 
which
+  // run sequentially. Random ids keep this suite's rows from colliding with 
data other
+  // suites may have left behind in that shared DB.
+  private val testWid = 4000 + scala.util.Random.nextInt(1000)
+  private val testUid = 4000 + scala.util.Random.nextInt(1000)
+  private val secondWid = testWid + 1
+
+  private var testUser: User = _
+  private var testWorkflow: Workflow = _
+  private var testVersion: WorkflowVersion = _
+
+  private var userDao: UserDao = _
+  private var workflowDao: WorkflowDao = _
+  private var workflowVersionDao: WorkflowVersionDao = _
+  private var workflowExecutionsDao: WorkflowExecutionsDao = _
+  private var workflowUserAccessDao: WorkflowUserAccessDao = _
+
+  private val resource = new AdminExecutionResource()
+
+  override protected def beforeAll(): Unit = {
+    initializeDBAndReplaceDSLContext()
+  }
+
+  override protected def afterAll(): Unit = {
+    shutdownDB()
+  }
+
+  override protected def beforeEach(): Unit = {
+    userDao = new UserDao(getDSLContext.configuration())
+    workflowDao = new WorkflowDao(getDSLContext.configuration())
+    workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration())
+    workflowExecutionsDao = new 
WorkflowExecutionsDao(getDSLContext.configuration())
+    workflowUserAccessDao = new 
WorkflowUserAccessDao(getDSLContext.configuration())
+
+    cleanupTestData()
+
+    testUser = new User
+    testUser.setUid(testUid)
+    testUser.setName("test_user")
+    testUser.setEmail("[email protected]")
+    testUser.setPassword("password")
+
+    testWorkflow = new Workflow
+    testWorkflow.setWid(testWid)
+    testWorkflow.setName("test_workflow_" + 
UUID.randomUUID().toString.substring(0, 8))
+    testWorkflow.setContent("{}")
+    testWorkflow.setDescription("test description")
+    testWorkflow.setCreationTime(new Timestamp(System.currentTimeMillis()))
+    testWorkflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))
+
+    testVersion = new WorkflowVersion
+    testVersion.setWid(testWid)
+    testVersion.setContent("{}")
+    testVersion.setCreationTime(new Timestamp(System.currentTimeMillis()))
+
+    userDao.insert(testUser)
+    workflowDao.insert(testWorkflow)
+    workflowVersionDao.insert(testVersion)
+  }
+
+  override protected def afterEach(): Unit = {
+    cleanupTestData()
+  }
+
+  private def cleanupTestData(): Unit = {
+    val vidSubquery = getDSLContext
+      .select(WORKFLOW_VERSION.VID)
+      .from(WORKFLOW_VERSION)
+      .where(WORKFLOW_VERSION.WID.in(testWid, secondWid))
+
+    getDSLContext
+      .deleteFrom(WORKFLOW_EXECUTIONS)
+      .where(WORKFLOW_EXECUTIONS.VID.in(vidSubquery))
+      .execute()
+    getDSLContext
+      .deleteFrom(WORKFLOW_USER_ACCESS)
+      .where(WORKFLOW_USER_ACCESS.WID.in(testWid, secondWid))
+      .execute()
+    getDSLContext
+      .deleteFrom(WORKFLOW_VERSION)
+      .where(WORKFLOW_VERSION.WID.in(testWid, secondWid))
+      .execute()
+    getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.in(testWid, 
secondWid)).execute()
+    getDSLContext.deleteFrom(USER).where(USER.UID.eq(testUid)).execute()
+  }
+
+  // ─── helpers 
────────────────────────────────────────────────────────────────
+
+  private def seedExecution(
+      status: Byte = 3.toByte,
+      name: String = "run",
+      startOffsetMillis: Long = 0L,
+      durationMillis: Long = 1000L
+  ): WorkflowExecutions = {
+    val execution = new WorkflowExecutions
+    execution.setVid(testVersion.getVid)
+    execution.setUid(testUser.getUid)
+    execution.setStatus(status)
+    execution.setResult("")
+    execution.setLogLocation("")
+    val now = System.currentTimeMillis()
+    execution.setStartingTime(new Timestamp(now - startOffsetMillis))
+    execution.setLastUpdateTime(new Timestamp(now - startOffsetMillis + 
durationMillis))
+    execution.setBookmarked(false)
+    execution.setName(name)
+    execution.setEnvironmentVersion("test-env-1.0")
+    workflowExecutionsDao.insert(execution)
+    execution
+  }
+
+  private def grantAccess(uid: Int, privilege: PrivilegeEnum): Unit = {
+    val access = new WorkflowUserAccess
+    access.setUid(uid)
+    access.setWid(testWid)
+    access.setPrivilege(privilege)
+    workflowUserAccessDao.insert(access)
+  }
+
+  // Seeds a full second workflow (workflow + version + one execution) owned by
+  // testUser, whose latest execution ends `endOffsetMillis` before "now". 
Used to
+  // give the ordering test two workflows with distinct end times.
+  private def seedSecondWorkflow(endOffsetMillis: Long): Unit = {
+    val workflow = new Workflow
+    workflow.setWid(secondWid)
+    workflow.setName("second_workflow_" + 
UUID.randomUUID().toString.substring(0, 8))
+    workflow.setContent("{}")
+    workflow.setDescription("second")
+    workflow.setCreationTime(new Timestamp(System.currentTimeMillis()))
+    workflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))
+    workflowDao.insert(workflow)
+
+    val version = new WorkflowVersion
+    version.setWid(secondWid)
+    version.setContent("{}")
+    version.setCreationTime(new Timestamp(System.currentTimeMillis()))
+    workflowVersionDao.insert(version)
+
+    val execution = new WorkflowExecutions
+    execution.setVid(version.getVid)
+    execution.setUid(testUser.getUid)
+    execution.setStatus(3.toByte)
+    execution.setResult("")
+    execution.setLogLocation("")
+    val now = System.currentTimeMillis()
+    execution.setStartingTime(new Timestamp(now - endOffsetMillis - 1000))
+    execution.setLastUpdateTime(new Timestamp(now - endOffsetMillis))
+    execution.setBookmarked(false)
+    execution.setName("second-run")
+    execution.setEnvironmentVersion("test-env-1.0")
+    workflowExecutionsDao.insert(execution)
+  }
+
+  private def emptyFilter: java.util.List[String] = new 
java.util.ArrayList[String]()
+
+  private def filterOf(statuses: String*): java.util.List[String] = {
+    val list = new java.util.ArrayList[String]()
+    statuses.foreach(list.add)
+    list
+  }
+
+  // Calls the admin listing as testUser with the default page window; keeps 
each
+  // call site under the 100-column limit and centralizes the fixed arguments.
+  private def list(
+      sortField: String = "NO_SORTING",
+      filter: java.util.List[String] = emptyFilter
+  ): List[AdminExecutionResource.dashboardExecution] =
+    resource.listWorkflows(new SessionUser(testUser), 20, 0, sortField, 
"desc", filter)
+
+  // ─── pure status lookups (no DB) 
─────────────────────────────────────────────
+
+  "AdminExecutionResource.mapToName" should "map known status codes and 
default the rest" in {
+    AdminExecutionResource.mapToName(0) shouldBe "READY"
+    AdminExecutionResource.mapToName(1) shouldBe "RUNNING"
+    AdminExecutionResource.mapToName(2) shouldBe "PAUSED"
+    AdminExecutionResource.mapToName(3) shouldBe "COMPLETED"
+    AdminExecutionResource.mapToName(4) shouldBe "FAILED"
+    AdminExecutionResource.mapToName(5) shouldBe "KILLED"
+    AdminExecutionResource.mapToName(99) shouldBe "UNKNOWN"
+  }
+
+  "AdminExecutionResource.mapToStatus" should "invert mapToName and map 
unknowns to -1" in {
+    Seq("READY", "RUNNING", "PAUSED", "COMPLETED", "FAILED", 
"KILLED").zipWithIndex.foreach {
+      case (name, code) => AdminExecutionResource.mapToStatus(name) shouldBe 
code
+    }
+    AdminExecutionResource.mapToStatus("NOT_A_STATUS") shouldBe -1
+  }
+
+  // ─── getTotalWorkflows 
───────────────────────────────────────────────────────
+
+  "getTotalWorkflows" should "count distinct workflows that have executions" 
in {
+    resource.getTotalWorkflows shouldBe 0
+
+    // two executions on the same workflow still count as one distinct workflow
+    seedExecution()
+    seedExecution()
+    resource.getTotalWorkflows shouldBe 1
+  }
+
+  // ─── listWorkflows 
───────────────────────────────────────────────────────────
+
+  "listWorkflows" should "return the seeded execution with the mapped status 
and access flag" in {
+    seedExecution(status = 3.toByte, name = "completed-run")
+    grantAccess(testUser.getUid, PrivilegeEnum.READ)
+
+    val rows = list()
+
+    rows should have size 1
+    val row = rows.head
+    row.workflowId shouldBe testWid
+    row.userName shouldBe "test_user"
+    row.executionName shouldBe "completed-run"
+    row.executionStatus shouldBe "COMPLETED"
+    row.access shouldBe true
+  }
+
+  it should "report access = false when the current user has no 
WORKFLOW_USER_ACCESS row" in {
+    seedExecution()
+
+    val rows = list()
+
+    rows should have size 1
+    rows.head.access shouldBe false
+  }
+
+  it should "keep only the latest execution per workflow" in {
+    seedExecution(name = "older")
+    val latest = seedExecution(name = "newer")
+
+    val rows = list()
+
+    rows should have size 1
+    rows.head.executionId shouldBe latest.getEid
+    rows.head.executionName shouldBe "newer"
+  }
+
+  it should "narrow the result to executions whose status is in the filter" in 
{
+    // The listing returns the latest execution per workflow; a RUNNING latest 
passes a
+    // RUNNING filter, and is excluded by a COMPLETED-only filter.
+    seedExecution(status = 1.toByte, name = "running-run")
+
+    list(filter = filterOf("RUNNING")) should have size 1
+    list(filter = filterOf("COMPLETED")) shouldBe empty
+  }
+
+  it should "order the results by the sort field and direction" in {
+    // testWid's latest execution ends ~now; the second workflow's ends 10 
minutes earlier.
+    seedExecution(name = "recent")
+    seedSecondWorkflow(endOffsetMillis = TimeUnit.MINUTES.toMillis(10))
+
+    // desc by end_time -> the more recently finished workflow (testWid) comes 
first
+    list(sortField = "end_time").map(_.workflowId) shouldBe List(testWid, 
secondWid)
+
+    // asc by end_time -> the older one comes first
+    val ascending =
+      resource.listWorkflows(new SessionUser(testUser), 20, 0, "end_time", 
"asc", emptyFilter)
+    ascending.map(_.workflowId) shouldBe List(secondWid, testWid)
+  }
+}

Reply via email to