Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,844 changes: 3,844 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"express": "^4.19.2",
"graphology": "^0.25.4",
"graphology-shortest-path": "^2.1.0",
"haptest": "^0.2.1",
"image-hash": "^5.3.2",
"log4js": "^6.9.1",
"moment": "^2.30.1",
Expand Down
72 changes: 72 additions & 0 deletions scripts/extract_viewtree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

function fmtBounds(bounds) {
if (!bounds || !Array.isArray(bounds) || bounds.length < 2) return '';
const a = bounds[0];
const b = bounds[1];
return `bounds=${a.x},${a.y}-${b.x},${b.y}`;
}

function nodeSummary(node) {
if (!node || typeof node !== 'object') return '';
const parts = [];
if (node.type) parts.push(node.type);
const b = fmtBounds(node.bounds || node.origBounds);
if (b) parts.push(b);
if (node.id) parts.push(`id=${node.id}`);
if (node.key) parts.push(`key=${node.key}`);
if (node.text) parts.push(`text=${String(node.text).replace(/\s+/g, ' ').slice(0,80)}`);
return parts.join(' | ');
}

function walk(node, indent, lines) {
if (!node || typeof node !== 'object') return;
const summary = nodeSummary(node) || '(no-type)';
lines.push(`${' '.repeat(indent)}- ${summary}`);
const children = node.children;
if (Array.isArray(children) && children.length > 0) {
for (const c of children) walk(c, indent + 1, lines);
}
}

if (process.argv.length < 3) {
console.error('Usage: node scripts/extract_viewtree.js <input.json>');
process.exit(2);
}

const infile = process.argv[2];
if (!fs.existsSync(infile)) {
console.error('File not found:', infile);
process.exit(2);
}

let obj;
try {
obj = JSON.parse(fs.readFileSync(infile, 'utf8'));
} catch (e) {
console.error('JSON parse error:', e.message);
process.exit(2);
}

const outPath = infile.replace(/\.json$/i, '_viewtree.txt');
const lines = [];

function extractSide(sideName) {
const root = obj[sideName] && obj[sideName].viewTree && obj[sideName].viewTree.root;
lines.push(`${sideName.toUpperCase()} VIEWTREE:`);
if (!root) {
lines.push(' (no viewTree.root)');
lines.push('');
return;
}
walk(root, 0, lines);
lines.push('');
}

extractSide('from');
extractSide('to');

fs.writeFileSync(outPath, lines.join('\n'), 'utf8');
console.log('Wrote:', outPath);
119 changes: 119 additions & 0 deletions scripts/format_and_analyze.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

function maxDepth(obj) {
if (obj === null || typeof obj !== 'object') return 0;
if (Array.isArray(obj)) {
let m = 0;
for (const v of obj) m = Math.max(m, maxDepth(v));
return 1 + m;
}
let m = 0;
for (const k of Object.keys(obj)) m = Math.max(m, maxDepth(obj[k]));
return 1 + m;
}

function countViewNodes(node) {
if (!node || typeof node !== 'object') return 0;
let count = 0;
if (node.type) count = 1;
const children = node.children || [];
for (const c of children) count += countViewNodes(c);
return count;
}

function safeGet(o, pathArr) {
try {
let cur = o;
for (const p of pathArr) {
if (cur == null) return undefined;
cur = cur[p];
}
return cur;
} catch (e) {
return undefined;
}
}

if (process.argv.length < 3) {
console.error('Usage: node scripts/format_and_analyze.js <input.json>');
process.exit(2);
}

const infile = process.argv[2];
if (!fs.existsSync(infile)) {
console.error('File not found:', infile);
process.exit(2);
}
const raw = fs.readFileSync(infile, 'utf8');
let obj;
try {
obj = JSON.parse(raw);
} catch (e) {
console.error('JSON parse error:', e.message);
process.exit(2);
}

const pretty = JSON.stringify(obj, null, 2);
const outPretty = infile.replace(/\.json$/i, '_pretty.json');
fs.writeFileSync(outPretty, pretty, 'utf8');

// Analysis
const stats = fs.statSync(infile);
const analysis = [];
analysis.push('File: ' + infile);
analysis.push('Size: ' + stats.size + ' bytes');
analysis.push('Pretty JSON: ' + outPretty);

const topKeys = Object.keys(obj);
analysis.push('Top-level keys: ' + topKeys.join(', '));
analysis.push('Top-level key count: ' + topKeys.length);
analysis.push('Max object depth: ' + maxDepth(obj));

// Try to detect viewTree nodes
let totalViewNodes = 0;
const fromViewRoot = safeGet(obj, ['from','viewTree','root']);
const toViewRoot = safeGet(obj, ['to','viewTree','root']);
if (fromViewRoot) totalViewNodes += countViewNodes(fromViewRoot);
if (toViewRoot) totalViewNodes += countViewNodes(toViewRoot);
if (totalViewNodes > 0) analysis.push('Estimated total view nodes (from+to): ' + totalViewNodes);

