Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/NATIVE_CHECKUPDATE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,8 @@ full 超时整体失败)、激活判定(`activate || crashHold || forceActiv

| 端 | 驱动入口 | 绑定 | 适配器自身的测试 |
| --- | --- | --- | --- |
| iOS | `RCTPushyOrchestrator.mm` `runOnce` → `perform:` / `download:` / `commit:` | 直接调 `flowcore::StepRound` | purge-restore 测试(提交与窗口)、e2e-ios native |
| Android | `SyncCoordinator.java` `runOnce` → `perform` / `download` / `commit` | `FlowBridge.stepRound`(JNI,`.so` 已重编) | `SyncCoordinatorTest`(单次下载的回报)、e2e-android |
| iOS | `RCTPushyOrchestrator.mm` `runOnce` → `perform:` / `download:` / `commit:` | 直接调 `flowcore::StepRound` | purge-restore 测试(提交与窗口;`round_replay_test` 以轨迹向量回放适配器)、e2e-ios native |
| Android | `SyncCoordinator.java` `runOnce` → `perform` / `download` / `commit` | `FlowBridge.stepRound`(JNI,`.so` 已重编) | `SyncRoundReplayTest`(经 `RoundHost` 注入,以轨迹向量回放适配器;JVM 单测仅本地跑)、`SyncCoordinatorTest`(单次下载的回报)、e2e-android |
| 鸿蒙 | `SyncCoordinator.ts` `runOnce` → `perform` / `download` / `commit` | `NativePatchCore.stepRound`(NAPI,同步) | `harmonySyncRound.test.ts`(以轨迹向量回放适配器)、`nativeHostApi.test.ts` |

三端适配器都在提交被拒时按配置代数回报 `reason: "config_changed"`(Android/鸿蒙换配置时同时递增 reset
Expand Down
28 changes: 28 additions & 0 deletions scripts/test-ios-purge-restore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,21 @@ build() {
"$ROOT_DIR"/cpp/update_flow_core/*.cpp \
"$ROOT_DIR/ios/RCTPushy/RCTPushyConfiguration.mm" \
-framework Foundation -o "$destination/purge_restore_test"
# The round adapter against the engine vectors: the extracted runOnce and
# command methods, a scripted engine instead of round_engine.cpp.
xcrun clang++ -std=c++17 -fobjc-arc -fblocks $WARNING_FLAGS $SANITIZE_FLAGS \
-I"$ROOT_DIR" -I"$destination" \
"$TEST_DIR/round_replay_test.mm" "$ROOT_DIR/cpp/update_flow_core/flow_json.cpp" \
-framework Foundation -o "$destination/round_replay_test"
}

VECTORS="$ROOT_DIR/cpp/update_flow_core/tests/round-engine-vectors.json"

build baseline
"$BUILD_DIR/baseline/purge_restore_test" "$@"
if [ "$#" -eq 0 ]; then
"$BUILD_DIR/baseline/round_replay_test" "$VECTORS"
fi

# Negative controls execute only generated test copies. Every original bug must
# fail its named assertion, not merely fail to compile or time out.
Expand Down Expand Up @@ -121,4 +132,21 @@ if [ "${VERIFY_REGRESSIONS:-0}" = "1" ]; then
fi
echo "[PASS] negative control: $variant is detected by $testcase"
done

# The round adapter: a lost commit reported as a reset instead of a config
# change must fail that trace.
variant=drop-config-changed
round="a config change during the download wins the commit"
build "$variant"
logfile="$BUILD_DIR/$variant/result.log"
if "$BUILD_DIR/$variant/round_replay_test" "$VECTORS" "$round" >"$logfile" 2>&1; then
echo "ERROR: the round replay did not detect $variant" >&2
exit 1
fi
if ! grep -F "[FAIL] $round: event 5: expected" "$logfile"; then
cat "$logfile" >&2
echo "ERROR: $variant failed for an unexpected reason" >&2
exit 1
fi
echo "[PASS] negative control: $variant is detected by the round replay"
fi
31 changes: 30 additions & 1 deletion scripts/tests/ios-purge-restore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,35 @@ injected, so the tests do not spend 12 seconds per case. They still assert that
production requests exactly the 12-second budget minus the time already spent
since process start.

## Round adapter replay

`round_replay_test.mm` is a second executable over the same extractor. It
compiles the unchanged `RCTPushyOrchestrator` round adapter (`runOnce`,
`perform:`, `download:`, `commit:`, `finishRound:`, `PushyWaitSeconds` and the
globals they use, written to `round*.inc`) against a scripted engine: for each
round in `cpp/update_flow_core/tests/round-engine-vectors.json` the host's
`flowcore::StepRound` returns the vector's commands, and the IO they ask for
(`PushyHttpRequest`, the install check, `+[RCTPushyInstaller fetch:...]`,
`commitRoundWithGeneration:`, the monotonic clock) is faked from the vector's
next event. It checks every event the adapter answers with (type, text,
installed, ok, code, timedOut, reason, error, activated, now; ok and code as
the engine reads them), the sticky crashHold/forceActivate flags, the arguments
each command ran with, the start facts, the host result, the remembered
unactivated hash and the breadcrumb. Every round except the protocol-error ones
replays, crash hold, forceActivate, deadlines and timed-out downloads included
(a timed-out attempt never settles; the fake clock already reads its
deadline). Same idea as Harmony's `src/__tests__/harmonySyncRound.test.ts` and
Android's `SyncRoundReplayTest`.

```sh
# One round:
.tmp/ios-purge-restore-tests/baseline/round_replay_test \
cpp/update_flow_core/tests/round-engine-vectors.json "a reset during the download wins the commit"
```

## Negative controls

`VERIFY_REGRESSIONS=1` additionally compiles six generated-only mutants:
`VERIFY_REGRESSIONS=1` additionally compiles seven generated-only mutants:

- Remove the closed-window activation veto: `late_commit` must fail.
- Return `NO` after timeout instead of re-resolving: `commit_before_signal` must fail.
Expand All @@ -120,6 +146,9 @@ since process start.
out-of-lock verification and the under-lock re-check):
`round_commit_verifies` must fail.

- Report a commit lost to a config change without `reason: "config_changed"`
(round adapter): the config-change round of `round_replay_test` must fail.

Each mutant must compile and fail its specific state assertion; unrelated
compiler errors, crashes or deadlock-guard failures do not count as detection.
The mutants never modify the checkout's production source.
Expand Down
31 changes: 29 additions & 2 deletions scripts/tests/ios-purge-restore/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,31 @@ def declaration(source: str, name: str) -> tuple[int, str]:
'PushyIsCurrentOrLastLocked', 'PushyVerifyActivation',
'PushyVerifiedActivationUnchangedLocked',
]
# The round adapter (round_replay_test.mm): RCTPushyOrchestrator's runOnce and
# the methods that run the engine's commands, compiled in their own host
# against a scripted engine. Their globals and pure helpers come along; the
# IO collaborators are the host's.
ROUND_GLOBALS = [
'keyNativeConfig', 'keyNativeCheckIncomplete', 'pushyResetGeneration',
'pushyNativeConfigGeneration', 'pushyCrashHoldActive', 'pushyPurgeRestoreActive',
'pushyHostRoundResult', 'pushyHostRoundConfig', 'pushyHostRoundGeneration',
'pushyHostRoundConfigGeneration', 'pushyUnactivatedHash', 'pushyUnactivatedGeneration',
'pushyUnactivatedConfigGeneration',
]
ROUND_HELPERS = ['PushyToStdString', 'PushyFromStdString', 'PushyJsonDictionary',
'PushyHostResult', 'PushyWaitSeconds']
ROUND_METHODS = ['runOnce', 'perform', 'download', 'commit', 'finishRound']
# Emitted ahead of HELPERS, which use them.
STRUCTS: list[str] = ['PushyVerifiedActivation']
# The completion record core switchVersion:error: calls. Copied (with #line)
# into the build directory so the record mutants below can alter it; the
# test script compiles the copy instead of the checkout's file.
CORE_SOURCE = Path('cpp/patch_core/install_record.cpp')
CORE_MUTATIONS = ('trust-digestless', 'grandfather-recorded')
# Mutate the round adapter (round.inc) instead of the purge-restore methods.
ROUND_MUTATIONS = ('drop-config-changed',)
MUTATIONS = ('late-activation', 'skip-reresolve', 'ignore-reset',
'skip-commit-verify') + CORE_MUTATIONS
'skip-commit-verify') + CORE_MUTATIONS + ROUND_MUTATIONS


def mutate(text: str, mutation: str) -> str:
Expand All @@ -134,6 +150,10 @@ def mutate(text: str, mutation: str) -> str:
# Grandfather current/last versions even when they carry a record.
'grandfather-recorded': (
r'if \(grandfathered && state == Record::State::kAbsent\)', 'if (grandfathered)'),
# Report a commit lost to a config change as an ordinary reset.
'drop-config-changed': (
r'event\.Set\("reason", flowjson::Value::String\("config_changed"\)\);',
'(void)configGeneration;'),
# Activate from the round commit without the activation gate (both
# the out-of-lock verification and the under-lock re-check).
'skip-commit-verify': [
Expand Down Expand Up @@ -183,7 +203,11 @@ def generate(source_dir: Path, output: Path, mutation: str | None = None) -> Non
switch_version = locate(sources, method, 'switchVersion')
core_path = (source_dir / '..' / '..' / CORE_SOURCE).resolve()
core = core_path.read_text(encoding='utf-8')
if mutation in CORE_MUTATIONS:
round_methods = [locate(sources, method, name) for name in ROUND_METHODS]
if mutation in ROUND_MUTATIONS:
round_methods = [(path, line, mutate(text, mutation) if name == 'commit' else text)
for name, (path, line, text) in zip(ROUND_METHODS, round_methods)]
elif mutation in CORE_MUTATIONS:
core = mutate(core, mutation)
elif mutation == 'skip-reresolve':
restore = (restore[0], restore[1], mutate(restore[2], mutation))
Expand All @@ -199,6 +223,9 @@ def generate(source_dir: Path, output: Path, mutation: str | None = None) -> Non
# The launch wait helper precedes restorePurgedLaunch so the host's
# dispatch seams apply to it as well.
'orchestrator.inc': [locate(sources, function, 'PushyWaitSeconds'), restore, commit],
'round_globals.inc': [locate(sources, declaration, name) for name in ROUND_GLOBALS],
'round_helpers.inc': [locate(sources, function, name) for name in ROUND_HELPERS],
'round.inc': round_methods,
}
output.mkdir(parents=True, exist_ok=True)
for name, snippets in sections.items():
Expand Down
Loading
Loading