aglinxinyuan opened a new pull request, #7845:
URL: https://github.com/apache/texera/pull/7845

   ### What changes were proposed in this PR?
   
   Three small targets, bundled because each is a few lines with no coverage. 
10 tests added (52 → 62 across the three specs).
   
   All figures below were measured with `<Module>/jacoco` under a 
`Tests.Filter` naming only the spec(s) in question, the same filter for the 
before and after run, parsing `jacoco.xml` per `<sourcefile>`. Two numbers per 
file, because they differ: JaCoCo line-hit (any instruction covered) and the 
Codecov metric (fully-covered lines, so a line with any missed branch arm 
counts against you).
   
   | File | JaCoCo line-hit | Codecov metric |
   |---|---|---|
   | `UserActivityTracker.scala` | 47/56 = 83.9% → **56/56 = 100%** | 42/56 = 
75.0% → **49/56 = 87.5%** |
   | `Utils.scala` | 58/69 = 84.1% → **66/69 = 95.7%** | 38/69 = 55.1% → 
**44/69 = 63.8%** |
   | `WorkflowActor.scala` | 83/89 = 93.3% → **88/89 = 98.9%** | 48/89 = 53.9% 
→ **52/89 = 58.4%** |
   
   All 9 previously-missed lines of `UserActivityTracker` (151-157, 159, 160 — 
the whole of the private `defaultUpsert`) are covered, plus the two 
fatal-escapes-the-catch arms at 89 and 103.
   
   **Two honest caveats on those numbers.** First, `WorkflowActor`'s apparent 
gain is inflated by the narrow filter: against a full CI amber run only lines 
236 and 237 are new, because 231-233 are already covered there by real workers. 
Second, `UserActivityTracker`'s Codecov metric carries about ±2 lines of 
run-to-run noise, because the 16-thread CAS race at lines 77 and 79 does not 
always cover both arms — do not read a 2-line drop on this file as a regression.
   
   ### What adversarial review changed, because it caught a test that pinned 
nothing
   
   The first draft of the `Utils` test asserted `amberHome.getFileName.toString 
== Utils.AMBER_HOME_FOLDER_NAME`. That is tautological: production selects the 
directory *using* that constant, so mutating it to `"common"` moves both sides 
of the equality together and the test stays green — `./common` also exists at 
depth 1, so the walk finds it and the two supporting legs (strict descendant, 
not equal to cwd) are satisfied by any descendant. It was the assertion the 
whole target rested on, and it constrained nothing.
   
   It now asserts the literal `"amber"`, with the constant pinned separately by 
its own one-line test. Mutating the constant kills both.
   
   ### Verification
   
   33 mutations, **28 killed, 5 survivors**, all 5 stated below. Each was 
applied one at a time from a scratch-dir snapshot with an applier that aborts 
unless the anchor occurs exactly once, reverted by copying the snapshot over 
the one exact file path, with the production diff and an md5 re-checked after 
every revert.
   
   A selection — the full table is long, so these are the ones that pin 
something a reader would otherwise assume:
   
   | Mutation | Killed by |
   |---|---|
   | `AMBER_HOME_FOLDER_NAME` `"amber"` → `"common"` | the constant test 
**and** the `getFileName` literal leg |
   | negate the `isAmberHomePath(cwd)` early-return guard | resolves to an 
`amber` directory beneath the working directory |
   | `.filter(isAmberHomePath)` → `.filter(_ => true)` | same test |
   | walk root `Paths.get(".")` → `.getParent` | the strict-descendant leg 
(environment-sensitive — see below) |
   | **reorder** `preStart` to initState / register / initialize | the recorded 
event sequence |
   | **reorder** `preStart` to hoist register above `initState` | the failure 
case's `expectNoMessage` on the parent probe |
   | `RegisterActorRef(actorId, …)` → 
`ActorVirtualIdentity(context.self.path.name)` | the identity assertion, after 
the fixture stopped reusing one literal for both |
   | `context.parent !` → `context.self !` | parent probe times out |
   | `postStop`'s `transferService.stop()` → `()` | the post-stop 
handle-cancellation assertion |
   | `NonFatal` → `Throwable` at each of the three catch sites | three 
fatal-escape cases, one per site |
   | `NonFatal` → `RuntimeException` at each of the three catch sites | three 
checked-`IOException` shapes, one per site |
   | `OffsetDateTime.ofInstant(ts, UTC)` → `now(UTC)` (discard `ts`) | the 
queued-write case — **and nothing else**, see below |
   | `.set(UID, uid)` → `uid + 1` | both singleton cases, each on its own 
assertion |
   | VALUES arm and DO UPDATE arm timestamps, separately | insert case and 
overwrite case respectively — the arms are orthogonal |
   | delete `.onConflict(UID).doUpdate()` | the overwrite case |
   | cooldown comparison `< 0` → `<= 0`, eviction `×2` → `×3`, `isBefore` → 
`!isAfter` | the interval and eviction boundary cases |
   
   Three of these are worth calling out because the obvious version of the test 
does not kill them:
   
   - **Discarding the upsert's `ts` argument survived a tightened wall-clock 
