The Android Check Was Green. The Binary Was Not.
A CI check called 'Android' passed every time. The Android crate would not have compiled. The check was stubbed to exit 0 without calling cargo.
A CI check called “Android” passed every time. The crate it was supposed to validate would not have compiled. The check was stubbed to exit 0 without calling cargo.
The bug was a one-word change in Cargo.toml. The CI green light was a lie by omission.
The setup
ActivityWatch/aw-server-rust#705 adds a configurable port for the Android JNI server. The code creates a Tokio runtime:
use tokio::runtime::Runtime;
let rt = Runtime::new().unwrap();
rt.block_on(async { … });
The Cargo.toml for the Android module declared:
[dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
rt enables the single-threaded current-thread runtime. macros enables #[tokio::main] and #[tokio::test]. Neither enables Runtime::new(). That method is part of rt-multi-thread. Without it, Runtime is in scope but its constructor is not — E0599.
CI was green. Every Android job passed. This is because test-compile-android-cwd.sh does:
#!/usr/bin/env bash
# Compile the Android crate
exit 0
The check reports success by doing nothing. The compilation it advertises never runs. So the E0599 was invisible to CI.
How the AI reviewer found it
The reviewer built a minimal repro crate with the same feature set:
[dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { println!("ok") });
}
cargo build fails:
error[E0599]: no function or associated item named `new` found for struct `Runtime`
--> src/main.rs:2:43
|
= note: the following trait bounds were not satisfied:
`tokio::runtime::Builder: std::ops::Fn() -> tokio::runtime::Builder`
Add "rt-multi-thread" to features, cargo build passes. That was the fix:
-tokio = { version = "1", features = ["rt", "macros"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
One word. The stub check could not catch it. A real compile step would have caught it immediately.
What a stub check costs
A CI check has two jobs: gate bad code, and signal confidence. A stub check fails the second silently. It keeps the gate open while telling reviewers the gate is closed.
The stub probably existed for a real reason — Android cross-compilation is heavy setup and the CI environment may not have had the toolchain. An honest version would mark the job as skipped or excluded, not pass with exit 0. A skipped check communicates absence of data. A green check communicates a test ran.
The Android module now has two layers of checks:
- The AI reviewer, which builds a repro crate on the host to probe feature-flag claims without the full cross-compilation stack.
- The PR description, which now notes that the Android job does not test host compilation.
Neither is a substitute for a real Android compile step. But both beat an unchallenged stub.
Tokio feature flags in brief
Tokio’s feature flags are additive and opt-in:
| Feature | What it enables |
|---|---|
rt |
Current-thread (single-threaded) runtime |
rt-multi-thread |
Multi-thread runtime + Builder::new_multi_thread() + Runtime::new() |
macros |
#[tokio::main], #[tokio::test] |
full |
Everything |
Runtime::new() returns a multi-thread runtime by default. Using it requires rt-multi-thread. This is correct and intentional — the multi-thread runtime is the heavier dependency — but it is a common mistake when rt appears to be enough.
The repro was enough to confirm the claim. The fix was obvious. The blocker was the stub making it invisible.
aw-server-rust#705 has the commits; the tokio fix is ab21828.