7dbd878398
Formatting reflowed two mutation target lines onto multiple lines; update the battery anchors (adapter #14 kit lookup, host #19 Content-Length push) to the new unique substrings. Both batteries kill all mutants again (adapter 14/14, host 20/20).
141 lines
6.2 KiB
Bash
Executable File
141 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# FIFA 17 squad HOST mutation battery.
|
|
#
|
|
# Each mutation injects a specific wrong behaviour into the committed source
|
|
# (host, or the adapter it composes), runs the ONE host test that defends the
|
|
# invariant, and requires that test to FAIL (mutant killed). Source is reverted
|
|
# via `git checkout` after every mutation. A SURVIVOR fails the battery.
|
|
#
|
|
# Run from the host crate root: bash mutation-battery.sh
|
|
set -u
|
|
cd "$(dirname "$0")"
|
|
|
|
H=src/lib.rs
|
|
PROJ=../openfut-adapter-fifa17/src/fut/squad_projection.rs
|
|
EXT=../openfut-adapter-fifa17/src/fut/squad_ext.rs
|
|
ITEM=../openfut-adapter-fifa17/src/fut/item.rs
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
declare -a SURVIVORS=()
|
|
|
|
mutate() {
|
|
OLD="$2" NEW="$3" perl -0777 -pi -e \
|
|
's/\Q$ENV{OLD}\E/$ENV{NEW}/g or die "PATTERN NOT FOUND in '"$1"'\n"' "$1"
|
|
}
|
|
|
|
kill_test() {
|
|
local n="$1" label="$2" filter="$3" file="$4" old="$5" new="$6"
|
|
git checkout -- "$file"
|
|
if ! mutate "$file" "$old" "$new"; then
|
|
echo " [#$n] SETUP ERROR — $label"; FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label (setup)"); git checkout -- "$file"; return
|
|
fi
|
|
if cargo test --quiet --test host_test "$filter" >/dev/null 2>&1; then
|
|
echo " [#$n] SURVIVED — $label"; FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label")
|
|
else
|
|
echo " [#$n] killed — $label"; PASS=$((PASS+1))
|
|
fi
|
|
git checkout -- "$file"
|
|
}
|
|
|
|
echo "== FIFA17 squad host mutation battery =="
|
|
|
|
kill_test 1 "resolvable wire id skips authorization" \
|
|
put_rejects_wire_id_owned_by_another_profile_core_unchanged "$H" \
|
|
'if !owned_set.contains(&slot.owned_card_id) {' 'if false {'
|
|
|
|
kill_test 2 "authorization loop never runs (wrong-profile accepted)" \
|
|
put_rejects_wire_id_owned_by_another_profile_core_unchanged "$H" \
|
|
'for slot in &build.canonical.slots {' 'for slot in build.canonical.slots.iter().take(0) {'
|
|
|
|
kill_test 3 "PUT/core failure masked as success (not failed loudly)" \
|
|
put_core_failure_returns_error_never_python "$H" \
|
|
' error_response(502, "core_error"),' ' json_response(&save_ack(put.id)),'
|
|
|
|
kill_test 4 "PUT forwarded as a partial slot diff" \
|
|
put_full_replacement_commits_canonical_and_extension_and_acks_id0 "$H" \
|
|
'.iter()
|
|
.map(|s| CoreSquadSlot {' '.iter()
|
|
.take(1)
|
|
.map(|s| CoreSquadSlot {'
|
|
|
|
kill_test 5 "extension dropped from the atomic write" \
|
|
put_full_replacement_commits_canonical_and_extension_and_acks_id0 "$H" \
|
|
'ext_payload: build.extension.to_payload(),' 'ext_payload: String::new(),'
|
|
|
|
kill_test 6 "stale extension accepted and projected" \
|
|
stale_extension_is_not_applied_on_reads "$H" \
|
|
' CoreExtState::Stale { .. } => return HostProjection::Stale,' \
|
|
' CoreExtState::Stale { schema_version, payload } => match Fifa17SquadExtensionV1::from_payload(*schema_version, payload) { Ok(e) => e, Err(_) => return HostProjection::Missing },'
|
|
|
|
kill_test 7 "missing extension fabricates defaults" \
|
|
missing_extension_is_explicit_on_reads "$H" \
|
|
' CoreExtState::Missing => return HostProjection::Missing,' \
|
|
' CoreExtState::Missing => Fifa17SquadExtensionV1::from_payload(1, "{}").unwrap(),'
|
|
|
|
kill_test 8 "overlay clobbers userInfo" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$H" \
|
|
' root["squad"] = squad_val;' ' root["squad"] = squad_val; root["userInfo"] = serde_json::json!({});'
|
|
|
|
kill_test 9 "overlay clobbers settings" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$H" \
|
|
' root["squad"] = squad_val;' ' root["squad"] = squad_val; root["settings"] = serde_json::json!({});'
|
|
|
|
kill_test 10 "overlay clobbers pileSizeClientData" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$H" \
|
|
' root["squad"] = squad_val;' ' root["squad"] = squad_val; root["pileSizeClientData"] = serde_json::json!({});'
|
|
|
|
kill_test 11 "/squad/list uses separate shaping logic" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$H" \
|
|
'json_response(&squad_list(&v)),' 'json_response(&serde_json::json!({ "squad": [{ "formation": "WRONG" }] })),'
|
|
|
|
kill_test 12 "captain emitted using resourceId (adapter projector)" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$PROJ" \
|
|
'captain_wire = id.item_id as i64;' 'captain_wire = id.asset_id as i64;'
|
|
|
|
kill_test 13 "kitNumber keyed by slot index (adapter extension)" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$EXT" \
|
|
'(s.owned_card_id.clone(), s.kit_number)' '(s.index.to_string(), s.kit_number)'
|
|
|
|
kill_test 14 "client-reported evaluation dropped/mishandled" \
|
|
put_full_replacement_commits_canonical_and_extension_and_acks_id0 "$H" \
|
|
'chemistry: build.extension.client_reported.chemistry,' 'chemistry: None,'
|
|
|
|
kill_test 15 "host trusts/acts on the Core fingerprint" \
|
|
put_full_replacement_commits_canonical_and_extension_and_acks_id0 "$H" \
|
|
' Ok(_) => (
|
|
json_response(&save_ack(put.id)),' \
|
|
' Ok(r) => (
|
|
if r.canonical_fingerprint.is_empty() { json_response(&save_ack(put.id)) } else { error_response(409, "host_trusts_fp") },'
|
|
|
|
kill_test 16 "per-slot Core lookup introduced (N+1)" \
|
|
read_path_is_bounded_no_per_slot_lookup "$H" \
|
|
' let owned = match deps.core.all_owned() {' \
|
|
' for _ in 0..2 { let _ = deps.core.all_owned(); }
|
|
let owned = match deps.core.all_owned() {'
|
|
|
|
kill_test 17 "/squad/active classified to Rust" \
|
|
classify_squad_and_usermassinfo_routes "$H" \
|
|
' Some(id) => !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit()),' \
|
|
' Some(id) => !id.is_empty(),'
|
|
|
|
kill_test 18 "duplicate definition collapses owned instances (adapter shaper)" \
|
|
duplicate_definition_instances_stay_distinct_through_host "$ITEM" \
|
|
'"id": id.item_id,' '"id": id.asset_id,'
|
|
|
|
kill_test 19 "Content-Length left stale after overlay" \
|
|
coupled_read_after_write_list_and_usermassinfo_agree "$H" \
|
|
'("Content-Length".to_string(), body.len().to_string())' \
|
|
'("Content-Length".to_string(), "0".to_string())'
|
|
|
|
kill_test 20 "Python squad silently used after Rust projection failure" \
|
|
usermassinfo_never_serves_python_squad_on_integrity_failure "$H" \
|
|
'empty_squad_overlay(persona)' 'root["squad"].clone()'
|
|
|
|
echo "== mutants killed: $PASS / $((PASS+FAIL)) =="
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
printf 'SURVIVORS:\n'; printf ' - %s\n' "${SURVIVORS[@]}"
|
|
exit 1
|
|
fi
|
|
echo "all mutants killed"
|