📘 AOSP Launcher3 单测覆盖率(.ec)获取完整流程(Android 15 · Headless)
目标:在无 GUI 的服务器环境中,针对
packages/apps/Launcher3
运行单测并稳定生成 coverage.ec文件
🧠 一、目标与核心路径
最终目标:
text
拿到 coverage/ec/launcher3_coverage.ec
完整流程:
text
环境准备 → Emulator → 修改 BP → 编译 → 安装 → 运行测试 → 导出 .ec
⚙️ 二、环境准备
1️⃣ Java 环境
bash
export JAVA_HOME=~/jdk-17.0.10+7
export PATH=$JAVA_HOME/bin:$PATH
验证:
bash
java -version
2️⃣ Android SDK(Headless)
bash
mkdir -p ~/android-sdk
cd ~/android-sdk
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip
unzip commandlinetools-linux-7583922_latest.zip
mkdir -p cmdline-tools/latest
mv cmdline-tools/* cmdline-tools/latest/
环境变量:
bash
export ANDROID_HOME=$HOME/android-sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH
验证:
bash
adb version
emulator -version
📱 三、Emulator(Android 15)
1️⃣ 安装镜像(推荐 google_apis)
bash
sdkmanager \
"platforms;android-35" \
"system-images;android-35;google_apis;x86_64"
2️⃣ 创建 AVD
bash
echo "no" | avdmanager create avd \
-n a15_phone \
-k "system-images;android-35;google_apis;x86_64" \
--force
3️⃣ 启动(Headless)
bash
emulator -avd a15_phone \
-no-window \
-no-audio \
-no-boot-anim \
-gpu swiftshader_indirect \
-accel on \
-cores 4 \
-snapshot none \
-no-snapshot-save \
-memory 4096
4️⃣ 等待系统 ready
bash
adb wait-for-device
until adb shell pm path android >/dev/null 2>&1; do sleep 2; done
🔧 四、修改 BP(开启覆盖率能力)
👉 这一步是关键,否则不会生成 .ec
1️⃣ 修改 Launcher3(被测模块)
文件:
text
packages/apps/Launcher3/Android.bp
添加:
bp
android_app {
name: "Launcher3",
static_libs: [
"jacocoagent", // 👈 必加
"Launcher3ResLib",
],
}
2️⃣ 修改 Launcher3Tests(测试模块)
文件:
text
packages/apps/Launcher3/tests/Android.bp
添加:
bp
android_test {
name: "Launcher3Tests",
static_libs: [
"jacocoagent", // 👈 必加
"Launcher3TestLib",
"com_android_launcher3_flags_lib",
],
}
🧹 五、清理旧编译产物(避免污染)
bash
rm -rf out/target/product/*/testcases/Launcher3Tests
rm -rf out/soong/.intermediates/packages/apps/Launcher3*
🛠️ 六、编译(开启覆盖率)
bash
source build/envsetup.sh
lunch aosp_cf_x86_64_phone-ap3a-userdebug
export EMMA_INSTRUMENT=true
m Launcher3 Launcher3Tests -j$(nproc)
📦 七、安装 APK
1️⃣ 先卸载旧版本(避免冲突)
bash
adb shell pm uninstall com.android.launcher3.tests || true
adb shell pm uninstall com.android.launcher3 || true
2️⃣ 安装 Launcher3
bash
adb push out/target/product/*/system_ext/priv-app/Launcher3/Launcher3.apk /data/local/tmp/
adb shell pm install -r -t /data/local/tmp/Launcher3.apk
3️⃣ 安装 Launcher3Tests
bash
adb push out/target/product/*/testcases/Launcher3Tests/*/Launcher3Tests.apk /data/local/tmp/
adb shell pm install -r -t /data/local/tmp/Launcher3Tests.apk
🧪 八、运行单测并生成覆盖率
👉 这里才会真正生成 .ec
运行指定测试类
bash
adb shell am instrument --user 0 -w -r \
-e coverage true \
-e coverageFile /data/user/0/com.android.launcher3/files/launcher3_coverage.ec \
-e class com.android.launcher3.model.CacheDataUpdatedTaskTest \
com.android.launcher3.tests/androidx.test.runner.AndroidJUnitRunner
📂 九、导出覆盖率文件
1️⃣ 创建目录
bash
mkdir -p coverage/ec
2️⃣ 从设备导出
bash
adb shell run-as com.android.launcher3 \
cat /data/user/0/com.android.launcher3/files/launcher3_coverage.ec \
> coverage/ec/launcher3_coverage.ec
3️⃣ 验证
bash
ls -lh coverage/ec/launcher3_coverage.ec
📦 十、从 .ec 提取"单文件覆盖率"
👉 前面步骤已经得到:
text
coverage/ec/launcher3_coverage.ec
但这个 .ec:
text
❗ 是"整个模块"的覆盖率,不是单文件
🧠 核心原理
text
.ec(全量执行数据)
+
指定 class 文件
↓
Jacoco 过滤
↓
得到该 class 的覆盖率
👉 ❗ 本质:
不是生成单文件 ec,而是用 class 从 ec 中"筛出来"
1️⃣ 准备目标 class 文件
👉 从 Launcher3.jar 中解压
bash
mkdir -p coverage/tmp/launcher3_classes
cd coverage/tmp/launcher3_classes
jar xf ~/android15/out/soong/.intermediates/packages/apps/Launcher3/Launcher3/android_common/*/jacoco-report-classes/Launcher3.jar
🎯 找到目标 class
例如你要分析:
text
CacheDataUpdatedTask.java
对应:
bash
coverage/tmp/launcher3_classes/com/android/launcher3/model/CacheDataUpdatedTask.class
2️⃣ 用 Jacoco 生成 XML(只针对单类)
bash
java -jar tools/jacoco/jacococli.jar report \
coverage/ec/launcher3_coverage.ec \
--classfiles coverage/tmp/launcher3_classes/com/android/launcher3/model/CacheDataUpdatedTask.class \
--sourcefiles packages/apps/Launcher3/src \
--xml coverage/tmp/report.xml
3️⃣ 解析 XML(提取分支覆盖率)
核心逻辑:
python
counter = root.find(".//counter[@type='BRANCH']")
missed = int(counter.get("missed", 0))
covered = int(counter.get("covered", 0))
percent = covered / (missed + covered) * 100
🔥 最关键结论
text
.ec 是全局的
单文件覆盖率是通过 class 过滤得到的
🧠 十一、最终结果
你现在不只是得到:
text
coverage/ec/launcher3_coverage.ec
而是可以得到:
text
某个 class 的覆盖率(精确到分支级别)
例如:
text
CacheDataUpdatedTask.java → 72.5% branch coverage