You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/31 19:03:02 UTC
(texera) branch gh-readonly-queue/main/pr-7165-c05c0fc783088fa84aa7272820646ac0a4ca869f created (now 0022e353c9)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-7165-c05c0fc783088fa84aa7272820646ac0a4ca869f
in repository https://gitbox.apache.org/repos/asf/texera.git
at 0022e353c9 test(amber): add unit test coverage for AdminExecutionResource (#7165)
This branch includes the following new commits:
new 0022e353c9 test(amber): add unit test coverage for AdminExecutionResource (#7165)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(amber): add unit test coverage for AdminExecutionResource (#7165)
Posted by gi...@apache.org.
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 <me...@uci.edu>
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("admin_exec_test@example.com")
+ 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)
+ }
+}