test(adapter): squad mutation battery (14 mutants, all killed)
mutation-battery.sh injects each of the 14 required wrong behaviours into the committed source, runs the one invariant test that must catch it, and requires a non-zero exit (mutant killed), reverting via git after each. Kills: kit-by-slot, captain-as-resourceId, chemistry-reconciled, custom-regenerated, index-derived, stale-accepted, missing-fabricated, faked-asset-id, duplicate-instance-collapse, PUT-as-slot-diff, wire-id-in-canonical, projector-bypasses-shared-shaper, schema-version- ignored, player-state-keyed-by-definition. 14/14 killed.
This commit is contained in:
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# FIFA 17 squad-adapter mutation battery.
|
||||
#
|
||||
# Each mutation injects a specific WRONG behaviour into the committed source,
|
||||
# runs the one test that defends the invariant, and requires that test to FAIL
|
||||
# (non-zero exit) — i.e. the mutant is killed. The source is reverted via
|
||||
# `git checkout` after every mutation, so the tree is left untouched.
|
||||
#
|
||||
# A mutant that SURVIVES (its guard test still passes) means the invariant is
|
||||
# not actually defended; the battery then exits non-zero.
|
||||
#
|
||||
# Run from the adapter crate root: bash mutation-battery.sh
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
FUT=src/fut
|
||||
PASS=0
|
||||
FAIL=0
|
||||
declare -a SURVIVORS=()
|
||||
|
||||
# mutate <file> <literal-old> <literal-new> (literal, multiline-safe)
|
||||
mutate() {
|
||||
OLD="$2" NEW="$3" perl -0777 -pi -e \
|
||||
's/\Q$ENV{OLD}\E/$ENV{NEW}/g or die "MUTATION PATTERN NOT FOUND in '"$1"'\n"' "$1"
|
||||
}
|
||||
|
||||
# kill <n> <label> <test-filter> <file> <old> <new>
|
||||
kill_test() {
|
||||
local n="$1" label="$2" filter="$3" file="$4" old="$5" new="$6"
|
||||
git checkout -- "$file"
|
||||
mutate "$file" "$old" "$new" || { echo " [#$n] SETUP ERROR"; FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label (setup)"); return; }
|
||||
if cargo test --quiet "$filter" >/dev/null 2>&1; then
|
||||
echo " [#$n] SURVIVED — $label (test '$filter' still passed)"
|
||||
FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label")
|
||||
else
|
||||
echo " [#$n] killed — $label"
|
||||
PASS=$((PASS+1))
|
||||
fi
|
||||
git checkout -- "$file"
|
||||
}
|
||||
|
||||
echo "== FIFA17 squad adapter mutation battery =="
|
||||
|
||||
kill_test 1 "kitNumber keyed by slot index, not owned item" \
|
||||
kit_number_is_keyed_by_owned_item_not_slot "$FUT/squad_ext.rs" \
|
||||
'(s.owned_card_id.clone(), s.kit_number)' '(s.index.to_string(), s.kit_number)'
|
||||
|
||||
kill_test 2 "captain emitted using resourceId (asset), not wire id" \
|
||||
fresh_projects_full_23_slot_array_with_captain_wire_id "$FUT/squad_projection.rs" \
|
||||
'captain_wire = id.item_id as i64;' 'captain_wire = id.asset_id as i64;'
|
||||
|
||||
kill_test 3 "client chemistry silently reconciled (shadow lost)" \
|
||||
swap_moves_two_players_with_their_kits_and_round_trips "$FUT/squad_projection.rs" \
|
||||
'"chemistry": ext.client_reported.chemistry,' '"chemistry": 0,'
|
||||
|
||||
kill_test 4 "custom[] regenerated instead of round-tripped verbatim" \
|
||||
custom_is_preserved_byte_for_byte "$FUT/squad_ext.rs" \
|
||||
'custom: put.custom.clone(),' 'custom: Some("[]".to_string()),'
|
||||
|
||||
kill_test 5 "index derived (zeroed) instead of round-tripped" \
|
||||
baseline_projects_the_known_squad_round_trip "$FUT/squad.rs" \
|
||||
'index: p.index,' 'index: 0,'
|
||||
|
||||
kill_test 6 "stale extension accepted and projected" \
|
||||
stale_is_never_applied "$FUT/squad_projection.rs" \
|
||||
'SquadExtInput::Stale(_) => return Ok(SquadProjection::Stale),' 'SquadExtInput::Stale(ext) => ext,'
|
||||
|
||||
kill_test 7 "missing extension fabricates default fields" \
|
||||
missing_is_explicit_never_fabricated "$FUT/squad_projection.rs" \
|
||||
'SquadExtInput::Missing => return Ok(SquadProjection::Missing),' \
|
||||
'SquadExtInput::Missing => return Ok(SquadProjection::Projected(json!({"custom":"[]","manager":[]}))),'
|
||||
|
||||
kill_test 8 "shaper fabricates asset id, bypassing real FIFA identity" \
|
||||
shapes_real_identity_and_reverse_entity_ids "$FUT/item.rs" \
|
||||
'let asset = id.asset_id;' 'let asset = 0;'
|
||||
|
||||
kill_test 9 "duplicate definition collapses owned instances (id=asset)" \
|
||||
two_owned_copies_of_one_definition_stay_distinct_on_the_wire "$FUT/item.rs" \
|
||||
'"id": id.item_id,' '"id": id.asset_id,'
|
||||
|
||||
kill_test 10 "PUT treated as a partial slot diff (drops slots)" \
|
||||
full_replacement_carries_every_occupied_slot_no_diff "$FUT/squad.rs" \
|
||||
'if p.item_data.id == 0 {' 'if p.item_data.id == 0 || p.index > 1 {'
|
||||
|
||||
kill_test 11 "FIFA wire item id stored in canonical replacement" \
|
||||
full_replacement_carries_every_occupied_slot_no_diff "$FUT/squad.rs" \
|
||||
'ProposedSlot {
|
||||
owned_card_id,' 'ProposedSlot {
|
||||
owned_card_id: p.item_data.id.to_string(),'
|
||||
|
||||
kill_test 12 "projector rebuilds items independently of shared shaper" \
|
||||
persisted_read_round_trips_via_reconstructed_canonical_and_extension "$FUT/squad_projection.rs" \
|
||||
'"itemData": shape_item(item, id, ent),' '"itemData": json!({"id": id.item_id}),'
|
||||
|
||||
kill_test 13 "extension schema version ignored on read" \
|
||||
unknown_schema_version_is_rejected_not_coerced "$FUT/squad_ext.rs" \
|
||||
'if schema_version != EXT_SCHEMA_VERSION {' 'if false {'
|
||||
|
||||
kill_test 14 "player state keyed by CardDefinitionId not OwnedItemId" \
|
||||
two_owned_copies_of_one_definition_project_as_distinct_players "$FUT/squad_projection.rs" \
|
||||
'ext.kit_numbers.get(&slot.owned_card_id)' 'ext.kit_numbers.get(&item.card_id)'
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user