const eventType = safeGet(obj, ['event','type']) || safeGet(obj, ['type']);
if (eventType) analysis.push('Event type: ' + eventType);

const fromAbility = safeGet(obj, ['from','abilityName']) || safeGet(obj, ['from','ability']);
const toAbility = safeGet(obj, ['to','abilityName']) || safeGet(obj, ['to','ability']);
if (fromAbility) analysis.push('From ability: ' + fromAbility);
if (toAbility) analysis.push('To ability: ' + toAbility);

const fromPage = safeGet(obj, ['from','pagePath']);
const toPage = safeGet(obj, ['to','pagePath']);
if (fromPage) analysis.push('From pagePath: ' + fromPage);
if (toPage) analysis.push('To pagePath: ' + toPage);

const fromCap = safeGet(obj, ['from','snapshot','screenCapPath']);
const toCap = safeGet(obj, ['to','snapshot','screenCapPath']);
if (fromCap) analysis.push('From screenCap: ' + fromCap);
if (toCap) analysis.push('To screenCap: ' + toCap);

// Summarize first-level differences between from and to (common keys)
const diffs = [];
for (const k of topKeys) {
if (k === 'from' || k === 'to') continue;
}

// Provide a short sample of 'to' top-level structure (first 200 chars)
try {
const sample = JSON.stringify(obj.to || obj, null, 2).slice(0, 2000);
analysis.push('Sample of `to` (truncated):');
analysis.push(sample);
} catch (e) {}

const outAnalysis = infile.replace(/\.json$/i, '.analysis.txt');
fs.writeFileSync(outAnalysis, analysis.join('\n'), 'utf8');

console.log('Wrote pretty JSON to:', outPretty);
console.log('Wrote analysis to:', outAnalysis);
console.log('Summary:');
console.log(analysis.join('\n'));
111 changes: 111 additions & 0 deletions scripts/run_haptests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -uo pipefail

# 超时时间(秒),可通过环境变量覆盖,例如:
# TIMEOUT_SECS=300 ./run_haptests.sh
TIMEOUT_SECS="${TIMEOUT_SECS:-600}"

# 在下面的数组中按顺序添加要执行的命令(每条一行)。
# 保持格式为: node bin/haptest -i com.huawei.hmos.* -o out/*

# 万兴喵影(剪辑软件逻辑太复杂无法测试)"node bin/haptest -i cn.wondershare.filmora.2in1 -o out/2in1/wondershare"
# 钉钉(暂时无法登陆)"node bin/haptest -i com.dingtalk.hmos.pc -o out/2in1/dingtalk"
COMMANDS=(
#"node bin/haptest -i cn.wps.office.hap -o out/2in1/wps"
"node bin/haptest -i com.eastmoney.emapp -o out/2in1/eastmoney"
"node bin/haptest -i com.edrawsoft.edrawmax.pc -o out/2in1/edrawsoft_edrawmax"
"node bin/haptest -i com.edrawsoft.mindmaster.pc -o out/2in1/edrawsoft_mindmaster"
"node bin/haptest -i com.example.first -o out/2in1/example_first"
"node bin/haptest -i com.example.memoryleak -o out/2in1/example_memoryleak"
"node bin/haptest -i com.foxit.foxitpdfeditor -o out/2in1/foxitpdfeditor"
"node bin/haptest -i com.haitai.htbrowser -o out/2in1/haitai_htbrowser"
"node bin/haptest -i com.haozip2345.app -o out/2in1/haozip2345"
"node bin/haptest -i com.hos.moonshot.kimichat -o out/2in1/moonshot_kimichat"
"node bin/haptest -i com.hp.printercontrol.china -o out/2in1/hp_printercontrol"
"node bin/haptest -i com.oray.sunloginclient -o out/2in1/oray_sunloginclient"
"node bin/haptest -i com.quark.ohosbrowser -o out/2in1/quark_ohosbrowser"
"node bin/haptest -i com.renyitu.pumpkinssh -o out/2in1/renyitu_pumpkinssh"
"node bin/haptest -i com.ss.ohpc.ugc.aweme -o out/2in1/ss_ohpc_ugc_aweme"
"node bin/haptest -i com.tencent.harmonyqq -o out/2in1/harmonyqq"
"node bin/haptest -i com.tencent.wechat.pc -o out/2in1/wechat"
"node bin/haptest -i com.usb.right -o out/2in1/usb_right"
"node bin/haptest -i com.wifiservice.portallogin -o out/2in1/wifiservice_portallogin"
"node bin/haptest -i com.xunlei.thunder -o out/2in1/xunlei_thunder"
"node bin/haptest -i com.xunlei.xmp -o out/2in1/xunlei_xmp"
"node bin/haptest -i com.zhihu.hmos -o out/2in1/zhihu"
"node bin/haptest -i com.zuler.ohospc.todesk -o out/2in1/zuler_ohospc_todesk"
"node bin/haptest -i com.zwsoft.zwcad.PE -o out/2in1/zwsoft_zwcad"
"node bin/haptest -i yylx.danmaku.bili -o out/2in1/yylx_danmaku_bili"
)


