Fix faction ammunition resupply and settings recovery
Wait for both convoy cargo phases and use actual magazine stock. Resolve empty primary weapon ammunition from muzzle metadata and recover cleanly from failed initial settings saves. Update native fixtures, scoped cleanup guards, packaging evidence and validation documentation. Make reviewed fixture tests checkout-relative. All repository checks pass against the isolated staged tree; native resupply completion and multiplayer qualification remain outstanding.
This commit is contained in:
@@ -53,3 +53,59 @@ int main(){
|
||||
}
|
||||
''')
|
||||
print('PASS: air logistics reservation preserves delivered search until radio handoff, excludes guards/combat/in-flight orders, and quarantines unresolved native loads/moves')
|
||||
|
||||
# A rifle with its last magazine removed must still identify supply cargo.
|
||||
# Execute the real selector with native weapon/muzzle/catalog calls doubled.
|
||||
import os
|
||||
from pathlib import Path
|
||||
selector_source=Path(os.environ.get('ERR_AIR_AMMO_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AirAmmoMission.c'))
|
||||
selector=body(selector_source,'protected ResourceName Magazine(')
|
||||
selector=selector.replace('protected ','').replace('ResourceName','std::string').replace('IEntity actor','Actor* actor').replace('IEntity owner = null;','Item* owner=nullptr;')
|
||||
selector=selector.replace('BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));','Manager* manager=actor->manager;')
|
||||
selector=selector.replace('array<BaseWeaponComponent>','std::vector<Weapon*>').replace('array<BaseMuzzleComponent>','std::vector<Muzzle*>')
|
||||
selector=selector.replace('foreach (','for (').replace('BaseWeaponComponent weapon','Weapon* weapon').replace('BaseMuzzleComponent muzzle','Muzzle* muzzle').replace('BaseMagazineComponent magazine','LoadedMagazine* magazine')
|
||||
selector=selector.replace('EWeaponType.','EWeaponType::').replace('EMuzzleType.','EMuzzleType::').replace('.IsEmpty()','.empty()')
|
||||
for name in ('manager','weapon','muzzle','magazine','owner'):selector=selector.replace(name+'.',name+'->')
|
||||
selector=selector.replace('GetOwner().','GetOwner()->').replace('GetPrefabData().','GetPrefabData()->')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
enum class EWeaponType{WT_RIFLE,WT_MACHINEGUN,WT_SNIPERRIFLE,WT_LAUNCHER};
|
||||
enum class EMuzzleType{MT_BaseMuzzle,MT_UGLMuzzle};
|
||||
struct Prefab {std::string name="loaded-custom-mag";std::string GetPrefabName(){return name;}};
|
||||
struct Item {Prefab prefab;bool valid=true;Prefab* GetPrefabData(){return valid?&prefab:nullptr;}};
|
||||
struct LoadedMagazine {Item* owner=nullptr;Item* GetOwner(){return owner;}};
|
||||
struct Muzzle {LoadedMagazine* loaded=nullptr;std::string fallback="default-rifle-mag";EMuzzleType type=EMuzzleType::MT_BaseMuzzle;bool well=true;
|
||||
LoadedMagazine* GetMagazine(){return loaded;}bool GetMagazineWell(){return well;}EMuzzleType GetMuzzleType(){return type;}std::string GetDefaultMagazineOrProjectileName(){return fallback;}};
|
||||
struct Weapon {EWeaponType type=EWeaponType::WT_RIFLE;std::vector<Muzzle*> muzzles;EWeaponType GetWeaponType(){return type;}void GetMuzzlesList(std::vector<Muzzle*>& out){out=muzzles;}};
|
||||
struct Manager {std::vector<Weapon*> weapons;void GetWeapons(std::vector<Weapon*>&out){out=weapons;}};
|
||||
struct Actor {Manager* manager=nullptr;};
|
||||
struct Mission {
|
||||
SELECTOR
|
||||
};
|
||||
int main(){
|
||||
Mission mission;Muzzle rifle;Weapon weapon;weapon.muzzles={&rifle};Manager manager;manager.weapons={&weapon};Actor actor{&manager};
|
||||
// This failed before the fix: no inserted magazine after the final reload.
|
||||
assert(mission.Magazine(&actor)=="default-rifle-mag");
|
||||
Item item;LoadedMagazine loaded{&item};rifle.loaded=&loaded;
|
||||
assert(mission.Magazine(&actor)=="loaded-custom-mag");
|
||||
item.prefab.name="";assert(mission.Magazine(&actor)=="default-rifle-mag");
|
||||
item.valid=false;assert(mission.Magazine(&actor)=="default-rifle-mag");
|
||||
loaded.owner=nullptr;assert(mission.Magazine(&actor)=="default-rifle-mag");
|
||||
// An earlier auxiliary launcher must not choose grenade cargo for a rifle.
|
||||
Item grenade;grenade.prefab.name="grenade";LoadedMagazine grenadeMag{&grenade};
|
||||
Muzzle auxiliary;auxiliary.type=EMuzzleType::MT_UGLMuzzle;auxiliary.loaded=&grenadeMag;
|
||||
weapon.muzzles={nullptr,&auxiliary,&rifle};assert(mission.Magazine(&actor)=="default-rifle-mag");
|
||||
rifle.well=false;assert(mission.Magazine(&actor).empty());rifle.well=true;
|
||||
rifle.loaded=nullptr;rifle.fallback="";assert(mission.Magazine(&actor).empty());
|
||||
Muzzle alternate;alternate.fallback="second-rifle-mag";weapon.muzzles.push_back(&alternate);
|
||||
assert(mission.Magazine(&actor)=="second-rifle-mag");
|
||||
weapon.type=EWeaponType::WT_MACHINEGUN;assert(mission.Magazine(&actor)=="second-rifle-mag");
|
||||
weapon.type=EWeaponType::WT_SNIPERRIFLE;assert(mission.Magazine(&actor)=="second-rifle-mag");
|
||||
weapon.type=EWeaponType::WT_LAUNCHER;assert(mission.Magazine(&actor).empty());
|
||||
manager.weapons={nullptr};assert(mission.Magazine(&actor).empty());
|
||||
actor.manager=nullptr;assert(mission.Magazine(&actor).empty());assert(mission.Magazine(nullptr).empty());
|
||||
}
|
||||
'''.replace('SELECTOR',selector))
|
||||
print('PASS: actual ammunition cargo selector handles empty rifle/machinegun/sniper magazines, preserves loaded variants, excludes auxiliary launchers and tolerates missing native metadata')
|
||||
|
||||
@@ -10,16 +10,18 @@ def cpp(s):
|
||||
s=s.replace('array<IEntity> items = {};','Array<IEntity> items;').replace('array<IEntity> raw = {};','Array<IEntity> raw;').replace('array<AIAgent> agents = {};','Array<AIAgent> agents;')
|
||||
s=s.replace('out array<IEntity> items','Array<IEntity>& items').replace('array<IEntity> items','Array<IEntity>& items')
|
||||
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
||||
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord'):
|
||||
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord','ERR_SupplyConvoyTask'):
|
||||
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||
for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
|
||||
s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('ERR_SupplyConvoyTask.Cast','ERR_SupplyConvoyTask::Cast').replace('FindComponent(BaseMagazineComponent)','HasMagazine()')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('InventoryStorageManagerComponent.Cast','InventoryStorageManagerComponent::Cast').replace('FindComponent(InventoryStorageManagerComponent)','FindComponent()')
|
||||
for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine'):
|
||||
for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine','supply'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Receipt','Entity','Group','Utility','StatusSampler','AmmoState','Combat','m_Perception'):
|
||||
s=s.replace(name+'.',name+'->')
|
||||
for name in ('Forces()','GetPrefabData()'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('ERR_SupplyConvoyTask* supply', 'Convoy* supply')
|
||||
s=s.replace('bool available;','bool available=false;').replace('bool living;','bool living=false;')
|
||||
return s
|
||||
rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair('))
|
||||
@@ -45,7 +47,7 @@ struct Entity;using IEntity=Entity*;
|
||||
struct InventoryStorageManagerComponent{Array<IEntity>items;void GetItems(Array<IEntity>& out){out=items;}static auto Cast(InventoryStorageManagerComponent* p){return p;}};
|
||||
struct Prefab{string name="magazine";string GetPrefabName(){return name;}};
|
||||
struct Group;using SCR_AIGroup=Group*;
|
||||
struct Entity{Prefab prefab;bool hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
||||
struct Entity{Prefab prefab;bool magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
|
||||
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
|
||||
struct Waypoint{};
|
||||
struct Group{Array<AIAgent>agents;bool slave=false;int players=0,faction=1;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}void GetAgents(Array<AIAgent>& out){out=agents;}void RemoveWaypoint(Waypoint*){}};
|
||||
@@ -72,7 +74,8 @@ struct Combat{bool Held=false;};
|
||||
struct Truck{int Index=1,SupplyToken=0;bool Registered=true,Released=false,Arrived=false,SupplyHeld=false,crew=true;struct Entity* Entity=nullptr;struct Group* Group=nullptr;struct Combat* Combat=nullptr;bool CrewValid(Mode*){return crew;}};
|
||||
struct Receipt{bool Consumed=true;Array<int>Records={1,2,3,4,5,6};};
|
||||
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
|
||||
struct Convoy{bool Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
|
||||
struct Convoy{bool supply=false,MagazinesIssued=true,CargoIssued=true,Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
|
||||
struct ERR_SupplyConvoyTask:Convoy{static Convoy* Cast(Convoy* task){return task&&task->supply?task:nullptr;}};
|
||||
struct ERR_ConvoyRules{static bool Captured(Entity* e,Group*){return e->captured;}};
|
||||
struct ERR_ConvoyResources{static string Magazine(){return "magazine";}};
|
||||
struct Route{};struct Leaf{void Stop(){}};
|
||||
@@ -81,7 +84,7 @@ struct HQ{Mode* m_Mode;Array<ERR_HQUnit>m_Units;Convoy* convoy=nullptr;Route rou
|
||||
'''+reserve+r'''
|
||||
};
|
||||
struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
|
||||
Array<IEntity>m_Stock,m_Spent,m_Members,m_Served;Map<int>m_Consumed;Map<float>m_NextAttempt;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0;bool m_Stopped=false;
|
||||
Array<IEntity>m_Stock,m_Spent,m_Members,m_Served;Map<int>m_Consumed;Map<float>m_NextAttempt;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;bool m_Stopped=false;
|
||||
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;string reason;bool Current(){return !m_Stopped&&Replication::IsServer()&&m_HQ->active;}void Audit(string why){reason=why;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
@@ -103,14 +106,33 @@ int main(){
|
||||
// Actual inventory aggregation collapses repeated native storage references, not distinct identical-prefab magazines.
|
||||
{Fixture f;f.inventory.items.Insert(&f.items[0]);f.inventory.items.Insert(nullptr);f.s.Stock(0);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Count()==48);assert(f.s.m_Stock[0]!=f.s.m_Stock[1]);}
|
||||
// Malformed/non-magazine items never become spendable stock; native startup partial cargo stays partial.
|
||||
{Fixture f;f.inventory.items={&f.items[0],&f.items[1],&f.items[2],nullptr};f.items[1].hasPrefab=false;f.items[2].prefab.name="rifle";f.s.Stock(0);assert(f.s.m_Stock.Count()==1&&f.s.m_State==1);}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.s.Stock(30000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.inventory.items={&f.items[0],&f.items[1],&f.items[2],nullptr};f.items[1].magazine=false;f.items[2].magazine=false;f.items[2].prefab.name="rifle";f.s.Stock(0);assert(f.s.m_Stock.Count()==1&&f.s.m_State==1);}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.s.Stock(89999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());f.s.Stock(90000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.cargo.Finished=false;f.s.Stock(0);f.cargo.Finished=true;f.inventory.items={&f.items[0]};f.s.Stock(1000);assert(f.s.m_Stock.Count()==1&&!f.s.m_Stopped);}
|
||||
{Fixture f;f.inventory.items.Insert(&f.items[48]);f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(int i=48;i<97;i++)f.inventory.items.Insert(&f.items[i]);f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());f.Unchanged();}
|
||||
{Fixture f;for(auto& item:f.items)f.inventory.items.Insert(&item);f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.vehicle.inventory=nullptr;f.s.Stock(0);assert(f.s.m_Stopped&&!f.Plan());}
|
||||
{Fixture f;f.convoy.Ready=false;f.s.Stock(0);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());}
|
||||
{Fixture f;f.receipt.Consumed=false;f.s.Stock(0);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Supply weapons settle after crew deadline; matching ammo must be issued and settled before snapshot.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.s.Stock(25000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(31000);assert(!f.s.m_Stopped&&f.s.m_State==0);f.convoy.MagazinesIssued=true;f.cargo.Finished=false;
|
||||
f.s.Stock(32000);assert(f.s.m_State==0&&f.s.m_Stock.IsEmpty());f.cargo.Finished=true;
|
||||
f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(33000);assert(f.s.m_State==1&&!f.s.m_Stopped&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Earliest admission: producer may finish weapons only at original Deadline+30000.
|
||||
{Fixture f;f.convoy.supply=true;f.convoy.MagazinesIssued=false;f.cargo.Finished=false;f.s.Stock(0);
|
||||
f.s.Stock(59999);assert(!f.s.m_Stopped&&f.s.m_Stock.IsEmpty());
|
||||
f.convoy.MagazinesIssued=true;f.s.Stock(60000);assert(!f.s.m_Stopped&&f.s.m_State==0);
|
||||
f.cargo.Finished=true;f.items[0].prefab.name="NATO_556_STANAG";f.s.Stock(89000);
|
||||
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Contains(&f.items[0]));}
|
||||
// Missing first-phase submission and missing second phase are bounded by one immutable deadline.
|
||||
for(int phase=0;phase<2;phase++){Fixture f;f.convoy.supply=true;f.convoy.CargoIssued=phase!=0;f.convoy.MagazinesIssued=false;
|
||||
f.s.Stock(25000);f.s.Stock(114999);assert(!f.s.m_Stopped&&f.s.m_StockDeadline==115000&&f.s.m_Stock.IsEmpty());
|
||||
f.s.Stock(115000);assert(f.s.m_Stopped&&f.s.m_Stock.IsEmpty());}
|
||||
// Faction catalogs may yield 96 distinct magazine identities; weapons are excluded.
|
||||
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.items[0].prefab.name="NATO_556_STANAG";
|
||||
f.items[96].magazine=false;f.inventory.items.Insert(&f.items[96]);f.s.Stock(0);
|
||||
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Count()==96&&f.s.m_Stock.Contains(&f.items[0])&&!f.s.m_Stock.Contains(&f.items[96]));}
|
||||
// Removed stock is unavailable; a different magazine added after capture does not replace its identity.
|
||||
{Fixture f;f.s.Stock(0);f.inventory.items={&f.items[48]};assert(!f.Plan());f.Unchanged();f.inventory.items.Insert(&f.items[0]);assert(f.Plan());assert(f.s.m_Stock.Count()==48&&!f.s.m_Stock.Contains(&f.items[48]));}
|
||||
{Fixture f;f.s.Stock(0);f.s.m_Spent=f.s.m_Stock;assert(!f.Plan());f.Unchanged();}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Execute fixture cargo readiness/selection against staged or repository source."""
|
||||
import os,re,sys
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO', Path(__file__).resolve().parents[1]))
|
||||
sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
P=Path(os.environ.get('ERR_INFANTRY_PROBE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeInfantryTransferProbe.c'))
|
||||
s='\n'.join(body(P,x) for x in ('protected int CargoReady(', 'protected bool SelectMagazine(', 'protected int MagazineCount('))
|
||||
s=s.replace('protected ','').replace('foreach (','for (').replace('array<IEntity>', 'Array<Entity*>')
|
||||
s=s.replace('ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(task);','Task* supply = task->phased ? task : nullptr;')
|
||||
s=s.replace('ERR_ConvoyTask task','Task* task').replace('ERR_ConvoyCargoCallback operation','Operation* operation').replace('ERR_HQ hq','HQ* hq').replace('IEntity ','Entity* ')
|
||||
s=s.replace('InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));','Inventory* inventory = owner->inventory;')
|
||||
s=s.replace('InventoryStorageManagerComponent recipient = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));','Inventory* recipient = actor->inventory;')
|
||||
s=s.replace('BaseMagazineComponent.Cast(candidate.FindComponent(BaseMagazineComponent))','candidate->magazine').replace('BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent))','item->magazine')
|
||||
s=s.replace('ERR_Inventory.Items','ERR_Inventory::Items').replace('ERR_Actors.CanFight','ERR_Actors::CanFight').replace('ERR_InfantryAmmoTransfer.Compatible','ERR_InfantryAmmoTransfer::Compatible').replace('EStoragePurpose.PURPOSE_DEPOSIT','0')
|
||||
for v in ('task','supply','operation','hq','m_Unit','actor','recipient','inventory','owner','item'):s=s.replace(v+'.',v+'->')
|
||||
s=s.replace('StatusSampler.','StatusSampler->').replace('Resupply().','Resupply()->')
|
||||
s=re.sub(r'\bnull\b','nullptr',s)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||
struct Entity;
|
||||
struct Inventory {bool space=true;Array<Entity*> items;bool FindStorageForItem(Entity*,int){return space;}};
|
||||
struct Entity {bool magazine=true,alive=true,captured=true;int caliber=1;Inventory* inventory=nullptr;};
|
||||
struct Operation{bool Finished=true;};
|
||||
struct Task {bool Stopped=false,Ready=true,CargoIssued=true,phased=true,MagazinesIssued=true;Array<int> Vehicles={0,1,2,3,4,5};Array<Operation*> Cargo;};
|
||||
struct Sampler{Array<Entity*> actors;bool attributed=true;int InitialStrength(){return actors.Count();}Entity* InitialMember(int i){return actors[i];}bool CanAttribute(int,Entity*){return attributed;}};
|
||||
struct Unit {int Group=1;Sampler* StatusSampler;};
|
||||
struct ResupplyService {bool idle=true;bool ERR_InfantryFixtureStock(Entity* e){return idle&&e->captured;}};
|
||||
struct HQ {ResupplyService service;std::map<int,Unit*> recipients;Array<int> selected;int releases=0;ResupplyService*Resupply(){return &service;}
|
||||
Unit* ERR_InfantryFixtureRecipient(Entity* m){selected.push_back(m->caliber);return recipients[m->caliber];}
|
||||
void ReleaseLogistics(int,int token){assert(token==200001);releases++;}};
|
||||
struct ERR_Actors{static bool CanFight(Entity* e){return e->alive;}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool Compatible(Entity*a,Entity*m){return a->caliber==m->caliber;}};
|
||||
struct ERR_Inventory{static void Items(Inventory*i,Array<Entity*>&out){if(i)out=i->items;}};
|
||||
struct Probe{static constexpr int TOKEN=200001;Unit*m_Unit=nullptr;Entity*m_Actor=nullptr,*m_Magazine=nullptr;
|
||||
METHODS
|
||||
};
|
||||
int main(){
|
||||
Probe p;Operation done,pending;pending.Finished=false;Task t;t.Cargo={&done};
|
||||
assert(p.CargoReady(&t)==1);t.MagazinesIssued=false;assert(p.CargoReady(&t)==0);
|
||||
t.phased=false;assert(p.CargoReady(&t)==1);t.phased=true;t.MagazinesIssued=true;
|
||||
t.Ready=false;t.Vehicles.clear();assert(p.CargoReady(&t)==0);t.Vehicles={0,1,2,3,4,5};t.Ready=true;
|
||||
t.CargoIssued=false;assert(p.CargoReady(&t)==0);t.CargoIssued=true;t.Ready=false;assert(p.CargoReady(&t)==0);t.Ready=true;
|
||||
t.Cargo={&done,&pending};assert(p.CargoReady(&t)==0);pending.Finished=true;assert(p.CargoReady(&t)==1);
|
||||
t.Cargo={nullptr};assert(p.CargoReady(&t)==-1);t.Cargo.assign(104,&done);assert(p.CargoReady(&t)==1);t.Cargo.push_back(&done);assert(p.CargoReady(&t)==-1);
|
||||
t.Cargo={&done};t.Stopped=true;assert(p.CargoReady(&t)==-1);t.Stopped=false;t.Vehicles.pop_back();assert(p.CargoReady(&t)==-1);assert(p.CargoReady(nullptr)==-1);
|
||||
Inventory inventory;Entity actor;actor.caliber=2;actor.inventory=&inventory;Sampler sampler{{&actor}};Unit unit{1,&sampler};HQ hq;hq.recipients[2]=&unit;
|
||||
Entity rifle;rifle.magazine=false;Entity wrong;wrong.caliber=1;Entity right;right.caliber=2;Entity foreign=right;foreign.captured=false;
|
||||
Array<Entity*> stock={nullptr,&rifle,&foreign,&wrong,&right};
|
||||
assert(p.SelectMagazine(&hq,stock));assert(p.m_Magazine==&right&&p.m_Actor==&actor&&p.m_Unit==&unit);assert(hq.selected.Count()==2&&hq.selected[0]==1&&hq.selected[1]==2);
|
||||
// A reservation whose actor changed cannot hide another compatible original item.
|
||||
Probe retry;Entity secondActor;secondActor.caliber=3;secondActor.inventory=&inventory;Sampler secondSampler{{&secondActor}};Unit second{2,&secondSampler};hq.recipients[3]=&second;Entity alternate;alternate.caliber=3;
|
||||
sampler.attributed=false;assert(retry.SelectMagazine(&hq,{&right,&alternate}));assert(retry.m_Magazine==&alternate&&hq.releases==1);
|
||||
// Item counts include all physical magazine types and refuse unbounded scans.
|
||||
inventory.items={&rifle,&wrong,&right,nullptr};Entity owner;owner.inventory=&inventory;assert(p.MagazineCount(&owner)==2);
|
||||
inventory.items.assign(129,&right);assert(p.MagazineCount(&owner)==-1);owner.inventory=nullptr;assert(p.MagazineCount(&owner)==-1);
|
||||
Probe unavailable;hq.service.idle=false;assert(!unavailable.SelectMagazine(&hq,{&right}));hq.service.idle=true;
|
||||
Array<Entity*> tooMany(129,&right);assert(!unavailable.SelectMagazine(&hq,tooMany));
|
||||
}
|
||||
'''.replace('METHODS',s))
|
||||
source=P.read_text();tick=body(P,'int Tick(ERR_HQ hq, float now)')
|
||||
assert 'task.Cargo.Count() == 56' not in tick and 'ERR_ConvoyResources.Magazine()' not in source
|
||||
assert 'm_Stock.Contains(magazine) && !m_Spent.Contains(magazine)' in source
|
||||
assert 'moved.GetAmmoCount() == m_Rounds' in tick and 'm_Mode.Forces().Count() == m_OriginalLedger' in tick
|
||||
assert '!Has(m_Truck.Entity, m_Magazine) && Has(m_Actor, m_Magazine)' in tick
|
||||
print('PASS: actual fixture phased-cargo readiness, bounded callbacks, captured-stock mixed-caliber selection/continuation, reservation cleanup and component magazine counts')
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Execute the actual infantry fixture setup branch; native scene boundaries adapted."""
|
||||
import os,sys
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO', Path(__file__).resolve().parents[1]))
|
||||
sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
p=Path(os.environ.get('ERR_SMOKE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c'))
|
||||
s=body(p,'if (m_ERRTestInfantryTransfer)\n\t\t\t{')
|
||||
s=s.replace('DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);','damage.EnableDamageHandling(false);')
|
||||
s=s.replace('OnStop().Insert(m_ERRInfantryTransfer.Stop);','OnStop().Insert(m_ERRInfantryTransfer);')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
int phase=1;
|
||||
struct ERR_NativeInfantryTransferProbe {static inline int created=0;ERR_NativeInfantryTransferProbe(void*){assert(phase==2);created++;}};
|
||||
struct Damage {bool enabled=true;void EnableDamageHandling(bool b){enabled=b;}};
|
||||
struct Invoker {int callbacks=0;void Insert(ERR_NativeInfantryTransferProbe* p){assert(p);callbacks++;}};
|
||||
struct Mode {bool m_ERRTestInfantryTransfer=true,authorized=true;int m_ERRSmokePlayer=1,m_ERRSmokeStep=0,unlocks=0,failures=0;
|
||||
Damage damage;Invoker stop;ERR_NativeInfantryTransferProbe*m_ERRInfantryTransfer=nullptr;
|
||||
bool UnlockPrison(int p){assert(p==m_ERRSmokePlayer);unlocks++;if(!authorized)return false;phase=2;return true;}
|
||||
bool ERR_Check(bool value,const char*){if(!value)failures++;return value;}Invoker&OnStop(){return stop;}
|
||||
void Setup(){METHOD}
|
||||
~Mode(){delete m_ERRInfantryTransfer;}
|
||||
};
|
||||
int main(){
|
||||
{Mode m;phase=1;ERR_NativeInfantryTransferProbe::created=0;m.Setup();assert(m.unlocks==1&&phase==2&&!m.damage.enabled&&m.failures==0&&m.m_ERRSmokeStep==108&&m.stop.callbacks==1&&ERR_NativeInfantryTransferProbe::created==1);}
|
||||
{Mode m;phase=1;ERR_NativeInfantryTransferProbe::created=0;m.authorized=false;m.Setup();assert(m.unlocks==1&&phase==1&&m.failures==1&&m.m_ERRSmokeStep==0&&m.stop.callbacks==0&&ERR_NativeInfantryTransferProbe::created==0);}
|
||||
{Mode m;phase=1;m.m_ERRTestInfantryTransfer=false;m.Setup();assert(m.unlocks==0&&m.damage.enabled&&m.m_ERRSmokeStep==0&&m.stop.callbacks==0);}
|
||||
}
|
||||
'''.replace('METHOD',s))
|
||||
assert 'Transition(' not in s and 'SetOrigin(' not in s
|
||||
mode=REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
||||
tick=body(mode,'protected void ServerTick(')
|
||||
assert 'if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH) m_Command.Tick(now);' in tick
|
||||
assert 'if (m_Phase >= ERR_Phase.RADIO_SEARCH) TickForces(now);' in tick
|
||||
print('PASS: actual inventory-fixture setup uses ordinary prison unlock before constructing its convoy-waiting probe; failed unlock cannot run fixture')
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Actual native-fixture startup/deadline gate, engine boundaries doubled."""
|
||||
import os,sys
|
||||
import os,sys,re
|
||||
from pathlib import Path
|
||||
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
|
||||
REPO=Path(os.environ.get('ERR_REPO', Path(__file__).resolve().parents[1]))
|
||||
sys.path.insert(0,str(REPO/'tests'))
|
||||
from policy_harness import body,run
|
||||
p=Path(os.environ.get('ERR_MEMORY_PROBE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeHQMemoryProbe.c'))
|
||||
@@ -31,4 +31,61 @@ int main(){ERR_HQ hq;
|
||||
smoke=Path(os.environ.get('ERR_SMOKE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')).read_text()
|
||||
assert 'if (m_ERRTestHQMemory) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 360000;' in smoke
|
||||
assert 'if (m_ERRTestHQBody) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 240000;' in smoke
|
||||
print('PASS: bounded150s startup followed by exactly one120s memory window; only HQ-memory outer deadline extended')
|
||||
# Execute the actual startup selector, so enlarged full-route budgets cannot
|
||||
# accidentally extend a death/radio test or shorten an existing specialized run.
|
||||
start=smoke.index('m_ERRTestLostHelicopter = System.GetCLIParam(')
|
||||
end=smoke.index('OnStop().Insert(ERR_StopHQPatrol);',start)
|
||||
selector=smoke[start:end].replace('System.GetCLIParam("errSmokeOutcome", outcome) && ', '')
|
||||
selector=selector.replace('GetWorld().GetWorldTime()', 'now').replace('ERR_NativeConvoyCoverProbe.Enabled()', 'cover')
|
||||
flags=sorted(set(re.findall(r'\bm_ERRTest\w+',selector)))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
using string = std::string;
|
||||
float deadline(string outcome,float now,bool cover=false){
|
||||
[[maybe_unused]] bool DECLARATIONS;
|
||||
float m_ERRSmokeDeadline=0;
|
||||
SELECTOR
|
||||
return m_ERRSmokeDeadline;
|
||||
}
|
||||
int main(){
|
||||
for(float now : {0.0f, 42000.0f}) {
|
||||
for(string outcome : {"", "victory", "lost-helicopter"}) assert(deadline(outcome,now)==now+300000);
|
||||
for(string outcome : {"escape-route", "player-death", "radio-network", "hq-response", "map-ownership"}) assert(deadline(outcome,now)==now+150000);
|
||||
assert(deadline("hq-memory",now)==now+360000);
|
||||
assert(deadline("hq-body",now)==now+240000);
|
||||
assert(deadline("convoy",now)==now+540000);
|
||||
assert(deadline("supply-convoys",now)==now+840000);
|
||||
assert(deadline("convoy",now,true)==now+840000);
|
||||
}
|
||||
}
|
||||
'''.replace('DECLARATIONS', ','.join(flag+'=false' for flag in flags)).replace('SELECTOR',selector))
|
||||
print('PASS: bounded HQ startup/memory windows and outcome-specific full-route deadlines')
|
||||
|
||||
# The new route diagnostic must leave the original administrator/reroll stress
|
||||
# intact and may skip only those extras, never clue or radio progression gates.
|
||||
route=body(Path(os.environ.get('ERR_SMOKE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c')), 'bool ERR_CheckRouteExtras(float now)')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
struct Probe {
|
||||
bool m_ERRTestEscapeRoute=false,rerollReady=false;
|
||||
int adminResult=0,adminCalls=0,rerollCalls=0,failures=0;
|
||||
int ERR_ProbeAdminMenu(float){adminCalls++;return adminResult;}
|
||||
bool ERR_Check(bool result,std::string){if(!result)failures++;return result;}
|
||||
bool ERR_CheckIntelReroll(float){rerollCalls++;return rerollReady;}
|
||||
METHOD
|
||||
};
|
||||
int main(){
|
||||
Probe route;route.m_ERRTestEscapeRoute=true;assert(route.ERR_CheckRouteExtras(100));
|
||||
assert(route.adminCalls==0&&route.rerollCalls==0&&route.failures==0);
|
||||
Probe full;assert(!full.ERR_CheckRouteExtras(100)&&full.adminCalls==1&&full.rerollCalls==0);
|
||||
full.adminResult=-1;assert(!full.ERR_CheckRouteExtras(200)&&full.failures==1&&full.rerollCalls==0);
|
||||
full.adminResult=1;assert(!full.ERR_CheckRouteExtras(300)&&full.rerollCalls==1);
|
||||
full.rerollReady=true;assert(full.ERR_CheckRouteExtras(400)&&full.rerollCalls==2);
|
||||
}
|
||||
'''.replace('METHOD',route))
|
||||
assert 'if (!ERR_CheckRouteExtras(now)) return;' in smoke
|
||||
assert 'if (!ERR_Check(m_ERRSmokeRadio.Knowledge() == 2,' in smoke
|
||||
assert 'if (!ERR_Check(Phase() == ERR_Phase.EXTRACTION,' in smoke
|
||||
print('PASS: explicit escape-route skips only administrator stress, preserving original full-fixture wait/failure paths')
|
||||
|
||||
@@ -5,7 +5,7 @@ import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
spec=importlib.util.spec_from_file_location('private_prefix_cleanup',ROOT/'tools/private_prefix_cleanup.py')
|
||||
m=importlib.util.module_from_spec(spec);sys.modules[spec.name]=m;spec.loader.exec_module(m)
|
||||
@@ -62,6 +62,37 @@ class PrivatePrefixCleanup(unittest.TestCase):
|
||||
for mutation in ({'uid':UID+1},{'pid':3},{'ppid':2},{'comm':'bash'},{'argv':(b'/usr/lib/systemd/systemd',)},{'exe':'/tmp/systemd'},{'state':'Z'}):
|
||||
with self.subTest(parent=mutation),self.assertRaises(m.CleanupRefused):m.prove_device(proc(),dataclasses.replace(PARENT,**mutation),UID,EXE,INODE)
|
||||
self.assertTrue(m.prove_device(proc(),dataclasses.replace(PARENT,exe='/usr/lib/systemd/systemd'),UID,EXE,INODE))
|
||||
def test_user_systemd_reexec_parent_and_unrelated_metadata(self):
|
||||
for descriptor in (b'0',b'10',b'2147483647'):
|
||||
parent=dataclasses.replace(PARENT,argv=(*m.SYSTEMD_ARGV,b'--deserialize='+descriptor))
|
||||
self.assertTrue(m.native_metadata_proves_unrelated(parent))
|
||||
self.assertEqual(m.prove_device(proc(),parent,UID,EXE,INODE).parent,parent)
|
||||
i=Inspector([proc()]);i.parent=parent;f=FakePidfds(i)
|
||||
self.assertTrue(m._cleanup(i,f)['cleared']);self.assertEqual(f.sent,[(10,signal.SIGTERM)])
|
||||
invalid=[(*m.SYSTEMD_ARGV,b'--deserialize='+value) for value in
|
||||
(b'',b'-1',b'+10',b'10 ',b' 10',b'10x',b'1.0',b'2147483648',b'99999999999')]
|
||||
invalid.extend([(*m.SYSTEMD_ARGV,b'--deserialize',b'10'),
|
||||
(*m.SYSTEMD_ARGV,b'--deserialize=10',b'extra'),
|
||||
(*m.SYSTEMD_ARGV,b'--deserialize=10',b'proton'),
|
||||
(*m.SYSTEMD_ARGV,b'--unknown=10'),
|
||||
(b'/tmp/systemd',b'--user',b'--deserialize=10')])
|
||||
for argv in invalid:
|
||||
parent=dataclasses.replace(PARENT,argv=argv)
|
||||
with self.subTest(argv=argv):
|
||||
self.assertFalse(m.user_systemd_argv(argv))
|
||||
self.assertFalse(m.native_metadata_proves_unrelated(parent))
|
||||
with self.assertRaises(m.CleanupRefused):m.prove_device(proc(),parent,UID,EXE,INODE)
|
||||
parent=dataclasses.replace(PARENT,argv=(*m.SYSTEMD_ARGV,b'--deserialize=10'))
|
||||
for mutation in ({'uid':UID+1},{'pid':3},{'ppid':2},{'comm':'bash'},
|
||||
{'exe':'/tmp/systemd'},{'state':'Z'}):
|
||||
with self.subTest(mutation=mutation),self.assertRaises(m.CleanupRefused):
|
||||
m.prove_device(proc(),dataclasses.replace(parent,**mutation),UID,EXE,INODE)
|
||||
def test_reexec_parent_identity_change_before_signal_refused(self):
|
||||
for mutation in ({'argv':(*m.SYSTEMD_ARGV,b'--deserialize=11')},{'start':999}):
|
||||
i=Inspector([proc()]);i.parent=dataclasses.replace(PARENT,argv=(*m.SYSTEMD_ARGV,b'--deserialize=10'));f=FakePidfds(i)
|
||||
f.on_open=lambda pid:i.__setattr__('parent',dataclasses.replace(i.parent,**mutation))
|
||||
with self.subTest(mutation=mutation),self.assertRaises(m.CleanupRefused):m._cleanup(i,f)
|
||||
self.assertEqual(f.sent,[]);self.assertEqual(f.closed,[1010])
|
||||
def test_clean_term_ignores_other_prefix(self):
|
||||
i=Inspector([proc(10),proc(11,prefix=m.PREFIX+b'/'),proc(12,prefix=b'/other/pfx')]);f=FakePidfds(i)
|
||||
result=m._cleanup(i,f);self.assertTrue(result['cleared']);self.assertEqual(result['term_sent'],[10,11]);self.assertEqual(result['kill_sent'],[])
|
||||
@@ -126,4 +157,20 @@ class PrivatePrefixCleanup(unittest.TestCase):
|
||||
for mutation in ({'comm':'winedevice.exe'},{'argv':agent.argv+(b'proton',)},{'argv':(b'/usr/bin/something-else',)},{'argv':(b'ssh-agent',)},{'comm':'apache2','argv':(b'apache2',b'-unknown')},{'comm':'wine64-preloade','argv':(b'/runtime/wine64-preloader',)}):
|
||||
self.assertFalse(m.native_metadata_proves_unrelated(dataclasses.replace(agent,**mutation)))
|
||||
|
||||
|
||||
def test_unreadable_scdaemon_skipped_but_runtime_and_near_matches_refused(self):
|
||||
entry=MagicMock();entry.name='10';entry.stat.return_value.st_uid=UID
|
||||
entry.__truediv__.return_value.read_bytes.side_effect=PermissionError()
|
||||
root=MagicMock();root.iterdir.side_effect=lambda:iter([entry])
|
||||
inspector=m.ProcFS(EXE,INODE,root=root,uid=UID,self_pid=99)
|
||||
native=proc(comm='scdaemon',argv=(b'scdaemon',b'--multi-server'),exe=None,executable_id=None)
|
||||
with patch.object(inspector,'metadata',return_value=native):
|
||||
self.assertEqual(inspector.devices(),[])
|
||||
for candidate in [proc(exe=None,executable_id=None),
|
||||
dataclasses.replace(native,comm='wine64-preloade'),
|
||||
dataclasses.replace(native,argv=native.argv+(b'proton',)),
|
||||
dataclasses.replace(native,argv=(b'scdaemon',b'--unknown'))]:
|
||||
with self.subTest(candidate=candidate),patch.object(inspector,'metadata',return_value=candidate),self.assertRaises(m.CleanupRefused):
|
||||
inspector.devices()
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
@@ -54,3 +54,89 @@ for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
checks.append(f'assert(Valid("{key}",{value})=={str(expected).lower()});')
|
||||
run('#include <cassert>\n#include <string>\n'+validator+'\nint main(){'+''.join(checks)+'assert(!Valid("unknown",1));}\n')
|
||||
print('PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned')
|
||||
|
||||
# Actual settings transaction: failures during final file promotion must not
|
||||
# poison the next round when there was no previous settings file.
|
||||
source = Path(os.environ.get('ERR_SETTINGS_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c'))
|
||||
method = body(source, 'bool Set(')
|
||||
method = (method.replace('string ', 'std::string ').replace('Replication.', 'Replication::')
|
||||
.replace('FileIO.', 'FileIO::').replace('foreach (', 'for (')
|
||||
.replace('JsonSaveContext json = new JsonSaveContext();', 'JsonSaveContext json;')
|
||||
.replace('map<string, int> ', 'Values ').replace('map<std::string, int> ', 'Values '))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string = std::string;
|
||||
struct Values {
|
||||
std::map<string,int> data;
|
||||
int Get(string key){return data.at(key);}
|
||||
void Remove(string key){data.erase(key);}
|
||||
};
|
||||
struct File {bool valid=true;Values values;};
|
||||
std::map<string,File> disk;
|
||||
int fault=0;
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace FileIO {
|
||||
bool FileExists(string path){return disk.count(path);}
|
||||
bool DeleteFile(string path){return disk.erase(path);}
|
||||
bool CopyFile(string from,string to){
|
||||
if(!disk.count(from))return false;
|
||||
disk[to]=disk[from];
|
||||
if(from=="settings.tmp"){
|
||||
if(fault==1){disk[to].valid=false;return false;} // partial copy reports failure
|
||||
if(fault==2)disk[to].valid=false; // copy succeeds, readback fails
|
||||
if(fault==3)disk[to].values.data["radio"]=31; // valid but incorrect contents
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
struct JsonSaveContext {
|
||||
File pending;
|
||||
void EnableTypeDiscriminator(bool value){}
|
||||
void WriteValue(string key,int value){pending.values.data[key]=value;}
|
||||
bool SaveToFile(string path){disk[path]=pending;return true;}
|
||||
};
|
||||
struct Settings {
|
||||
bool m_ReadOnly=false;
|
||||
string m_Path="settings";
|
||||
Values m_Values{{{"radio",180},{"limit",4}}},m_Resolved{{{"radio",60}}};
|
||||
std::vector<string> Keys(){return {"radio","limit"};}
|
||||
bool ValidSelection(string key,int value){return (key=="radio"&&value>=30&&value<=1800)||(key=="limit"&&value>=0&&value<=8);}
|
||||
int Get(string key){return m_Values.Get(key);}
|
||||
bool Read(string path,Values& values){
|
||||
if(!disk.count(path)||!disk[path].valid)return false;
|
||||
values=disk[path].values;return true;
|
||||
}
|
||||
bool Restart(){
|
||||
Values loaded;
|
||||
if(Read(m_Path,loaded)||Read(m_Path+".bak",loaded))m_Values=loaded;
|
||||
else if(FileIO::FileExists(m_Path)||FileIO::FileExists(m_Path+".bak"))m_ReadOnly=true;
|
||||
return !m_ReadOnly;
|
||||
}
|
||||
'''+method+r'''
|
||||
};
|
||||
int main(){
|
||||
for(int injected : {1,2,3}){
|
||||
// A failed first save must not install a partial or rejected file for restart.
|
||||
disk.clear();fault=injected;Settings initial;
|
||||
assert(!initial.Set("radio",300));
|
||||
assert(initial.Get("radio")==180&&initial.m_Resolved.Get("radio")==60);
|
||||
assert(!FileIO::FileExists("settings"));
|
||||
Settings restarted;assert(restarted.Restart());assert(restarted.Get("radio")==180);
|
||||
fault=0;assert(restarted.Set("radio",450));assert(restarted.Get("radio")==450);
|
||||
Settings persisted;assert(persisted.Restart()&&persisted.Get("radio")==450);
|
||||
// An existing valid save is restored from the newly captured backup.
|
||||
disk.clear();fault=0;Settings saved;assert(saved.Set("radio",600));
|
||||
fault=injected;assert(!saved.Set("radio",900));assert(saved.Get("radio")==600);
|
||||
Settings restored;assert(restored.Restart()&&restored.Get("radio")==600);
|
||||
// A preexisting fallback backup remains authoritative when primary is absent.
|
||||
disk.clear();disk["settings.bak"]={true,Values{{{"radio",750},{"limit",4}}}};
|
||||
Settings fallback;assert(fallback.Restart()&&fallback.Get("radio")==750);
|
||||
assert(!fallback.Set("radio",900));
|
||||
Settings recovered;assert(recovered.Restart()&&recovered.Get("radio")==750);
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: failed initial settings promotion leaves defaults writable after restart; prior/fallback saves survive copy, readback and mismatch faults')
|
||||
|
||||
Reference in New Issue
Block a user