window.** The single-threaded writer picks the task up inside the clock's own 
granularity — measured `beforeCall == afterCall` to the tick on this machine. 
Killing it needed the stall the property exists for: an exclusive lock on 
`texera_db.user_last_active_time` held on a raw connection *outside* the Hikari 
pool the writer borrows from, which blocks one insert so the writer cannot 
reach the next task for 1.5s. If that case is ever deleted, this mutation goes 
straight back to surviving.
   - **The three `NonFatal` catch sites needed a checked non-fatal**, since a 
suite where every non-fatal thrower is a `RuntimeException` lets `case 
NonFatal(e)` → `case e: RuntimeException` survive everywhere. The third site 
(inside the executor task) also needed a capturing executor to be observable at 
all — an `IOException` from `upsertFn` on a same-thread executor is caught one 
level up by `markActive`, so the caller sees nothing either way.
   - **The fixture uids moved to 8810/8820/8830/8840.** With adjacent uids, the 
off-by-one mutation redirected one case's write onto the other case's seed row, 
and the resulting "kill" was really a test aborting on its own clobbered 
precondition.
   
   ### The five survivors, and the equivalent mutants among them
   
   - **`Files.walk(cwd, 2)` → `1` survives.** Unpinnable from a test: 
`amberHomePath` is a `lazy val` reading the process working directory, so one 
JVM takes one branch, and `isAmberHomePath` is private. `amber/` is a direct 
child of the repo root, so depth 1 still finds it. Only depth 0 dies, and it 
dies through production's own exception rather than any assertion about depth. 
Both suggested fixes needed a production visibility change, so neither was 
taken; a comment in the spec records this so the depth-0 kill is not mistaken 
for a depth pin.
   - **Dropping `.toRealPath()` from `isAmberHomePath` survives** — every 
`Files.walk` result is already rooted at a real path. Pinning the symlink 
intent would need a temp symlink tree.
   - **Deleting `preStart`'s entire try/catch survives, and is an equivalent 
mutant.** The `IllegalStateException` leaves `preStart` either way, Pekko wraps 
it in `ActorInitializationException` either way, the default decider Stops 
either way, `postStop` runs either way, and `Terminated` arrives either way. 
Asserting on the exception's cause does not discriminate it either — the cause 
is identical with and without the catch. So `throw t` is pinned *given the 
catch exists*, and nothing more than that is claimed.
   - **Deleting the catch's `logger.warn` survives** — a strict subset of the 
above. Line 236 is line-hit only, never verified, and is stated as such in the 
spec.
   - **Deleting `markActive`'s `if (uid == null) return` survives.** 
`ConcurrentHashMap.get(null)` throws an NPE, which is non-fatal, so the outer 
catch swallows it and the caller still observes a no-op with no write and no 
cooldown entry. Only the log noise differs. Reported rather than repaired for 
that reason; it was untried by the first pass and found while re-deriving the 
table.
   
   One kill is environment-sensitive and says so: retargeting the walk root at 
`cwd.getParent` dies here only because sibling worktrees contain their own 
`amber/` directories. In a checkout whose parent has no sibling `amber`, the 
walk would return `<checkout>/amber`, which still starts with cwd, and the 
mutant would survive.
   
   ### Deliberately not included
   
   `WRITE_INTERVAL` (5 minutes), `WRITER_QUEUE_CAPACITY` (256), the 
`DiscardOldest` policy and the periodic eviction scheduler are unverified — the 
singleton fixture is deliberately built *around* the throttle by giving each 
case its own uid. So `UserActivityTracker.scala` reaching 100% line-hit should 
not be read as 100% verified, and the fixture carries a comment saying so.
   
   `WorkflowActor.scala:47` (the companion's module initializer) remains 
uncovered; nothing here can reach it.
   
   One inherent fragility, recorded in the fixture: the singleton cases assume 
no other `Auth` suite calls `UserActivityTracker.markActive` for uids 
8810/8820/8830/8840, since the cooldown is per-uid and per-JVM and a prior call 
would suppress the write. That is unavoidable when testing a JVM singleton with 
a 5-minute throttle.
   
   No production file is touched.
   
   ### Any related issues, documentation, discussions?
   
   Closes #7844
   
   ### How was this PR tested?
   
   ```
   sbt "Auth/testOnly org.apache.texera.auth.UserActivityTrackerSpec"
   ```
   
   ```
   [info] Tests: succeeded 28, failed 0, canceled 0, ignored 0, pending 0
   ```
   
   ```
   sbt "WorkflowExecutionService/testOnly 
org.apache.texera.amber.engine.common.UtilsSpec 
org.apache.texera.amber.engine.architecture.common.WorkflowActorSpec"
   ```
   
   ```
   [info] Tests: succeeded 34, failed 0, canceled 0, ignored 0, pending 0
   ```
   
   The whole `Auth` module is green at 105 tests across 10 suites, which is the 
check that matters here — it confirms the new `MockTexeraDB` mixin does not 
disturb the sibling suites sharing the JVM and the `SqlServer` singleton. 
`Test/scalafmtCheck` and `Test/scalafix --check` pass for both modules.
   
   ### Was this PR authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 5)
   


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