run_with_timeout() {
local cmd="$1"

# Prefer coreutils `timeout` if available
if command -v timeout >/dev/null 2>&1; then
timeout "${TIMEOUT_SECS}" bash -c "$cmd"
return $?
fi

# Fallback implementation using background process and manual kill
bash -c "$cmd" &
local pid=$!
local start_ts
start_ts=$(date +%s)

while kill -0 "$pid" >/dev/null 2>&1; do
sleep 1
local now
now=$(date +%s)
local elapsed=$((now - start_ts))
if [ "$elapsed" -ge "$TIMEOUT_SECS" ]; then
echo "Timeout (${TIMEOUT_SECS}s) reached for PID $pid, terminating..."
kill -TERM "$pid" >/dev/null 2>&1 || true
sleep 2
kill -KILL "$pid" >/dev/null 2>&1 || true
wait "$pid" 2>/dev/null || true
return 124
fi
done

wait "$pid"
return $?
}

for cmd in "${COMMANDS[@]}"; do
if [[ -z "$cmd" ]]; then
continue
fi

echo "Running: $cmd (timeout ${TIMEOUT_SECS}s)"
run_with_timeout "$cmd"
rc=$?

if [ $rc -eq 0 ]; then
echo "Command finished successfully."
elif [ $rc -eq 124 ]; then
echo "Command timed out after ${TIMEOUT_SECS}s — retrying once..."
# retry once
run_with_timeout "$cmd"
rc2=$?
if [ $rc2 -eq 0 ]; then
echo "Retry succeeded."
elif [ $rc2 -eq 124 ]; then
echo "Retry also timed out after ${TIMEOUT_SECS}s — skipping to next."
else
echo "Retry exited with status $rc2 — continuing to next."
fi
else
echo "Command exited with status $rc — continuing to next."
fi
done

# 处理 JSON 文件并生成结构化输出
for json_file in events/*.json; do
if [[ -f "$json_file" ]]; then
node scripts/format_and_analyze.js "$json_file"
fi
done

echo "All commands finished."
39 changes: 39 additions & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FuzzOptions } from '../runner/fuzz_options';
import { EnvChecker } from './env_checker';
import { HapTestLogger, LOG_LEVEL } from '../utils/logger';
import { startUIViewerServer } from '../ui/ui_viewer_server';
import { compareDynamicLogs } from '../utils/dynamic_compare';

const logger = getLogger();

Expand Down Expand Up @@ -97,6 +98,25 @@ async function runUIViewerCommand(options: any, version: string): Promise<void>
});
}

async function runCompareCommand(options: any): Promise<void> {
const outputDir = path.resolve(options.output ?? 'out');
const logLevel = resolveLogLevel(options);
HapTestLogger.configure(path.join(outputDir, 'haptest.log'), logLevel);

const reportPath = options.report
? path.resolve(options.report)
: path.join(outputDir, `compare_${options.app}_mobile_2in1.json`);

compareDynamicLogs({
outputRoot: outputDir,
appFolder: options.app,
mobileDir: options.mobile,
twoInOneDir: options.twoInOne,
reportPath,
fullWidthTolerance: Number(options.tolerance),
});
}

(async function (): Promise<void> {
const packageCfg = parsePackageConfig();

Expand All @@ -118,6 +138,25 @@ async function runUIViewerCommand(options: any, version: string): Promise<void>
}
});

program
.command('compare')
.description('Compare mobile and 2in1 dynamic logs for the same app')
.requiredOption('-a, --app <dir>', 'app log folder name under each device directory')
.option('-o, --output <dir>', 'output dir', 'out')
.option('--mobile <dir>', 'mobile device folder name', 'mobile')
.option('--twoInOne <dir>', '2in1 device folder name', '2in1')
.option('--report <file>', 'output report file path')
.option('--tolerance <number>', 'full width tolerance in px', '1')
.option('--debug', 'debug log level', false)
.action(async (cmdOptions) => {
try {
await runCompareCommand(cmdOptions);
} catch (err) {
logger.error('haptest compare command failed.', err);
process.exit(1);
}
});

program
.description('HapTest fuzz runner')
.option('-i, --hap <items...>', 'HAP bundle name or HAP file path or HAP project source root (can specify multiple)')
Expand Down
Loading