…pache#7804)
### What changes were proposed in this PR?
Three error paths that silently lost the information they exist to
surface. All were characterized during earlier coverage work and
deliberately left unpinned so a fix would not have to fight a test
asserting the broken behaviour.
**1. `OPVersion.getVersion` no longer returns `null` when `git log`
fails.** The `GitAPIException` catch now stores the same `"N/A"`
sentinel its `NullPointerException` sibling already did, so the result
is non-null *and* memoized — previously the trailing
`opMap.get(operatorName)` handed back `null`, which propagated into
operator metadata (`LogicalOp.scala:462-466` →
`OperatorMetadataGenerator.scala:171`), and every later call re-ran the
failing `git log`. `printStackTrace()` is kept.
That `"N/A"` is the intended sentinel is corroborated downstream:
`agent-service/src/agent/util/workflow-utils.ts:193` already defends
with `?? "N/A"`.
**2. `InputManager.startInputPortReaderThreads` now passes the cause.**
It wrapped a start failure as `new RuntimeException(s"…:
${e.getMessage}")` without `e`, discarding the original stack trace. The
reader thread itself already does this correctly
(`InputPortMaterializationReaderThread.scala:147`).
**3. `PekkoActorRefMappingService.retrieveActorRef`'s catch no longer
re-reads the failing value.** It bound `e` but never used it, instead
reading `actorService.parent` again to name the parent in its warning —
so a **persistently** unreachable parent threw straight out of the
handler meant to contain it. The message now omits the parent and passes
`e` as the log throwable, matching house style
(`WorkflowActor.scala:144`, `SyncExecutionResource.scala:705`).
**Why not hoist the read instead:** capturing the parent ref before the
`try` was considered and rejected — the read *is* what throws, so
hoisting it would let the failure escape `retrieveActorRef` entirely
rather than fixing anything.
### All three are pinned, verified in both directions
The production files were reverted and restored to confirm each test
actually fails without its fix:
| Suite | production reverted | with fixes |
|---|---|---|
| `OPVersionSpec` | **9 passed, 2 failed** | **11 passed** |
| `InputManagerSpec` + `PekkoActorRefMappingServiceSpec` | **20 passed,
2 failed** | **22 passed** |
The before-state failures are the right ones:
- `null was not equal to "N/A"` (both new OPVersion cases)
- `null equaled null the original failure must be attached as the cause`
- `java.lang.IllegalStateException: parent is unreachable at …
PekkoActorRefMappingService.retrieveActorRef` — i.e. thrown *out of* the
handler, exactly the predicted failure mode
### On the tests
- **OPVersion**: two cases using the existing unborn-HEAD throwaway-repo
technique — the `"N/A"` fallback, and that it is *memoized* so a failing
`git log` is not retried.
- **InputManager**: a `NoOpReaderThread` subclass (no-op `run()`, so no
storage is touched) is started and joined, then installed into the
private map by reflection — the same technique as the existing
`OutputPortStorageWriterThreadSpec.installWriterThread`.
`Thread.start()` on a TERMINATED thread then throws
`IllegalThreadStateException` deterministically, and the test asserts
both the wrapping message and that `getCause` is that exception.
- **PekkoActorRefMappingService**: the new case sets the failure counter
to `Int.MaxValue` so *every* read throws, not just the first — the
existing single-failure test could not have caught this. It also asserts
the id remains askable afterwards, i.e. was not wrongly marked queried.
Three stale comments were updated rather than left contradicting the new
assertions: `OPVersionSpec`'s "Deliberately NOT asserted" paragraph
about the null, and the defect notes on the two amber specs.
### Verification
- `OPVersionSpec` 11/11; `InputManagerSpec` +
`PekkoActorRefMappingServiceSpec` +
`InputPortMaterializationReaderThreadSpec` 32/32 (the reader-thread spec
included as the nearest neighbour to the `InputManager` change).
- Specs were filtered deliberately rather than running the whole
`WorkflowOperator` module, because `FileScanSourceOpExecSpec` currently
aborts on Windows over a leaked handle — that is being fixed separately
in apache#7800.
- `scalafmtCheck`, `Test/scalafmtCheck` and `scalafixAll --check` pass
for both modules. One pre-existing scalafix *warning* in an untouched
file (`OutputManagerSpec.scala:59`, unused suppression) is unrelated.
### Any related issues, documentation, discussions?
Closes apache#7803
### How was this PR tested?
```
sbt "WorkflowOperator/testOnly org.apache.texera.amber.operator.metadata.OPVersionSpec" "WorkflowExecutionService/testOnly org.apache.texera.amber.engine.architecture.messaginglayer.InputManagerSpec org.apache.texera.amber.engine.architecture.common.PekkoActorRefMappingServiceSpec"
```
```
[info] Tests: succeeded 11, failed 0, canceled 0, ignored 0, pending 0
[info] Tests: succeeded 22, failed 0, canceled 0, ignored 0, pending 0
```
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
What changes were proposed in this PR?
Three test-only fixes. 45 insertions, 9 deletions across 3 spec files;
git diff -- '*/src/main/*'is empty.1. A leaked file handle was aborting the operator suite and killing the module's coverage run
The exact mechanism, established by instrumenting the run rather than by inspection: in
"create LargeBinary when reading file with LARGE_BINARY type",createTuplesFromFilethrows mid-mapwithIllegalStateException: LargeBinaryManager.create() requires a base URI, but none was set on the current thread. That abandons theAutoClosingIteratorbefore exhaustion, so its close-on-exhaustion hook never fires.afterAllthen fails withtest_large_binary.txt: The process cannot access the file because it is being used by another process, aborting the suite.The blast radius is the whole module:
jacoco.xml, 1.8 MBSo
WorkflowOperator/jacococould not produce coverage for any file in the module on Windows. POSIXunlinkmasks this on Linux CI.The fix is in the test: the read is wrapped so the iterator is drained in a
finally, firing the close hook on both the success and failure paths.AutoClosingIteratorandcreateTuplesFromFileare untouched — the leak is a test that stops early, not a broken production contract.Also worth recording: the
.zipfixture was stranded too. It only looked fine because.gitignore:10is*.zip;afterAllnever reached it, because the.txtdelete threw first.2.
IntervalOpExecSpecis now deterministicIt imported
scala.util.Random.{nextInt, nextLong}and used them at four sites — input ordering twice, a 1k-row dataset, and the interval constant — soIntervalJoinOpExec's coverage footprint drifted between runs of identical source (CI has reported 2 missed + 14 partial where a local run gave 0 + 13).Now a fixed
Seed, with a freshnew Random(Seed)created at each use site so determinism does not depend on test execution order either. No assertion changed.Verified rather than assumed: instrumented to print every generated input, two runs produced byte-identical output (22 lines, empty
diff), and two fullWorkflowOperator/jacocoruns now give byte-identicalIntervalJoinOpExeccounters and all 110 per-line entries.One observation left alone as out of scope: the 1k test deterministically yields 0 matches, because random 64-bit longs essentially never fall inside a sub-1000-wide window, so
outputTuples.size == bruteForceResultis0 == 0. That was equally true before this change.3.
PveResourceSpec's traversal assertion said something it did not testgetPythonBin(testCuid, "..") shouldBe Noneappeared to pin the guard atPveManager.scala:91, but".."matches the name regex on line 88 (^[A-Za-z0-9._-]+$admits dots), and<VenvRoot>/<cuid>/../pvenormalizes to<VenvRoot>/pve— still under root. TheNonecame from theFiles.existscheck on line 92.Proven, not argued: deleting line 91 produced results identical to baseline (43 run, 37 succeeded, 6 failed, 1 canceled) and the traversal test still passed. Nothing in the spec detects that guard's removal. The production file was reverted afterwards.
Split into two honest cases:
"reject pveNames containing a path separator"keeps"../../../etc"and"foo/bar"— both carry a/, so they genuinely pin the name regex."return None for a dot-only pveName, which has no venv"keeps the".."case and pins what it actually pins: a name with no venv on disk yieldsNone.A comment records that line 91 is unreachable by construction — the regex forbids
/andcuidis anInt— so it is defensive code rather than untested code. The guard is left in place; removing it is a production decision, not a test one.Verification
WorkflowOperator/jacoco: 290 suites, 0 aborted, 2301 tests succeeded, sbt exit 0, report emitted.PveResourceSpec: 44 run, 38 succeeded — both reshaped tests pass. The 6 failures are pre-existing and environmental (the mock fabricatesbin/pythonwhilePveManagerlooks forScripts/python.exeon Windows); baseline shows the identical 6.git diff -- '*/src/main/*'is empty; no stranded fixtures (checked withgit status --ignored).WorkflowOperator/Test/scalafmtCheck,WorkflowOperator/scalafixAll --check,WorkflowExecutionService/Test/scalafmtCheck,WorkflowExecutionService/scalafixAll --check.One further defect found, deliberately not fixed here
The same
FileScanSourceOpExecSpectest wraps its assertions incatch { case e: Exception => info(...) }. ScalaTest'sTestFailedExceptionextendsException, andinfooutput is invisible under this build's-u-only reporter (build.sbt:37) — so that test currently cannot fail. Narrowing the catch (or usingcancel/assume) would make it a real test again, but it needs care about howLargeBinaryManagerSpecbinds a base URI, so it does not belong in this change.Any related issues, documentation, discussions?
Closes #7799
How was this PR tested?
Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)