Advance standalone admission, finite artillery and native validation
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
from policy_harness import body,run
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
s=body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HelicopterAvailability.c','string Check(')
|
||||
run('#include <string>\n#include <cassert>\nusing std::string;\nstruct Availability {float m_fGroundedSince=-1;\n'+s+'''};
|
||||
int main(){
|
||||
Availability a;
|
||||
assert(a.Check(0,0,false,false,true,false,false,0,0).empty());
|
||||
assert(!a.Check(0,0,true,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(20000,0,false,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(79999,0,false,false,true,false,false,0,0).empty());
|
||||
assert(!a.Check(80000,0,false,false,true,false,false,0,0).empty());
|
||||
assert(a.Check(81000,0,false,false,true,true,false,0,0).empty());
|
||||
assert(a.Check(82000,0,false,false,true,false,true,0,0).empty());
|
||||
assert(!a.Check(82000,0,false,true,true,false,false,0,0).empty());
|
||||
assert(!a.Check(82000,0,false,false,false,false,false,0,0).empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: migrated aircraft availability policy (controller migration still pending)')
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute production bounded radio graph and hop-delivery state machine."""
|
||||
from pathlib import Path
|
||||
import re, subprocess, tempfile
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
source=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQNetwork.c').read_text()
|
||||
def extract(signature):
|
||||
a=source.index(signature);b=source.index('{',a);end=b+1;depth=1
|
||||
while depth:depth+=(source[end]=='{')-(source[end]=='}');end+=1
|
||||
return source[a:end]
|
||||
def trans(s):
|
||||
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
|
||||
s=s.replace('string.Format','Format').replace('string ', 'std::string ')
|
||||
s=s.replace('Math.Round(distance).ToString()', 'std::to_string(int(distance))')
|
||||
s=re.sub(r'\b(\w+)\.ToString\(\)',r'std::to_string(\1)',s)
|
||||
for name in ['Math','vector']:s=s.replace(name+'.',name+'::')
|
||||
for typ in ['IEntity','SCR_AIGroup','ERR_HQRoute','ERR_HQRadioNode','ERR_HQJournal']:
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
s=s.replace('array<int> queue = {}','Arr<int> queue').replace('array<int> cell','Arr<int>* cell').replace('array<int> neighbors','Arr<int>* neighbors')
|
||||
s=s.replace('cell = {};','cell = new Arr<int>();')
|
||||
s=re.sub(r'foreach\s*\(int (\w+) : neighbors\)',r'for (auto \1 : *neighbors)',s)
|
||||
for name in ['carrier','caller','node','route','journal','cell']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
s=re.sub(r'(Nodes|m_Nodes)\[([^\]]+)\]\.',r'\1[\2]->',s)
|
||||
s=s.replace('Carrier.GetOrigin()', 'Carrier->GetOrigin()')
|
||||
s=re.sub(r'\b(int|float|bool) (\w+);',r'\1 \2{};',s)
|
||||
return s
|
||||
headers=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
struct vector{float v[3]{};float operator[](int i)const{return v[i];}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}};
|
||||
vector V(float x,float z=0){return {{x,0,z}};}
|
||||
namespace Math{int Floor(float x){return std::floor(x);}}
|
||||
std::string Format(const char*,int x,int z){return std::to_string(x)+":"+std::to_string(z);}
|
||||
template<class T>struct Arr:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}void InsertAt(T x,int i){this->insert(this->begin()+i,x);}};
|
||||
template<class K,class V>struct Map:std::map<K,V>{void Clear(){this->clear();}bool Contains(K k){return this->count(k);}bool Find(K k,V& v){if(!Contains(k))return false;v=this->at(k);return true;}V Get(K k){if(!Contains(k))return {};return this->at(k);}void Insert(K k,V v){(*this)[k]=v;}};
|
||||
struct SCR_AIGroup{bool enemy=true;};struct IEntity{vector pos;bool alive=true;int radio=1;bool captured=false;vector GetOrigin(){return pos;}};
|
||||
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return Carrier&&Carrier->alive&&!Carrier->captured&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
|
||||
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
|
||||
'''
|
||||
routefields='Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{};int Cursor{},MessageId{};float Due{},StartedAt=-1;vector SentFrom;std::string Payload;'
|
||||
networkfields='Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
|
||||
route='struct ERR_HQRoute{'+routefields+'\n'+'\n'.join(trans(extract(x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
|
||||
network='struct Network{'+networkfields+'\n'+'\n'.join(trans(extract(x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route('])+'};\n'
|
||||
tests=r'''
|
||||
int main(){
|
||||
Network network;SCR_AIGroup group;IEntity c[5];for(int i=0;i<5;i++)c[i].pos=V(i*500);
|
||||
c[0].radio=2;for(int i=0;i<5;i++)network.Add(&c[i],&group,37+i,c[i].radio);network.Build();
|
||||
assert(network.Route(&c[0])->Nodes.Count()==1);assert(network.Route(&c[3])->Nodes.Count()==4);assert(!network.Route(&c[4]));
|
||||
ERR_HQJournal journal;auto up=network.Route(&c[2]);assert(up->Nodes.Count()==3);
|
||||
assert(up->Advance(0,&journal)==0);assert(up->Advance(1999,&journal)==0);assert(up->Advance(2000,&journal)==0);
|
||||
c[2].alive=false;assert(up->Advance(4000,&journal)==0);c[0].alive=false;assert(up->Advance(6000,&journal)==1);
|
||||
assert(journal.events.back().find("HQ received")!=std::string::npos); // cannot recall an emitted uplink
|
||||
for(auto& carrier:c)carrier.alive=true;
|
||||
auto lost=network.Route(&c[2]);assert(lost->Advance(0,&journal)==0);c[1].radio=0;assert(lost->Advance(2000,&journal)==-1);assert(lost->Advance(4000,&journal)==-1);
|
||||
c[1].radio=1;auto moving=network.Route(&c[2]);moving->Advance(0,&journal);c[1].pos=V(499);assert(moving->Advance(2000,&journal)==-1);c[1].pos=V(500);
|
||||
auto down=network.Route(&c[2],true);assert(down->Nodes[0]->Carrier==&c[0]);assert(down->Advance(0,&journal)==0);
|
||||
assert(down->Advance(2000,&journal)==0);assert(down->Advance(4000,&journal)==0);assert(down->Advance(6000,&journal)==1);
|
||||
auto captured=network.Route(&c[2],true);captured->Advance(0,&journal);c[0].captured=true;assert(captured->Advance(2000,&journal)==-1);c[0].captured=false;
|
||||
auto timeout=network.Route(&c[2]);timeout->Advance(0,&journal);assert(timeout->Advance(30001,&journal)==-1);
|
||||
Network disconnected;disconnected.Add(&c[1],&group,1,1);disconnected.Build();assert(!disconnected.Route(&c[1]));
|
||||
Network boundary;c[1].pos=V(501);boundary.Add(&c[0],&group,1,2);boundary.Add(&c[1],&group,2,1);boundary.Build();assert(!boundary.Route(&c[1]));
|
||||
network.Clear();assert(!network.Route(&c[0]));
|
||||
Network crowded;std::vector<IEntity> many(2050);for(auto& carrier:many){carrier.radio=2;crowded.Add(&carrier,&group,1,2);}assert(crowded.Overflow&&crowded.m_Nodes.Count()==2048);
|
||||
crowded.Build();assert(crowded.Overflow); // bounded dense graph work
|
||||
std::cout<<"PASS: real-carrier shortest-hop paths, 500m boundary, three-hop cap, no-manpack failure, hop timing, emitted-message survival, radio loss, capture, motion, timeout, reset and network bounds\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='eai-hq-network-') as tmp:
|
||||
cpp=Path(tmp)/'test.cpp';exe=Path(tmp)/'test';cpp.write_text(headers+route+network+tests)
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Wextra',str(cpp),'-o',str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
from policy_harness import body,run
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
s=body(ROOT/'addons/ai/Scripts/Game/ERR/ERR_MortarSalvo.c','static vector Offset(').replace('Math.','Math::')
|
||||
run('''#include <cassert>
|
||||
#include <cmath>
|
||||
struct vector { float x,y,z;};
|
||||
vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
namespace Math {constexpr float DEG2RAD=0.01745329252;float Sqrt(float n){return std::sqrt(n);}float Sin(float n){return std::sin(n);}float Cos(float n){return std::cos(n);}}
|
||||
constexpr int ROUNDS=8;constexpr float RADIUS_METRES=40;
|
||||
'''+s+'''
|
||||
int main(){for(int site=0;site<4;site++)for(int order=0;order<10;order++){
|
||||
float previous=0;
|
||||
for(int shot=0;shot<8;shot++){
|
||||
auto a=Offset(site,order,shot),b=Offset(site,order,shot);
|
||||
assert(a.x==b.x&&a.z==b.z&&a.y==0);
|
||||
float radius=std::hypot(a.x,a.z);assert(radius>previous&&radius<=40.0001);previous=radius;
|
||||
}
|
||||
}}
|
||||
''')
|
||||
print('PASS: migrated salvo offsets are deterministic, spread and bounded (firing controller pending)')
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Execute actual Enforce policy bodies as C++, without claiming engine coverage."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile,re
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
def body(path,signature):
|
||||
s=path.read_text();a=s.index(signature);b=s.index('{',a);i=b+1;depth=1
|
||||
while depth:
|
||||
depth+=(s[i]=='{')-(s[i]=='}');i+=1
|
||||
return s[a:i]
|
||||
def run(code):
|
||||
with tempfile.TemporaryDirectory(prefix='err-policy-') as tmp:
|
||||
p=Path(tmp);(p/'check.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-Wall','-Werror',str(p/'check.cpp'),'-o',str(p/'check')],check=True)
|
||||
subprocess.run([str(p/'check')],check=True)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Exercise actual Core alias, integer parsing and settings-boundary bodies.
|
||||
Native dispatch/storage and remote ownership remain separate tests.
|
||||
"""
|
||||
from policy_harness import ROOT,body,run
|
||||
admin=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
|
||||
settings=ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c'
|
||||
rules='\n'.join(body(path,sig) for path,sig in ((admin,'static string CoreSetting('),(admin,'static bool Integer('),(settings,'static bool Valid(')))
|
||||
rules=(rules.replace('string ', 'std::string ').replace('out int ', 'int& ')
|
||||
.replace('text.Length()', 'int(text.size())').replace('.IsEmpty()', '.empty()')
|
||||
.replace('.Substring(', '.substr(').replace('"0123456789".IndexOf(', 'std::string("0123456789").find(')
|
||||
.replace(' < 0) return false;', ' == std::string::npos) return false;').replace('text.ToInt()', 'std::stoi(text)'))
|
||||
run('#include <cassert>\n#include <string>\n'+rules+r'''
|
||||
int main(){
|
||||
assert(CoreSetting("intel","reveal")=="intelRevealChancePercent");
|
||||
assert(CoreSetting("mission","radio")=="radioDefenceSeconds");
|
||||
for(std::string section:{"", "hq", "mission", "intel", "unknown"})
|
||||
for(std::string option:{"", "interval", "carrying", "radio", "reveal", "unknown"}){
|
||||
bool recognized=(section=="intel"&&option=="reveal")||(section=="mission"&&option=="radio");
|
||||
assert(CoreSetting(section,option).empty()==!recognized);
|
||||
}
|
||||
int parsed=-1;
|
||||
for(std::string malformed:{"", "-1", "+1", "1.0", "10junk", "1e2", "0x10", " 50", "50 ", "1234567"})
|
||||
assert(!Integer(malformed,parsed));
|
||||
for(int value=0;value<=101;++value){
|
||||
assert(Integer(std::to_string(value),parsed)&&parsed==value);
|
||||
assert(Valid(CoreSetting("intel","reveal"),parsed)==(value<=100));
|
||||
}
|
||||
assert(Integer("000050",parsed)&&parsed==50);
|
||||
assert(Integer("999999",parsed)&&!Valid("intelRevealChancePercent",parsed));
|
||||
assert(!Valid("intelRevealChancePercent",-1));
|
||||
assert(Valid("radioDefenceSeconds",30)&&Valid("radioDefenceSeconds",1800));
|
||||
assert(!Valid("radioDefenceSeconds",29)&&!Valid("radioDefenceSeconds",1801));
|
||||
}
|
||||
''')
|
||||
print('PASS: Core document-discovery alias, companion-only alias exclusion, strict integer parsing and setting boundaries')
|
||||
@@ -0,0 +1,17 @@
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
source = body(ROOT/'addons/core/Scripts/Game/ERR/ERR_CasualtyGroundSupport.c', 'static bool SupportHit(')
|
||||
source = re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])', r'\1f', source)
|
||||
run('#include <cassert>\n' + source + '''
|
||||
int main() {
|
||||
assert(SupportHit(0,1,false,false));
|
||||
assert(SupportHit(0.999,0.85,false,false));
|
||||
assert(!SupportHit(1,1,false,false)); // no hit is no support
|
||||
assert(!SupportHit(-0.01,1,false,false));
|
||||
assert(!SupportHit(0.5,0.849,false,false));
|
||||
assert(!SupportHit(0.5,-1,false,false));
|
||||
assert(!SupportHit(0.5,1,true,false)); // own landing gear cannot support itself
|
||||
assert(!SupportHit(0.5,1,false,true)); // player/vehicle is not static parking support
|
||||
}
|
||||
''')
|
||||
print('PASS: actual short-cast hit, upward support normal, no self support and no dynamic support')
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
"""Run the actual fixture method against a narrow native-damage API fake."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/ERR_NativeCasualtyLoadProbe.c'
|
||||
method=body(p,'protected bool IncapacitateGroundCasualty(')
|
||||
method=method.replace('protected bool','bool').replace('m_Damage.','m_Damage->').replace('resilience.','resilience->').replace('ERR_Actors.','ERR_Actors::').replace('null','nullptr')
|
||||
run('''
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
struct NativeHitZone { float health=100; float GetHealth(){return health;} };
|
||||
using HitZone=NativeHitZone*;
|
||||
struct Actor { bool alive=true; };
|
||||
struct Damage {
|
||||
NativeHitZone resilience; bool enabled=true, missing=false, swallow=false; int forces=0;
|
||||
HitZone GetResilienceHitZone(){return missing ? nullptr : &resilience;}
|
||||
bool IsDamageHandlingEnabled(){return enabled;}
|
||||
void EnableDamageHandling(bool value){enabled=value;}
|
||||
void ForceUnconsciousness(){forces++; if(enabled && !swallow)resilience.health=0;}
|
||||
};
|
||||
struct ERR_Actors {static bool Alive(Actor* actor){return actor && actor->alive;}};
|
||||
Actor* m_Casualty; Damage* m_Damage;
|
||||
bool Check(bool value,const char*){return value;}
|
||||
template<class... Args> void PrintFormat(const char*,Args...){}
|
||||
'''+method+'''
|
||||
int main(){
|
||||
Actor actor; m_Casualty=&actor;
|
||||
for(bool original:{false,true}) {
|
||||
Damage damage; m_Damage=&damage; damage.enabled=original;
|
||||
assert(IncapacitateGroundCasualty());
|
||||
assert(damage.forces==1 && damage.resilience.health==0 && actor.alive);
|
||||
assert(damage.enabled==original);
|
||||
// No second artificial reduction: already-zero initial state must fail.
|
||||
assert(!IncapacitateGroundCasualty());
|
||||
assert(damage.enabled==original);
|
||||
}
|
||||
Damage blocked; blocked.enabled=false; blocked.swallow=true; m_Damage=&blocked;
|
||||
assert(!IncapacitateGroundCasualty());
|
||||
assert(blocked.forces==1 && !blocked.enabled && actor.alive);
|
||||
Damage missing; missing.missing=true; m_Damage=&missing;
|
||||
assert(!IncapacitateGroundCasualty() && missing.forces==0);
|
||||
m_Damage=nullptr; assert(!IncapacitateGroundCasualty());
|
||||
Damage dead; m_Damage=&dead; actor.alive=false;
|
||||
assert(!IncapacitateGroundCasualty() && dead.forces==0);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual fixture method permits one native resilience operation and restores either prior protection state, including failure paths')
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
path = ROOT/'addons/core/Scripts/Game/ERR/ERR_CasualtyLoadingService.c'
|
||||
source = '\n'.join(body(path, signature) for signature in [
|
||||
'static int RoundAction(', 'static bool ObserveTicket(', 'static bool Within(', 'static bool SettledFor(', 'static bool Ranges(', 'static bool AircraftMotion('])
|
||||
# Enforce float literals are single precision; C++ otherwise promotes these to doubles.
|
||||
source = re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])', r'\1f', source)
|
||||
source = source.replace('ERR_Phase.PREPARING', '0').replace('ERR_MissionRules.Terminal(phase)', '(phase == 5 || phase == 6)')
|
||||
run('#include <cassert>\n' + source + '''
|
||||
int main() {
|
||||
assert(RoundAction(0)==0); // pre-start roster callbacks must not latch Stop
|
||||
for (int phase=1;phase<=4;phase++) assert(RoundAction(phase)==1);
|
||||
assert(RoundAction(5)==2); assert(RoundAction(6)==2);
|
||||
for (int flags=0;flags<8;flags++) {
|
||||
bool committed=flags&1, physical=flags&2, authorized=flags&4;
|
||||
bool keep=physical && (committed || authorized);
|
||||
assert(ObserveTicket(committed,physical,authorized)==keep);
|
||||
}
|
||||
assert(ObserveTicket(true,true,false)); // disconnect after irreversible commit retains seat
|
||||
assert(!ObserveTicket(false,true,false)); // before commit it cancels
|
||||
// A late/reordered/expired lease cannot authorize a native entry.
|
||||
assert(Within(1000,1000,3000)); assert(Within(1000,3999,3000));
|
||||
assert(!Within(1000,4000,3000)); assert(!Within(1000,999,3000));
|
||||
assert(!Within(2147483000,-2147483000,3000));
|
||||
// A continuous observation window needs both sufficient settling and fresh data.
|
||||
assert(!SettledFor(-1,5000,5000)); assert(!SettledFor(1000,5999,5999));
|
||||
assert(SettledFor(1000,6000,6000)); assert(SettledFor(1000,6000,7999));
|
||||
assert(!SettledFor(1000,6000,8000)); assert(!SettledFor(1000,6000,5999));
|
||||
assert(!SettledFor(6000,6000,6000)); // recovered/moving body starts a new window
|
||||
// Every participant must independently be inside the physical rescue range.
|
||||
assert(Ranges(3,12,12)); assert(!Ranges(3.01,12,12));
|
||||
assert(!Ranges(3,12.01,12)); assert(!Ranges(3,12,12.01));
|
||||
assert(!Ranges(-1,0,0)); assert(!Ranges(0,-1,0)); assert(!Ranges(0,0,-1));
|
||||
// Contact alone cannot authorize an airborne, inverted, sliding or turning craft.
|
||||
assert(AircraftMotion(true,1,0,0)); assert(AircraftMotion(true,0.85,0.5,0.1));
|
||||
assert(!AircraftMotion(false,1,0,0)); assert(!AircraftMotion(true,-1,0,0));
|
||||
assert(!AircraftMotion(true,0.849,0,0)); assert(!AircraftMotion(true,1,0.501,0));
|
||||
assert(!AircraftMotion(true,1,0,0.101));
|
||||
}
|
||||
''')
|
||||
print('PASS: rescue lease expiry/reordering, fresh five-second settling, independent rescue ranges and grounded upright aircraft motion boundaries')
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Fixture boundaries and actual safe-exit policy; not an engine substitute."""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
source = ROOT/'addons/core/Scripts/Game/ERR/ERR_NativeCasualtyRemoveProbe.c'
|
||||
method = body(source,'static bool SafeExit(')
|
||||
method = re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])',r'\1f',method)
|
||||
run('#include <cassert>\n'+method+'''
|
||||
int main() {
|
||||
assert(SafeExit(0,0));
|
||||
assert(SafeExit(1.25,-0.25));
|
||||
assert(SafeExit(1.25,1.5));
|
||||
assert(!SafeExit(1.251,0));
|
||||
assert(!SafeExit(-0.01,0));
|
||||
assert(!SafeExit(1,-0.251));
|
||||
assert(!SafeExit(1,1.501));
|
||||
}
|
||||
''')
|
||||
text=source.read_text()
|
||||
assert text.startswith('#ifdef WORKBENCH')
|
||||
assert 'GetContextIndex("door_l01_door") >= 0' in text
|
||||
assert 'actions.Contains(m_Remove)' in text
|
||||
assert 'PickDoorIndexForPoint(m_Context.GetOrigin())' in text
|
||||
assert 'GetExitTeleportPointInfo()' in text
|
||||
assert 'm_Remove.GetActionDuration() == 1' in text
|
||||
assert 'm_Remove.CanBeShown(m_Caller)' in text and 'm_Remove.CanBePerformed(m_Caller)' in text
|
||||
assert text.count('m_Remove.PerformAction(') == 1
|
||||
assert 'm_Remove.SetCompartmentDoorInfoIndex(m_SavedDoor)' in body(source,'override void Cleanup(')
|
||||
assert 'if (m_RemoveStopped) return -1;' in body(source,'override int Tick(')
|
||||
for forbidden in ('GetOutVehicle(', 'GetOutVehicle_NoDoor(', 'KickFromVehicle(', 'SetUnconscious(', 'm_Casualty.Set', 'm_Casualty).Teleport', 'SetActive('):
|
||||
assert forbidden not in text,forbidden
|
||||
# Existing arbitrary-ground mechanism remains exactly its original contract.
|
||||
base=ROOT/'addons/core/Scripts/Game/ERR/ERR_NativeCasualtyLoadProbe.c'
|
||||
assert 'GetOutVehicle_NoDoor(exitTransform, true, false)' in body(base,'protected bool BeginExit(')
|
||||
assert 'exitTransform[3] = m_GroundPosition;' in body(base,'protected bool BeginExit(')
|
||||
assert body(base,'protected bool ExitSettled(').strip().endswith('return ERR_Actors.Alive(m_Casualty) && vector.Distance(m_Casualty.GetOrigin(), m_GroundPosition) < 4 && m_Seat.IsCompartmentAccessible();\n }')
|
||||
base_text=(ROOT/'addons/core/Scripts/Game/ERR/ERR_NativeBaseSmoke.c').read_text()
|
||||
assert base_text.count('m_ERRExtractionCase == "casualty-remove"') == 3
|
||||
assert 'm_ERRCasualtyLoadProbe = new ERR_NativeCasualtyRemoveProbe(this);' in base_text
|
||||
assert 'm_ERRBaseStep = 802; Transition(ERR_Phase.DEFEAT);' in base_text
|
||||
runner=(ROOT/'tools/headless_workbench.py').read_text()
|
||||
assert runner.count("'casualty-remove'") == 3
|
||||
compile(runner,'headless_workbench.py','exec')
|
||||
print('PASS: actual safe-exit boundary policy, native action/door selection, stop/cache cleanup, separate old mechanism and opt-in runner contracts')
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"""Exercise the production structural-ticket check after post-commit death.
|
||||
No synthetic fixture is claimed as network/owner-RPC proof.
|
||||
"""
|
||||
from policy_harness import ROOT, body, run
|
||||
path = ROOT/'addons/core/Scripts/Game/ERR/ERR_CasualtyLoadingService.c'
|
||||
physical = body(path, 'protected bool PhysicalTicket(')
|
||||
physical = (physical.replace('protected ', '').replace('ERR_CasualtyLoadTicket ticket', 'Ticket* ticket')
|
||||
.replace('ticket.', 'ticket->').replace('m_Mode.', 'm_Mode->')
|
||||
.replace('ERR_Phase.EXTRACTION', '4').replace('ERR_Actors.Alive', 'Alive')
|
||||
.replace('ERR_CasualtyLoadingRules.TransportBench', 'TransportBench').replace('null', 'nullptr'))
|
||||
policy = '\n'.join(body(path, signature) for signature in ['static bool ObserveTicket(', 'static bool Within('])
|
||||
run('''#include <cassert>
|
||||
struct Entity { bool alive=true; };
|
||||
bool Alive(Entity* entity){return entity && entity->alive;}
|
||||
bool TransportBench(Entity* seat,Entity* aircraft){return seat && aircraft;}
|
||||
struct Ticket { Entity* Aircraft; Entity* Casualty; Entity* Seat; };
|
||||
struct Mode {
|
||||
Entity* aircraft; bool active=true; int phase=4;
|
||||
bool Active(){return active;} int Phase(){return phase;}
|
||||
Entity* ExtractionVehicle(){return aircraft;}
|
||||
};
|
||||
''' + policy + '''
|
||||
struct Service { Mode* m_Mode;
|
||||
''' + physical + '''
|
||||
};
|
||||
int main(){
|
||||
Entity aircraft, body, bench; Mode mode{&aircraft}; Service service{&mode};
|
||||
Ticket ticket{&aircraft,&body,&bench};
|
||||
assert(service.PhysicalTicket(&ticket));
|
||||
body.alive=false; // server death after authorization; owner commit can still be in flight
|
||||
assert(service.PhysicalTicket(&ticket));
|
||||
assert(ObserveTicket(true,service.PhysicalTicket(&ticket),false));
|
||||
assert(!ObserveTicket(false,service.PhysicalTicket(&ticket),false));
|
||||
assert(Within(1000,8999,8000)); // keep observation reservation through its budget
|
||||
assert(!Within(1000,9000,8000)); // eventually release, without another owner command
|
||||
ticket.Casualty=nullptr; assert(!service.PhysicalTicket(&ticket));
|
||||
ticket.Casualty=&body; ticket.Seat=nullptr; assert(!service.PhysicalTicket(&ticket));
|
||||
ticket.Seat=&bench; aircraft.alive=false; assert(!service.PhysicalTicket(&ticket));
|
||||
aircraft.alive=true; mode.active=false; assert(!service.PhysicalTicket(&ticket));
|
||||
mode.active=true; mode.phase=5; assert(!service.PhysicalTicket(&ticket));
|
||||
}
|
||||
''')
|
||||
print('PASS: actual committed-ticket structure preserves reservation after death, rejects missing bodies/seats and ends at observation expiry')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Execute the actual opt-in Load body; no native engine or GUI is launched."""
|
||||
from policy_harness import ROOT, body, run
|
||||
source=ROOT/'addons/core/Scripts/GameLib/ERR/ERR_DiagnosticNoSplash.c'
|
||||
load=body(source,'override void Load()').replace('override void','void').replace('System.','System::').replace('super.Load()', 'Base::Load()')
|
||||
run('''#include <cassert>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
struct System {
|
||||
static bool noSplash, explicitScenario;
|
||||
static string project;
|
||||
static bool IsCLIParam(const char*) { return noSplash; }
|
||||
static bool GetCLIParam(const char*, string& value) {value=project;return explicitScenario;}
|
||||
};
|
||||
bool System::noSplash=false; bool System::explicitScenario=false; string System::project;
|
||||
int diagnostics=0;
|
||||
void Print(const char*) {diagnostics++;}
|
||||
struct Base {
|
||||
int nativeLoads=0, s_iSplashShown=0;
|
||||
void Load(){nativeLoads++;s_iSplashShown++;}
|
||||
};
|
||||
struct Subject:Base {
|
||||
bool m_ERRDiagnosticSplashSkipped=false;
|
||||
''' + load + '''
|
||||
};
|
||||
int main(){
|
||||
for(bool flag:{false,true}) for(bool explicitRequest:{false,true})
|
||||
for(const string& project:{string("core"),string("chat"),string(""),string("other")}) {
|
||||
System::noSplash=flag;System::explicitScenario=explicitRequest;System::project=project;
|
||||
diagnostics=0;Subject instance;
|
||||
instance.Load();instance.Load();instance.Load();
|
||||
bool selected=flag && explicitRequest && (project=="core" || project=="chat");
|
||||
assert(instance.nativeLoads==(selected ? 0 : 3));
|
||||
assert(instance.s_iSplashShown==(selected ? 1 : 3));
|
||||
assert(diagnostics==(selected ? 1 : 0));
|
||||
assert(instance.m_ERRDiagnosticSplashSkipped==selected);
|
||||
}
|
||||
// Recreated splash instance on addon reload remains independently bounded.
|
||||
System::noSplash=true;System::explicitScenario=true;System::project="core";
|
||||
Subject first,reloaded;diagnostics=0;
|
||||
first.Load();reloaded.Load();reloaded.Load();
|
||||
assert(first.s_iSplashShown==1 && reloaded.s_iSplashShown==1 && diagnostics==2);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual noSplash Load body requires both explicit diagnostic and noSplash, preserves vanilla otherwise, and handles repeated Load/reload')
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Execute actual environment parsing/formatting and settings policy bodies.
|
||||
Calendar validity expectations come from Python's independent datetime calendar.
|
||||
"""
|
||||
from datetime import date
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
settings = ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c'
|
||||
service = ROOT/'addons/core/Scripts/Game/ERR/ERR_EnvironmentService.c'
|
||||
admin = ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
|
||||
|
||||
def translate(source):
|
||||
source = (source.replace('string ', 'std::string ').replace('out int ', 'int& ')
|
||||
.replace('text.Length()', 'int(text.size())').replace('.IsEmpty()', '.empty()')
|
||||
.replace('.Substring(', '.substr(')
|
||||
.replace('"0123456789".IndexOf(', 'std::string("0123456789").find(')
|
||||
.replace(' < 0) return false;', ' == std::string::npos) return false;')
|
||||
.replace('text.ToInt()', 'std::stoi(text)')
|
||||
.replace('ERR_CommandArguments.Integer', 'Integer').replace('ERR_Settings.Valid', 'Valid'))
|
||||
return re.sub(r'(\w+)\.ToString\(\)', r'std::to_string(\1)', source)
|
||||
|
||||
functions='\n'.join(translate(body(path, sig)) for path, sig in (
|
||||
(settings,'static bool Valid('), (settings,'static int AddedDefault('),
|
||||
(admin,'static bool Integer('), (service,'static string Pad2('),
|
||||
(service,'static string Date('), (service,'static bool ParseDate('),
|
||||
(service,'static string ProfileName('), (service,'static string WeatherStateName('),
|
||||
(service,'static bool ParseProfile('),
|
||||
))
|
||||
cases=[]
|
||||
# Every calendar boundary in range, including leap/century rules and malformed days.
|
||||
for year in range(1900,2051):
|
||||
for month in range(1,13):
|
||||
for day in (0,1,28,29,30,31,32):
|
||||
code=(year-1900)*10000+month*100+day
|
||||
text=f'{year:04}-{month:02}-{day:02}'
|
||||
try: date(year,month,day); valid=True
|
||||
except ValueError: valid=False
|
||||
cases.append(f'{{{code},"{text}",{str(valid).lower()}}}')
|
||||
run('#include <cassert>\n#include <string>\n'+functions+'\nstruct Case {int code; const char* text; bool valid;};\nstatic Case cases[]={' + ','.join(cases) + '};\nint main(){int parsed=-1;' + r'''
|
||||
for (auto c:cases) {assert(Valid("startDateCode",c.code)==c.valid); assert(ParseDate(c.text,parsed)==c.valid); if(c.valid)assert(parsed==c.code && Date(parsed)==c.text);}
|
||||
assert(AddedDefault("startDateCode")==0 && AddedDefault("weatherProfile")==0);
|
||||
assert(Valid("startDateCode",0) && Date(0)=="authored" && ParseDate("authored",parsed) && parsed==0);
|
||||
for(std::string text:{"1900-00-00","1900-00-01","2050-13-01","1899-12-31","2051-01-01","1989-6-01","1989-06-1","1989-06-01junk"," 1989-06-01","1989-06-01 ","198a-06-01","", "1989/06/01"}) assert(!ParseDate(text,parsed));
|
||||
assert(!Valid("startDateCode",-1) && !Valid("startDateCode",1501232));
|
||||
for(int p=0;p<=3;p++){assert(Valid("weatherProfile",p)); assert(ParseProfile(ProfileName(p),parsed)&&parsed==p);}
|
||||
assert(ParseProfile("rainy",parsed)&&parsed==3);
|
||||
assert(!ParseProfile("random",parsed)&&!ParseProfile("storm",parsed));
|
||||
assert(!Valid("weatherProfile",-1)&&!Valid("weatherProfile",4));
|
||||
assert(WeatherStateName(0)=="" && WeatherStateName(1)=="Clear" && WeatherStateName(2)=="Overcast" && WeatherStateName(3)=="Rainy");
|
||||
}
|
||||
''')
|
||||
print('PASS: native settings policy matches calendar boundaries 1900-2050, date/profile parsing is strict, authored defaults preserve old worlds')
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Negative runner checks stop before any native process/audio initialization."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
class EnvironmentRestartRunnerTests(unittest.TestCase):
|
||||
def invoke(self, profile, *extra):
|
||||
return subprocess.run([sys.executable, str(ROOT/'tools/headless_workbench.py'),
|
||||
'--compat', str(profile/'missing-prefix'), '--profile', str(profile),
|
||||
'--smoke', 'environment-restart', '--project', 'core', '--rounds', '2',
|
||||
*extra], text=True, capture_output=True)
|
||||
|
||||
def test_requires_two_core_rounds(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for extra in [('--rounds','1'), ('--rounds','3'), ('--project','chat')]:
|
||||
with self.subTest(extra=extra):
|
||||
result = self.invoke(Path(directory), *extra)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('requires --project core --rounds 2', result.stderr)
|
||||
|
||||
def test_every_existing_fixture_file_is_refused_and_preserved(self):
|
||||
for name in ['erri-environment-restart.json', 'erri-environment-restart.json.bak',
|
||||
'erri-environment-restart.json.tmp', 'erri-environment-restart-round.json']:
|
||||
with self.subTest(name=name), tempfile.TemporaryDirectory() as directory:
|
||||
profile = Path(directory)
|
||||
path = profile/name
|
||||
path.write_text('prior evidence')
|
||||
result = self.invoke(profile)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('fixture paths must not preexist', result.stderr)
|
||||
self.assertEqual(path.read_text(), 'prior evidence')
|
||||
|
||||
def test_broken_fixture_symlink_is_refused(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
profile = Path(directory)
|
||||
(profile/'erri-environment-restart.json').symlink_to(profile/'missing-target')
|
||||
result = self.invoke(profile)
|
||||
self.assertIn('fixture paths must not preexist', result.stderr)
|
||||
self.assertTrue((profile/'erri-environment-restart.json').is_symlink())
|
||||
|
||||
def test_empty_fixture_reaches_existing_prefix_guard_without_side_effects(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
profile = Path(directory)
|
||||
result = self.invoke(profile)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('must be a disposable cloned prefix', result.stderr)
|
||||
self.assertEqual(list(profile.iterdir()), [])
|
||||
|
||||
def test_existing_environment_outcome_does_not_acquire_restart_path_policy(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
profile = Path(directory)
|
||||
(profile/'erri-environment-restart.json').write_text('prior evidence')
|
||||
result = self.invoke(profile, '--smoke', 'environment', '--rounds', '1')
|
||||
self.assertIn('must be a disposable cloned prefix', result.stderr)
|
||||
self.assertNotIn('fixture paths must not preexist', result.stderr)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"""Execute production time policies; do not claim native simulation coverage."""
|
||||
from policy_harness import ROOT, body, run
|
||||
source = ROOT/'addons/core/Scripts/Game/ERR/ERR_ExtractionPlacement.c'
|
||||
methods = '\n'.join(body(source, signature) for signature in (
|
||||
'static bool CanWake(', 'static bool Fresh(', 'static bool QualificationAllowed(', 'static bool Stable('))
|
||||
run('#include <cassert>\n' + methods + '''
|
||||
int main() {
|
||||
assert(CanWake(true,0,0,30000));
|
||||
assert(CanWake(true,0,29999,30000));
|
||||
assert(!CanWake(false,0,1,30000));
|
||||
assert(!CanWake(true,1,1,30000)); // activation cannot be repeated
|
||||
assert(!CanWake(true,2,1,30000));
|
||||
assert(!CanWake(true,0,30000,30000)); // no wake after expired preparation
|
||||
assert(!CanWake(true,0,30001,30000));
|
||||
assert(QualificationAllowed(false,29999,30000));
|
||||
assert(!QualificationAllowed(false,30000,30000)); // exactly deadline is not first qualification
|
||||
assert(!QualificationAllowed(false,30001,30000));
|
||||
assert(QualificationAllowed(true,30000,30000)); // continuous monitoring may outlive settling
|
||||
assert(QualificationAllowed(true,90000,30000));
|
||||
assert(!Fresh(-1,0));
|
||||
assert(!Fresh(1000,999));
|
||||
assert(Fresh(1000,2500));
|
||||
assert(!Fresh(1000,2501)); // delayed callbacks restart the stability window
|
||||
assert(!Stable(-1,1000,2000));
|
||||
assert(!Stable(1000,999,2000));
|
||||
assert(!Stable(1000,1750,1999));
|
||||
assert(Stable(1000,1750,2000));
|
||||
assert(!Stable(1000,1000,2501));
|
||||
assert(!Stable(1000,2000,1999));
|
||||
}
|
||||
''')
|
||||
text = source.read_text()
|
||||
poll = body(source, 'ERR_PrepareResult Poll(')
|
||||
assert text.count('physics.SetActive(ActiveState.ACTIVE)') == 1
|
||||
assert poll.index('!QualificationAllowed(') < poll.index('m_Result = ERR_PrepareResult.READY;')
|
||||
assert poll.index('if (m_Stopped') < poll.index('physics.SetActive(')
|
||||
assert poll.index('!Replication.IsServer()') < poll.index('physics.SetActive(')
|
||||
assert poll.index('m_Mode.ExtractionVehicle() != m_Aircraft') < poll.index('physics.SetActive(')
|
||||
assert poll.index('m_WakeCount = 1;') < poll.index('physics.SetActive(')
|
||||
assert 'fuel.HasFuel() && ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)' in poll
|
||||
for forbidden in ('SetVelocity(', 'SetAngularVelocity(', 'SetTransform(', 'SetWorldTransform(', 'Teleport(', 'ApplyImpulse', 'EnableDamageHandling('):
|
||||
assert forbidden not in text, forbidden
|
||||
site = (ROOT/'addons/core/Scripts/Game/ERR/ERR_SiteService.c').read_text()
|
||||
assert 'if (m_ExtractionPlacement) m_ExtractionPlacement.Stop();' in site
|
||||
assert 'Fail(m_ExtractionPlacement.FailureReason())' in site
|
||||
assert 'pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING' in site
|
||||
assert 'SetSpawnImmediately(false)' in site and 'PurgeSpawnRequestsForGroup' in site
|
||||
probe = (ROOT/'addons/core/Scripts/Game/ERR/ERR_NativeCasualtyGroundProbe.c').read_text()
|
||||
assert 'placement.Ready() && placement.WakeCount() == 1' in probe
|
||||
assert 'SetActive(' not in probe
|
||||
print('PASS: actual one-shot deadline/stability policies and preparation ownership, fuel, cleanup integration contracts')
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Offline resource compatibility checks; these are not native loading tests."""
|
||||
import hashlib
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CORE = ROOT / "addons/core"
|
||||
VANILLA_NORMALIZED_SHA256 = {
|
||||
"US": "d8d16abbecadc6987ca7eed3b114446d0af5753535c891fcefc20d5b2d7db583",
|
||||
"USSR": "2d3d3af74d0da33017ae3132c613fc9c19c0d20cd850a9830bb208db59e4bf25",
|
||||
}
|
||||
OLD_VEHICLES = {
|
||||
"US": "{D83A5EFEEF6CBF61}Configs/EntityCatalog/US/Vehicles_EntityCatalog_US.conf",
|
||||
"USSR": "{09C2ED5F617A0390}Configs/EntityCatalog/USSR/Vehicles_EntityCatalog_USSR.conf",
|
||||
}
|
||||
|
||||
|
||||
class FactionCatalogScopeTests(unittest.TestCase):
|
||||
def test_only_vehicle_catalog_changes_from_installed_faction_profiles(self):
|
||||
for side in ("US", "USSR"):
|
||||
with self.subTest(side=side):
|
||||
text = (CORE / f"Configs/ERR/Factions/{side}.conf").read_text()
|
||||
restored, count = re.subn(
|
||||
r"\{[A-F0-9]{16}\}Configs/ERR/Factions/MissionVehicles\.conf",
|
||||
lambda _: OLD_VEHICLES[side], text,
|
||||
)
|
||||
self.assertEqual(count, 1)
|
||||
# Pin every other vanilla field, including all inventory/group
|
||||
# references, radio/identity/callsign/rank settings and base.
|
||||
self.assertEqual(hashlib.sha256(restored.encode()).hexdigest(),
|
||||
VANILLA_NORMALIZED_SHA256[side])
|
||||
|
||||
def test_vehicle_catalog_has_only_the_two_authored_assets(self):
|
||||
text = (CORE / "Configs/ERR/Factions/MissionVehicles.conf").read_text()
|
||||
self.assertRegex(text, r"^SCR_EntityCatalogMultiList\s*\{")
|
||||
prefabs = re.findall(r'm_sEntityPrefab\s+"([^\"]+)"', text)
|
||||
self.assertCountEqual(prefabs, [
|
||||
"{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et",
|
||||
"{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et",
|
||||
])
|
||||
self.assertEqual(len(prefabs), 2)
|
||||
|
||||
def test_world_uses_own_manager_without_full_vanilla_faction_inheritance(self):
|
||||
manager = (CORE / "Prefabs/ERR/FactionManager.et").read_text()
|
||||
self.assertIn("Prefabs/MP/Managers/Factions/FactionManager_Base.et", manager)
|
||||
self.assertNotIn("FactionManager_USxUSSR.et", manager)
|
||||
for side in ("US", "USSR"):
|
||||
self.assertIn(f"Configs/ERR/Factions/{side}.conf", manager)
|
||||
self.assertNotIn(f"Configs/Factions/{side}.conf", manager)
|
||||
layer = (CORE / "Worlds/ERR_Everon_Layers/default.layer").read_text()
|
||||
self.assertEqual(layer.count("Prefabs/ERR/FactionManager.et"), 1)
|
||||
self.assertNotIn("FactionManager_USxUSSR.et", layer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"""Run actual ownership cancellation/Stop bodies with shared-array fixtures."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/core/Scripts/Game/ERR/ERR_SiteService.c'
|
||||
s='\n'.join(body(p, sig) for sig in ('void CancelInitialGuardRequests(', 'void Stop('))
|
||||
for old,new in [
|
||||
('GetGame().','GetGame()->'),
|
||||
('ChimeraAIWorld world', 'ChimeraAIWorld* world'),
|
||||
('ChimeraAIWorld.Cast', 'ChimeraAIWorld::Cast'),
|
||||
('foreach (ERR_SiteRuntime site : m_Sites)', 'for (ERR_SiteRuntime* site : m_Sites)'),
|
||||
('site.Group', 'site->Group'),
|
||||
('site.InitialGuardCount', 'site->InitialGuardCount'),
|
||||
('site->Group.', 'site->Group->'),
|
||||
('new array<ResourceName>()', 'std::make_shared<Array<std::string>>()'),
|
||||
('world.Purge', 'world->Purge'),
|
||||
('m_ExtractionPlacement.', 'm_ExtractionPlacement->'),
|
||||
('ERR_PrepareResult.', 'ERR_PrepareResult::'),
|
||||
]: s=s.replace(old,new)
|
||||
run('''#include <cassert>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
template<typename T> using Array=std::vector<T>;
|
||||
enum class ERR_PrepareResult {FAILED=-1,PENDING,READY};
|
||||
struct Group {bool immediate=true; std::shared_ptr<Array<std::string>> m_aUnitPrefabSlots; int physical=0; void SetSpawnImmediately(bool b){immediate=b;} };
|
||||
struct ERR_SiteRuntime {struct Group* Group; int InitialGuardCount;};
|
||||
struct ChimeraAIWorld {
|
||||
Array<Group*> purged;
|
||||
static ChimeraAIWorld* Cast(ChimeraAIWorld* p){return p;}
|
||||
void PurgeSpawnRequestsForGroup(Group* group){
|
||||
assert(!group->immediate && group->m_aUnitPrefabSlots->empty());
|
||||
purged.push_back(group);
|
||||
}
|
||||
};
|
||||
struct Game {ChimeraAIWorld* world; ChimeraAIWorld* GetAIWorld(){return world;}};
|
||||
Game* game;
|
||||
Game* GetGame(){return game;}
|
||||
struct Placement { int stops=0; void Stop(){stops++;} };
|
||||
struct Owner {
|
||||
std::shared_ptr<Placement> m_ExtractionPlacement;
|
||||
Array<ERR_SiteRuntime*> m_Sites;
|
||||
bool m_Stopped=false;
|
||||
ERR_PrepareResult m_State=ERR_PrepareResult::PENDING;
|
||||
int routeStops=0;
|
||||
void StopPatrols(){routeStops++;}
|
||||
''' + s + '''
|
||||
};
|
||||
int main(){
|
||||
ChimeraAIWorld world; Game instance{&world}; game=&instance;
|
||||
auto authored=std::make_shared<Array<std::string>>(6,"soldier");
|
||||
Group empty{true,authored,0}, partial{true,authored,2}, control{true,authored,6}, unrequested{true,authored,0};
|
||||
ERR_SiteRuntime a{&empty,6},b{&partial,6},c{&control,6},ignored{&unrequested,0},missing{nullptr,6};
|
||||
Owner owner; owner.m_ExtractionPlacement=std::make_shared<Placement>(); owner.m_Sites={&a,&b,&ignored,&missing};
|
||||
owner.Stop(); owner.Stop();
|
||||
assert(owner.m_Stopped && owner.routeStops==1 && world.purged.size()==2 && owner.m_ExtractionPlacement->stops==1);
|
||||
assert(empty.m_aUnitPrefabSlots->empty() && partial.m_aUnitPrefabSlots->empty());
|
||||
assert(authored->size()==6 && control.m_aUnitPrefabSlots==authored && unrequested.m_aUnitPrefabSlots==authored);
|
||||
assert(empty.physical==0 && partial.physical==2 && control.physical==6);
|
||||
assert(control.immediate && unrequested.immediate);
|
||||
Owner ready; ready.m_State=ERR_PrepareResult::READY; ready.m_Sites={&c}; ready.Stop();
|
||||
assert(world.purged.size()==2 && control.immediate && control.m_aUnitPrefabSlots->size()==6);
|
||||
game=nullptr; Owner deletedGame; deletedGame.m_Sites={&c}; deletedGame.Stop();
|
||||
assert(world.purged.size()==2);
|
||||
}
|
||||
''')
|
||||
print('PASS: pending-only cancellation retires copied instance intent, preserves physical/control actors, and stops once')
|
||||
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise process refusal, command scope and evidence gates without native launch."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import headless_game as runner
|
||||
|
||||
|
||||
class GameRunnerTests(unittest.TestCase):
|
||||
def process(self, root, command, comm='wine', pid='123'):
|
||||
proc = root/pid
|
||||
proc.mkdir()
|
||||
(proc/'cmdline').write_bytes(command)
|
||||
(proc/'comm').write_text(comm)
|
||||
(proc/'environ').write_bytes(b'')
|
||||
return proc
|
||||
|
||||
def test_refuses_existing_game_variants_without_mutation(self):
|
||||
for command in (b'Z:/Games/ArmaReforgerSteamDiag.exe\0-world\0x',
|
||||
b'C:\\Games\\ArmaReforgerSteam.exe\0',
|
||||
b'proton\0run\0/Z/ArmaReforgerSteamDiag.exe\0'):
|
||||
with self.subTest(command=command), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
proc = self.process(root, command)
|
||||
with self.assertRaisesRegex(ValueError, 'game process already exists'):
|
||||
runner.assert_no_game(root)
|
||||
self.assertEqual((proc/'cmdline').read_bytes(), command)
|
||||
|
||||
def test_truncated_comm_still_refuses_empty_cmdline(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
self.process(root, b'', comm='ArmaReforgerStea')
|
||||
with self.assertRaisesRegex(ValueError, 'game process already exists'):
|
||||
runner.assert_no_game(root)
|
||||
|
||||
def test_unrelated_workbench_is_not_a_game_process(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
self.process(root, b'Z:/Workbench/ArmaReforgerWorkbenchSteamDiag.exe\0', comm='ArmaReforgerWor')
|
||||
runner.assert_no_game(root)
|
||||
|
||||
def test_unreadable_process_fails_closed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
self.process(root, b'')
|
||||
with patch.object(Path, 'read_bytes', side_effect=PermissionError('blocked')):
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot exclude an existing game'):
|
||||
runner.assert_no_game(root)
|
||||
|
||||
def test_same_prefix_process_refused_other_prefix_preserved(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
compat = root/'compat'
|
||||
proc = self.process(root, b'wineserver\0')
|
||||
(proc/'environ').write_bytes(('WINEPREFIX='+str(compat/'pfx')).encode()+b'\0')
|
||||
with self.assertRaisesRegex(ValueError, 'prefix already has a process'):
|
||||
runner.assert_prefix_idle(compat, root)
|
||||
runner.assert_prefix_idle(root/'different', root)
|
||||
|
||||
def test_nondumpable_native_service_does_not_require_environment(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
proc = self.process(root, b'/usr/lib/systemd/systemd\0--user\0', comm='systemd')
|
||||
original = Path.read_bytes
|
||||
def protected_read(path):
|
||||
if path.name == 'environ':
|
||||
self.fail('Unrelated native service environment must not be read')
|
||||
return original(path)
|
||||
with patch.object(Path, 'read_bytes', protected_read), patch.object(runner.os, 'readlink', side_effect=PermissionError('non-dumpable')):
|
||||
runner.assert_prefix_idle(root/'compat', root)
|
||||
self.assertTrue(proc.exists())
|
||||
|
||||
def test_runtime_candidates_require_environment_even_if_exe_is_protected(self):
|
||||
for command, comm in [(b'/usr/bin/wineserver\0', 'wineserver'),
|
||||
(b'/usr/bin/python3\0/Steam/Proton - Experimental/proton\0run\0', 'python3'),
|
||||
(b'/usr/bin/gamescope\0--backend\0headless\0', 'gamescope'),
|
||||
(b'C:\\windows\\system32\\services.exe\0', 'services.exe')]:
|
||||
with self.subTest(comm=comm), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory); self.process(root, command, comm=comm)
|
||||
original = Path.read_bytes
|
||||
def protected_read(path):
|
||||
if path.name == 'environ':
|
||||
raise PermissionError('protected candidate')
|
||||
return original(path)
|
||||
with patch.object(Path, 'read_bytes', protected_read), patch.object(runner.os, 'readlink', side_effect=PermissionError('protected exe')):
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot verify disposable prefix'):
|
||||
runner.assert_prefix_idle(root/'compat', root)
|
||||
|
||||
def test_unclassifiable_process_remains_fail_closed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory); self.process(root, b'', comm='unknown')
|
||||
with patch.object(runner.os, 'readlink', side_effect=PermissionError('protected exe')):
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot classify possible Wine'):
|
||||
runner.assert_prefix_idle(root/'compat', root)
|
||||
|
||||
def test_exact_systemd_pam_helper_is_native_without_environment(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory); self.process(root, b'(sd-pam)\0', comm='(sd-pam)')
|
||||
original = Path.read_bytes
|
||||
def protected_read(path):
|
||||
if path.name == 'environ':
|
||||
self.fail('Native PAM helper environment must not be read')
|
||||
return original(path)
|
||||
with patch.object(Path, 'read_bytes', protected_read), patch.object(runner.os, 'readlink', side_effect=PermissionError('non-dumpable')):
|
||||
runner.assert_prefix_idle(root/'compat', root)
|
||||
|
||||
def test_observed_native_apache_signature_is_narrowly_classified(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
proc = self.process(root, b'apache2\0-DFOREGROUND\0', comm='apache2')
|
||||
with patch.object(runner.os, 'readlink', side_effect=PermissionError('protected exe')):
|
||||
self.assertFalse(runner.prefix_process_candidate(proc))
|
||||
(proc/'cmdline').write_bytes(b'apache2\0-DFOREGROUND\0/Steam/proton\0')
|
||||
self.assertTrue(runner.prefix_process_candidate(proc))
|
||||
(proc/'cmdline').write_bytes(b'apache2\0')
|
||||
with self.assertRaisesRegex(ValueError, 'Cannot classify possible Wine'):
|
||||
runner.prefix_process_candidate(proc)
|
||||
|
||||
def test_bare_matching_names_are_not_generically_trusted(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
self.process(root, b'unknown\0', comm='unknown', pid='101')
|
||||
self.process(root, b'another\0', comm='another', pid='102')
|
||||
with patch.object(runner.os, 'readlink', side_effect=PermissionError('protected exe')):
|
||||
with self.assertRaises(ValueError) as raised:
|
||||
runner.assert_prefix_idle(root/'compat', root)
|
||||
self.assertIn('101', str(raised.exception))
|
||||
self.assertIn('102', str(raised.exception)) # report all blockers in one pass
|
||||
|
||||
def test_pam_name_does_not_hide_runtime_arguments(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
proc = self.process(root, b'(sd-pam)\0/Steam/proton\0', comm='(sd-pam)')
|
||||
with patch.object(runner.os, 'readlink', side_effect=PermissionError('protected exe')):
|
||||
self.assertTrue(runner.prefix_process_candidate(proc))
|
||||
|
||||
def test_readonly_inspection_does_not_enter_launcher_or_audio(self):
|
||||
with patch.object(sys, 'argv', ['headless_game.py', '--compat', '/unused-prefix', '--inspect-processes']):
|
||||
with patch.object(runner, 'assert_no_game'), patch.object(runner, 'assert_prefix_idle', side_effect=ValueError('ambiguous process')):
|
||||
with patch.object(runner, 'run') as launch, patch.object(runner.subprocess, 'Popen') as spawn, patch.object(runner.subprocess, 'run') as commands, patch('builtins.print'):
|
||||
self.assertEqual(runner.main(), 1)
|
||||
launch.assert_not_called(); spawn.assert_not_called(); commands.assert_not_called()
|
||||
|
||||
def test_existing_unmarked_profile_and_steam_prefix_refused(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
compat = base/'compat'; (compat/'pfx').mkdir(parents=True)
|
||||
(compat/'pfx/system.reg').touch()
|
||||
profile = base/'profile'; profile.mkdir(); (profile/'sentinel').write_text('keep')
|
||||
with self.assertRaisesRegex(ValueError, 'not marked'):
|
||||
runner.validate_paths(base/'source', base/'steam', compat, profile)
|
||||
self.assertEqual((profile/'sentinel').read_text(), 'keep')
|
||||
with self.assertRaisesRegex(ValueError, 'outside Steam'):
|
||||
runner.validate_paths(base/'source', base, compat, base/'fresh')
|
||||
|
||||
def test_profile_cannot_overlap_owned_prefix_or_source(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory); compat = base/'compat'
|
||||
(compat/'pfx').mkdir(parents=True); (compat/'pfx/system.reg').touch()
|
||||
for profile in (base/'source/profile', compat/'profile', base):
|
||||
with self.assertRaisesRegex(ValueError, 'separate from source'):
|
||||
runner.validate_paths(base/'source', base/'steam', compat, profile)
|
||||
|
||||
def test_command_is_ordinary_game_with_explicit_successor_identity(self):
|
||||
for project, guid in [('core', '2528466DE220D91C'), ('chat', 'FA804F411BDDF2C6')]:
|
||||
command = runner.command_for(runner.ROOT, Path('/Steam'), Path('/compat'), Path('/profile'), project)
|
||||
self.assertEqual(command[command.index('--')+3], '/Steam/steamapps/common/Arma Reforger/ArmaReforgerSteam.exe')
|
||||
self.assertFalse(any('ArmaReforger_BE.exe' in arg or 'ArmaReforgerSteamDiag.exe' in arg for arg in command))
|
||||
self.assertEqual(command[command.index('-addons')+1], guid)
|
||||
self.assertNotIn('-world', command)
|
||||
self.assertEqual(command[command.index('-errOrdinaryScenario')+1], project)
|
||||
for flag in ['-noBackend', '-noSound', '-noSplash', '-noThrow', '-disableCrashReporter']:
|
||||
self.assertIn(flag, command)
|
||||
for unwanted in ['-server', '-gproj', '-errPlay', '-errSmoke', '-plugin', 'Workbench', '-silentCrashReport', '-noCrashDialog']:
|
||||
self.assertFalse(any(unwanted in arg for arg in command))
|
||||
|
||||
def test_ready_requires_game_module_without_workbench_and_normal_phase(self):
|
||||
module = 'SCRIPT : Module: Game; loaded 12x files; defines: "PLATFORM_WINDOWS,ENABLE_DIAG"; CRC32: a\n'
|
||||
phase = 'SCRIPT : Rereintroduced [mission]: phase 0 -> 1\nSCRIPT : ERR ORDINARY: header verified project=core systems=MissionSystems\nSCRIPT : ERR ORDINARY: local participant verified project=core phase=prison\n'
|
||||
self.assertTrue(runner.game_evidence(module+phase)['ready'])
|
||||
self.assertFalse(runner.game_evidence(phase)['ready'])
|
||||
self.assertFalse(runner.game_evidence(module)['ready'])
|
||||
self.assertFalse(runner.game_evidence(module.replace('ENABLE_DIAG', 'WORKBENCH')+phase)['ready'])
|
||||
for error in ['SCRIPT (E): failure', 'RESOURCES (E): missing', 'ERR SMOKE PASS: fixture leaked', 'Rereintroduced: scenario preparation failed']:
|
||||
self.assertTrue(runner.game_evidence(module+phase+error)['failed'])
|
||||
|
||||
def test_startup_requires_selected_header_and_actual_systems_marker(self):
|
||||
module = 'SCRIPT : Module: Game; defines: "PLATFORM_WINDOWS"; CRC32: a\n'
|
||||
phase = 'SCRIPT : Rereintroduced [mission]: phase 0 -> 1\n'
|
||||
core = 'SCRIPT : ERR ORDINARY: header verified project=core systems=MissionSystems\nSCRIPT : ERR ORDINARY: local participant verified project=core phase=prison\n'
|
||||
chat = 'SCRIPT : ERR ORDINARY: header verified project=chat systems=MissionSystems\nSCRIPT : ERR ORDINARY: local participant verified project=chat phase=prison\n'
|
||||
self.assertFalse(runner.game_evidence(module+phase, 'core')['ready'])
|
||||
self.assertFalse(runner.game_evidence(module+phase+core, 'chat')['ready'])
|
||||
self.assertTrue(runner.game_evidence(module+phase+chat, 'chat')['ready'])
|
||||
self.assertFalse(runner.game_evidence(module+phase+core.replace('MissionSystems', 'unknown'), 'core')['ready'])
|
||||
error = "GUI (E): Unknown class 'SCR_WidgetExportRuleRoot'\n"
|
||||
result = runner.game_evidence(error+module+phase+core, 'core')
|
||||
self.assertTrue(result['scenario_header_verified'])
|
||||
self.assertTrue(result['failed'])
|
||||
self.assertFalse(result['ready'])
|
||||
|
||||
def test_world_without_normal_local_admission_is_not_ready(self):
|
||||
world = ('SCRIPT : Module: Game; defines: "PLATFORM_WINDOWS"; CRC32: a\n'
|
||||
'SCRIPT : Rereintroduced [mission]: phase 0 -> 1\n'
|
||||
'SCRIPT : ERR ORDINARY: header verified project=core systems=MissionSystems\n')
|
||||
result = runner.game_evidence(world, 'core')
|
||||
self.assertTrue(result['scenario_header_verified'])
|
||||
self.assertFalse(result['local_participant_verified'])
|
||||
self.assertFalse(result['ready'])
|
||||
self.assertFalse(result['failed']) # incomplete admission may still arrive
|
||||
self.assertFalse(runner.game_evidence(world+'ERR ORDINARY: local participant verified project=chat phase=prison\n', 'core')['ready'])
|
||||
self.assertTrue(runner.game_evidence(world+'ERR ORDINARY: local participant verified project=core phase=prison\n', 'core')['ready'])
|
||||
|
||||
def test_no_engine_or_audio_spawn_during_import_and_process_refusal(self):
|
||||
with patch.object(runner, 'assert_no_game', side_effect=ValueError('existing game')):
|
||||
with patch.object(runner.subprocess, 'Popen') as spawn, patch.object(runner.subprocess, 'run') as commands:
|
||||
class Args:
|
||||
steam=Path('/steam'); compat=Path('/compat'); profile=Path('/profile')
|
||||
timeout=180; settle=15; failure_grace=0
|
||||
with self.assertRaisesRegex(ValueError, 'existing game'):
|
||||
runner.run(Args())
|
||||
spawn.assert_not_called(); commands.assert_not_called()
|
||||
|
||||
|
||||
class FailureGraceTests(unittest.TestCase):
|
||||
def test_exit_at_settle_boundary_is_not_success(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 5)
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 101))
|
||||
self.assertEqual(monitor.observe({'ready': True}, 106, True), 'process-exited')
|
||||
|
||||
def test_missing_readiness_resets_continuous_settle(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 5)
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 101))
|
||||
self.assertIsNone(monitor.observe({}, 104))
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 106))
|
||||
self.assertEqual(monitor.observe({'ready': True}, 111), 'game-started')
|
||||
|
||||
def test_default_error_stops_immediately(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 15)
|
||||
self.assertEqual(monitor.observe({'failed': True}, 101), 'game-failed')
|
||||
self.assertEqual(monitor.first_failure_at, 101)
|
||||
|
||||
def test_repeated_errors_do_not_renew_grace(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 15, 30)
|
||||
self.assertIsNone(monitor.observe({'failed': True}, 105))
|
||||
self.assertIsNone(monitor.observe({'failed': True}, 134.99))
|
||||
self.assertEqual(monitor.first_failure_at, 105)
|
||||
self.assertEqual(monitor.observe({'failed': True}, 135), 'game-failed')
|
||||
|
||||
def test_readiness_cannot_turn_latched_error_into_success(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 5, 30)
|
||||
self.assertIsNone(monitor.observe({'failed': True}, 101))
|
||||
for now in (102, 110, 130):
|
||||
self.assertIsNone(monitor.observe({'ready': True}, now))
|
||||
self.assertEqual(monitor.observe({'ready': True}, 131), 'game-failed')
|
||||
self.assertIsNone(monitor.ready_at)
|
||||
|
||||
def test_failure_after_readiness_aborts_settle_success(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 5, 30)
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 101))
|
||||
self.assertIsNone(monitor.observe({'failed': True}, 105))
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 106))
|
||||
self.assertEqual(monitor.observe({}, 135), 'game-failed')
|
||||
|
||||
def test_absolute_timeout_cuts_short_grace(self):
|
||||
monitor = runner.StartupMonitor(100, 60, 5, 30)
|
||||
self.assertIsNone(monitor.observe({'failed': True}, 159))
|
||||
self.assertEqual(monitor.observe({'ready': True}, 160), 'game-failed')
|
||||
self.assertEqual(monitor.timeout_result(), 'game-failed')
|
||||
self.assertEqual(monitor.deadline, 160)
|
||||
|
||||
def test_process_exit_during_grace_returns_failed_immediately(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 15, 30)
|
||||
monitor.observe({'failed': True}, 101)
|
||||
self.assertEqual(monitor.observe({}, 102, process_exited=True), 'game-failed')
|
||||
|
||||
def test_final_raw_analysis_cannot_erase_observed_failure(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 15, 30)
|
||||
monitor.observe({'failed': True}, 101)
|
||||
final = monitor.annotate({'ready': True, 'failed': False, 'engine_errors': [], 'game_module_observed': True})
|
||||
self.assertTrue(final['failure_observed_during_polling'])
|
||||
self.assertTrue(final['failed'])
|
||||
self.assertFalse(final['ready'])
|
||||
self.assertEqual(final['engine_errors'], []) # no invented final-log errors
|
||||
self.assertTrue(final['game_module_observed'])
|
||||
|
||||
def test_grace_does_not_delay_normal_success_or_exit(self):
|
||||
monitor = runner.StartupMonitor(100, 180, 15, 30)
|
||||
self.assertIsNone(monitor.observe({'ready': True}, 101))
|
||||
self.assertEqual(monitor.observe({'ready': True}, 116), 'game-started')
|
||||
monitor = runner.StartupMonitor(100, 180, 15, 30)
|
||||
self.assertEqual(monitor.observe({}, 101, process_exited=True), 'process-exited')
|
||||
self.assertEqual(monitor.observe({}, 280), 'timeout')
|
||||
self.assertEqual(monitor.timeout_result(), 'timeout')
|
||||
|
||||
def test_grace_bounds_rejected_before_any_native_mutation(self):
|
||||
for grace in (-1, 31):
|
||||
with self.subTest(grace=grace):
|
||||
with self.assertRaisesRegex(ValueError, 'failure-grace 0..30'):
|
||||
runner.StartupMonitor(100, 180, 15, grace)
|
||||
class Args:
|
||||
steam=Path('/steam'); compat=Path('/compat'); profile=Path('/profile')
|
||||
timeout=180; settle=15; failure_grace=grace
|
||||
with patch.object(runner, 'assert_no_game') as processes, patch.object(runner.subprocess, 'Popen') as spawn:
|
||||
with self.assertRaisesRegex(ValueError, 'failure-grace 0..30'):
|
||||
runner.run(Args())
|
||||
processes.assert_not_called(); spawn.assert_not_called()
|
||||
|
||||
def test_cli_default_and_explicit_grace(self):
|
||||
for extra, expected in (([], 0), (['--failure-grace', '30'], 30)):
|
||||
with self.subTest(extra=extra), patch.object(sys, 'argv', ['headless_game.py', '--compat', '/unused', *extra]):
|
||||
with patch.object(runner, 'run', return_value=1) as run:
|
||||
self.assertEqual(runner.main(), 1)
|
||||
self.assertEqual(run.call_args.args[0].failure_grace, expected)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Execute initial readiness policy; native population timing needs engine tests."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p = ROOT/'addons/core/Scripts/Game/ERR/ERR_InitialGuardRules.c'
|
||||
s = '\n'.join(body(p, signature) for signature in ('static float TimeoutMilliseconds(', 'static ERR_PrepareResult State(')).replace('ERR_PrepareResult.', 'ERR_PrepareResult::')
|
||||
run('''#include <cassert>
|
||||
#include <initializer_list>
|
||||
enum class ERR_PrepareResult { FAILED=-1,PENDING,READY };
|
||||
''' + s + '''
|
||||
int main() {
|
||||
using R=ERR_PrepareResult;
|
||||
assert(TimeoutMilliseconds()==90000);
|
||||
for(int requested: {1,6,12}) {
|
||||
for(int physical=0;physical<=requested;physical++) {
|
||||
for(int alive=0;alive<=physical;alive++) {
|
||||
assert(State(requested,physical,alive,false,false)==R::PENDING);
|
||||
assert(State(requested,physical,alive,true,false)==((alive==requested)?R::READY:R::PENDING));
|
||||
assert(State(requested,physical,alive,true,true)==R::FAILED);
|
||||
assert(State(requested,physical,alive,false,true)==R::FAILED);
|
||||
}
|
||||
}
|
||||
assert(State(requested,requested+1,requested,true,false)==R::FAILED);
|
||||
}
|
||||
assert(State(0,0,0,true,false)==R::FAILED);
|
||||
assert(State(6,2,3,false,false)==R::FAILED);
|
||||
assert(State(6,-1,0,false,false)==R::FAILED);
|
||||
assert(State(6,1,-1,false,false)==R::FAILED);
|
||||
}
|
||||
''')
|
||||
print('PASS: requested guard population requires complete physical conscious membership before deadline')
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
"""Synthetic identity regression using actual Enforce inventory/count method bodies.
|
||||
|
||||
This checks source policy only; it does not simulate native inventory transfers.
|
||||
"""
|
||||
import unittest
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
|
||||
def translated(source):
|
||||
for old, new in [
|
||||
('InventoryStorageManagerComponent inventory', 'InventoryStorageManagerComponent* inventory'),
|
||||
('notnull array<IEntity> items', 'Array<Entity*>& items'),
|
||||
('out array<IEntity> items', 'Array<Entity*>& items'),
|
||||
('array<IEntity>', 'Array<Entity*>'),
|
||||
('IEntity item', 'Entity* item'),
|
||||
('foreach (', 'for ('),
|
||||
('inventory.GetItems', 'inventory->GetItems'),
|
||||
('item.GetPrefabData()', 'item->GetPrefabData()'),
|
||||
('.GetPrefabName()', '->GetPrefabName()'),
|
||||
('ERR_Inventory.', 'ERR_Inventory::'),
|
||||
('int count;', 'int count = 0;'),
|
||||
]:
|
||||
source = source.replace(old, new)
|
||||
return source
|
||||
|
||||
|
||||
class InventoryEnumerationPolicy(unittest.TestCase):
|
||||
def test_duplicate_references_distinct_rounds_and_cross_inventory_union(self):
|
||||
helper = ROOT / 'addons/core/Scripts/Game/ERR/ERR_Inventory.c'
|
||||
mortar = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_MortarBattery.c'
|
||||
methods = '\n'.join(translated(body(helper, signature)) for signature in (
|
||||
'static int AppendUnique(', 'static int Items('))
|
||||
count = translated(body(mortar, 'int InventoryShells('))
|
||||
run(r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
template <typename T> struct Array : std::vector<T> {
|
||||
using std::vector<T>::vector;
|
||||
bool Contains(T v) const { return std::find(this->begin(),this->end(),v)!=this->end(); }
|
||||
void Insert(T v) { this->push_back(v); }
|
||||
void Clear() { this->clear(); }
|
||||
int Count() const { return this->size(); }
|
||||
};
|
||||
struct Prefab { std::string name; std::string GetPrefabName() { return name; } };
|
||||
struct Entity { Prefab* prefab; Prefab* GetPrefabData() { return prefab; } };
|
||||
struct InventoryStorageManagerComponent {
|
||||
Array<Entity*> raw;
|
||||
void GetItems(Array<Entity*>& out) { out = raw; }
|
||||
};
|
||||
const std::string SHELL = "mortar-shell";
|
||||
struct ERR_Inventory {
|
||||
''' + methods + '\n};\n' + count + r'''
|
||||
int main() {
|
||||
Prefab shell{SHELL}, watch{"watch"};
|
||||
Entity roundA{&shell}, roundB{&shell}, watchA{&watch}, missingPrefab{nullptr};
|
||||
InventoryStorageManagerComponent one{{&roundA,&roundA,nullptr,&roundB,&watchA,&watchA,&missingPrefab}};
|
||||
Array<Entity*> unique{&watchA};
|
||||
assert(ERR_Inventory::Items(&one,unique)==4);
|
||||
assert(unique[0]==&roundA && unique[1]==&roundB && unique[2]==&watchA && unique[3]==&missingPrefab);
|
||||
assert(InventoryShells(&one)==2); // Different shells share one prefab, not one identity.
|
||||
assert(one.raw.size()==7 && one.raw[0]==one.raw[1]); // Enumeration remains untouched.
|
||||
InventoryStorageManagerComponent two{{&roundB,&roundB,&roundA}};
|
||||
assert(ERR_Inventory::AppendUnique(&two,unique)==4); // Cross-inventory repeated identities.
|
||||
assert(ERR_Inventory::AppendUnique(nullptr,unique)==4);
|
||||
assert(ERR_Inventory::Items(nullptr,unique)==0 && unique.empty());
|
||||
assert(InventoryShells(nullptr)==0);
|
||||
InventoryStorageManagerComponent repeated{{&roundA,&roundA,&roundA}};
|
||||
assert(InventoryShells(&repeated)==1);
|
||||
InventoryStorageManagerComponent empty{{nullptr,&missingPrefab,&watchA}};
|
||||
assert(InventoryShells(&empty)==0);
|
||||
}
|
||||
''')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Execute actual resolver/context/latch bodies with opaque native handles.
|
||||
UUID generation/nullness and native session/controller APIs are substituted;
|
||||
this is not authentication, native GenV4 or multiplayer evidence.
|
||||
"""
|
||||
from policy_harness import ROOT, body, run
|
||||
import re
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_LocalAdmission.c'
|
||||
s=source.read_text().split('\nmodded class ERR_GameMode')[0]
|
||||
s=s[s.index('class ERR_LocalAdmissionRules'):]
|
||||
s=s.replace('class ERR_LocalAdmissionRules','struct ERR_LocalAdmissionRules').replace('class ERR_LocalAdmission\n','struct ERR_LocalAdmission\n')
|
||||
s=s.replace('\n}\n','\n};\n').replace('protected ','')
|
||||
s=s.replace('void ERR_LocalAdmission(', 'ERR_LocalAdmission(')
|
||||
s=s.replace('ERR_GameMode', 'Mode').replace('SCR_PlayerController', 'Controller').replace('PlayerManager', 'Manager')
|
||||
s=re.sub(r'\b(Mode|Controller|Manager) (mode|controller|manager|m_Mode|m_Controller)\b',r'\1* \2',s)
|
||||
s=s.replace('Mode.Get()', 'Mode::Get()').replace('RplMode.None','0')
|
||||
s=s.replace('Replication.IsServer()', 'isServer').replace('RplSession.Mode()', 'sessionMode')
|
||||
s=s.replace('GetGame().', 'GetGame()->').replace('manager.', 'manager->').replace('controller.', 'controller->')
|
||||
s=s.replace('System.GetCLIParam', 'System::GetCLIParam').replace('string project;', 'std::string project;')
|
||||
s=s.replace('bool connected, sameManager;', 'bool connected=false, sameManager=false;').replace('int controllerId;', 'int controllerId=0;')
|
||||
s=s.replace('ERR_LocalAdmissionRules.', 'ERR_LocalAdmissionRules::')
|
||||
s=s.replace('UUID.NULL_UUID','UUID::NULL_UUID').replace('UUID.GenV4()', 'UUID::GenV4()').replace('null','nullptr')
|
||||
# Enforce initializes managed fields; retain that contract in this C++ model.
|
||||
s=s.replace('Mode* m_Mode;', 'Mode* m_Mode=nullptr;').replace('Controller* m_Controller;', 'Controller* m_Controller=nullptr;')
|
||||
s=re.sub(r'\b(bool|int) (m_\w+);',r'\1 \2=0;',s)
|
||||
run('''#include <cassert>
|
||||
#include <string>
|
||||
int generated=0;bool isServer=true;int sessionMode=0;
|
||||
struct UUID {int value=0;bool IsNull(){return value<=0;}bool operator==(UUID other){return value==other.value;}static UUID NULL_UUID;static UUID GenV4(){return UUID{++generated+100};}};
|
||||
UUID UUID::NULL_UUID{0};
|
||||
struct Controller {int id=1;int GetPlayerId(){return id;}};
|
||||
struct Manager {int count=1;bool connected=true;Controller* controller=nullptr;int GetPlayerCount(){return count;}bool IsPlayerConnected(int id){return connected;}Controller* GetPlayerController(int id){return controller;}};
|
||||
struct Mode {static Mode* current;static Mode* Get(){return current;}};Mode* Mode::current=nullptr;
|
||||
struct Game {Manager* manager=nullptr;Controller* local=nullptr;Manager* GetManager(){return manager;}Controller* GetPlayerController(){return local;}};
|
||||
Game* game=nullptr;Game* GetGame(){return game;}void Print(const char*){}
|
||||
namespace System {bool GetCLIParam(const char*,std::string&){return false;}}
|
||||
template<class... Args> void PrintFormat(Args... args) {}
|
||||
'''+s+'''
|
||||
int main(){
|
||||
// Exact None only; every context condition independently matters.
|
||||
for(int mode=-1;mode<5;++mode)for(int count=0;count<3;++count)for(int flags=0;flags<16;++flags)
|
||||
assert(ERR_LocalAdmissionRules::Context(flags&1,mode,count,flags&2,flags&4,flags&8,1,1)==(mode==0&&count==1&&flags==15));
|
||||
assert(!ERR_LocalAdmissionRules::Context(true,0,1,true,true,true,0,0));
|
||||
assert(!ERR_LocalAdmissionRules::Context(true,0,1,true,true,true,1,2));
|
||||
Mode round,nextRound;Mode::current=&round;Controller owner,replacement;Manager manager;manager.controller=&owner;Game world{&manager,&owner};game=&world;
|
||||
ERR_LocalAdmission admission(&round);
|
||||
UUID selected=admission.Resolve(&round,1,&owner,UUID{0});assert(!selected.IsNull()&&admission.LocalKey()&&generated==1);
|
||||
assert(admission.Resolve(&round,1,&owner,UUID{0})==selected&&generated==1);
|
||||
assert(admission.Resolve(&round,1,&owner,UUID{55})==selected&&generated==1); // late BI arrival cannot make fresh life
|
||||
assert(admission.Validate());
|
||||
assert(!admission.Disconnected(2));assert(admission.Validate());
|
||||
assert(admission.Disconnected(1));assert(!admission.Validate());
|
||||
assert(admission.Resolve(&round,1,&owner,UUID{55}).IsNull()); // irreversible within this mode
|
||||
ERR_LocalAdmission native(&round);assert(native.Resolve(&round,1,&owner,UUID{55})==UUID{55});
|
||||
assert(!native.LocalKey()&&native.Resolve(&round,1,&owner,UUID{0})==UUID{55}&&generated==1);
|
||||
native.Stop();assert(native.Resolve(&round,1,&owner,UUID{0}).IsNull());assert(!native.Validate());
|
||||
// Replacement, mode switch, manager/local mismatch and additional connections latch.
|
||||
for(int fault=0;fault<6;++fault){
|
||||
sessionMode=0;Mode::current=&round;manager.controller=&owner;world.local=&owner;manager.count=1;manager.connected=true;
|
||||
ERR_LocalAdmission a(&round);assert(!a.Resolve(&round,1,&owner,UUID{0}).IsNull());
|
||||
if(fault==0)sessionMode=1;
|
||||
if(fault==1)manager.controller=&replacement;
|
||||
if(fault==2)world.local=&replacement;
|
||||
if(fault==3)manager.count=2;
|
||||
if(fault==4)manager.connected=false;
|
||||
if(fault==5)Mode::current=&nextRound;
|
||||
assert(!a.Validate());
|
||||
sessionMode=0;Mode::current=&round;manager.controller=&owner;world.local=&owner;manager.count=1;manager.connected=true;
|
||||
assert(!a.Validate());assert(a.Resolve(&round,1,&owner,UUID{88}).IsNull());
|
||||
}
|
||||
// A newly created round owns a fresh key. Old resolver cannot be reused.
|
||||
Mode::current=&nextRound;ERR_LocalAdmission next(&nextRound);UUID nextKey=next.Resolve(&nextRound,1,&owner,UUID{0});assert(!(nextKey==selected));
|
||||
assert(admission.Resolve(&nextRound,1,&owner,UUID{0}).IsNull());
|
||||
// Fresh network modes remain native even on an absent controller/local world,
|
||||
// after Stop, on null backend data, and with changing native identities.
|
||||
for(int mode: {1,2,3}){
|
||||
sessionMode=mode;int before=generated;ERR_LocalAdmission network(&nextRound);game=nullptr;
|
||||
assert(network.Resolve(&nextRound,9,nullptr,UUID{41})==UUID{41});
|
||||
assert(network.Resolve(&nextRound,9,nullptr,UUID{0}).IsNull());
|
||||
network.Stop();assert(network.Validate());
|
||||
assert(network.Resolve(&nextRound,9,nullptr,UUID{42})==UUID{42});
|
||||
assert(!network.Selected()&&!network.LocalKey()&&generated==before);game=&world;
|
||||
}
|
||||
// Incomplete local context cannot generate a key and cannot later self-repair.
|
||||
sessionMode=0;manager.count=0;int before=generated;ERR_LocalAdmission invalid(&nextRound);
|
||||
assert(invalid.Resolve(&nextRound,1,&owner,UUID{0}).IsNull()&&generated==before);
|
||||
manager.count=1;assert(invalid.Resolve(&nextRound,1,&owner,UUID{0}).IsNull());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual local admission context/selection/latch, same-round late identity, replacement/mode failure, fresh round and untouched network path')
|
||||
@@ -0,0 +1,29 @@
|
||||
from policy_harness import ROOT,body,run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_MissionRules.c'
|
||||
rules='\n'.join(body(source,x) for x in ['static bool Terminal(', 'static bool CanTransition(', 'static bool DefenceComplete(', 'static bool ContactFresh(', 'static int Discovery(']).replace('ERR_Phase.', 'ERR_Phase::')
|
||||
rules += '\n' + body(ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerRoster.c', 'static bool CanEnter(').replace('ERR_Phase.', 'ERR_Phase::')
|
||||
run('''#include <cassert>
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
'''+rules+'''
|
||||
int main(){
|
||||
for(int phase=-1;phase<=7;phase++){
|
||||
assert(!CanEnter(true,phase));
|
||||
assert(CanEnter(false,phase)==(phase>=1&&phase<=3));
|
||||
}
|
||||
for(int from=0;from<=6;from++)for(int to=0;to<=6;to++){
|
||||
bool expected=(from<5&&to==from+1)||(from>=1&&from<=4&&to==6);
|
||||
assert(CanTransition(from,to)==expected);
|
||||
}
|
||||
assert(!DefenceComplete(100,0,180,true));assert(DefenceComplete(180,0,180,true));
|
||||
assert(!DefenceComplete(180,0,180,false));assert(!DefenceComplete(300,-1,180,true));
|
||||
assert(!DefenceComplete(10,20,180,true));assert(!DefenceComplete(300,0,0,true));
|
||||
assert(ContactFresh(0,120000));assert(!ContactFresh(0,120001));
|
||||
assert(!ContactFresh(100,99));assert(!ContactFresh(-1,0));
|
||||
for(int roll=0;roll<100;roll++){
|
||||
assert(Discovery(0,roll,0)==1);assert(Discovery(0,roll,100)==2);
|
||||
assert(Discovery(1,roll,0)==2);assert(Discovery(2,roll,0)==2);
|
||||
assert(Discovery(0,roll,15)==(roll<15?2:1));
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: all phase transitions, terminal states, defence boundaries, stale/future observations, intel discovery')
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Actual Enforce policies and transaction observation body under narrow API fakes."""
|
||||
from policy_harness import ROOT, body, run
|
||||
r=ROOT/'addons/ai/Scripts/Game/ERR'
|
||||
readiness=r/'ERR_MortarReadiness.c'; transfer=r/'ERR_MortarSupplyTransfer.c'; battery=r/'ERR_MortarBattery.c'
|
||||
policy='\n'.join([body(readiness,'static ERR_MortarSupplyState Population('),body(readiness,'static bool AllocationValid('),body(readiness,'static int CycleGate('),body(transfer,'static bool Authority('),body(transfer,'static bool Proven(')])
|
||||
policy=policy.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::')
|
||||
run('''#include <cassert>
|
||||
enum class ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
|
||||
'''+policy+'''
|
||||
int main(){
|
||||
using S=ERR_MortarSupplyState;
|
||||
assert(Population(0,0,0,false,false)==S::PENDING);
|
||||
assert(Population(3,3,0,true,false)==S::PENDING);
|
||||
assert(Population(4,3,1,false,false)==S::PENDING);
|
||||
assert(Population(4,3,1,true,false)==S::READY);
|
||||
assert(Population(4,3,1,true,true)==S::FAILED);
|
||||
assert(Population(0,0,0,false,true)==S::FAILED);
|
||||
assert(Population(5,3,2,true,false)==S::FAILED);
|
||||
assert(Population(4,4,0,true,false)==S::FAILED);
|
||||
assert(AllocationValid(0)&&AllocationValid(9));
|
||||
assert(!AllocationValid(-1)&&!AllocationValid(10));
|
||||
assert(Authority(true,false,1,1));
|
||||
assert(!Authority(false,false,1,1));
|
||||
assert(!Authority(true,true,1,1));
|
||||
assert(!Authority(true,false,1,2));
|
||||
for(int bits=0;bits<16;bits++) {
|
||||
bool callback=bits&1, source=bits&2, destination=bits&4, exact=bits&8;
|
||||
assert(Proven(callback,source,destination,exact)==(bits==13));
|
||||
}
|
||||
int counter=0;
|
||||
assert(CycleGate(true,0,counter,8,1)==0); // no physical projectile, no count
|
||||
for(int i=0;i<1000;i++) if(CycleGate(true,1,counter,8,0)==1) counter++;
|
||||
assert(counter==0); // pending supply never invokes native super
|
||||
assert(CycleGate(true,1,counter,8,-1)==-1);
|
||||
if(CycleGate(true,1,counter,8,1)==1) counter++;
|
||||
assert(counter==1 && CycleGate(true,1,counter,8,1)==0); // same event not counted twice
|
||||
assert(CycleGate(false,2,counter,8,1)==-1);
|
||||
assert(CycleGate(true,8,7,8,-1)==1); // final confirmed round needs no ninth shell
|
||||
}
|
||||
''')
|
||||
# Retain actual control flow; translate only native member/type syntax and the
|
||||
# component lookup into a directly supplied fake item component.
|
||||
poll=body(transfer,'ERR_MortarSupplyState Poll(')
|
||||
poll=poll.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::').replace('Replication.IsServer()','Replication::IsServer()').replace('InventoryItemComponent item;','InventoryItemComponent* item=nullptr;').replace('if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));','if (m_Shell) item = m_Shell;').replace('item.GetParentSlot().GetStorage()','item->GetParentSlot()->GetStorage()').replace('item.GetParentSlot()','item->GetParentSlot()')
|
||||
ack=body(transfer,'void Acknowledge(')
|
||||
run('''#include <cassert>
|
||||
enum class ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
|
||||
struct Replication { static bool IsServer(){return true;} };
|
||||
struct Slot { void* storage; void* GetStorage(){return storage;} };
|
||||
struct InventoryItemComponent { Slot* slot; Slot* GetParentSlot(){return slot;} };
|
||||
'''+body(transfer,'static bool Proven(')+'''
|
||||
struct Transfer {
|
||||
bool valid=true,source=true,destination=false,m_Accepted=true,m_Invalidated=false,m_Resolved=false,m_Ready=false;
|
||||
int m_Source=1,m_Destination=2,m_Acknowledgement=0;
|
||||
float m_Deadline=8000;
|
||||
void* m_Storage=this;
|
||||
Slot slot{this}; InventoryItemComponent item{&slot}; InventoryItemComponent* m_Shell=&item;
|
||||
bool Valid(float){return valid&&!m_Invalidated;}
|
||||
bool Contains(int which){return m_Shell && (which==1 ? source : destination);}
|
||||
'''+ack+poll+'''
|
||||
};
|
||||
int main(){
|
||||
using S=ERR_MortarSupplyState;
|
||||
Transfer t; assert(t.Poll(0)==S::PENDING && !t.m_Resolved);
|
||||
t.Acknowledge(true); t.Acknowledge(false); assert(t.m_Acknowledgement==1);
|
||||
assert(t.Poll(1)==S::PENDING && !t.m_Resolved); // callback alone cannot supply
|
||||
t.destination=true; assert(t.Poll(2)==S::PENDING); // source still claims ownership
|
||||
t.source=false; t.slot.storage=nullptr; assert(t.Poll(3)==S::PENDING); // wrong storage
|
||||
t.slot.storage=t.m_Storage; assert(t.Poll(4)==S::READY && t.m_Resolved);
|
||||
Transfer stopped; stopped.m_Invalidated=true; stopped.source=false; stopped.destination=true; stopped.Acknowledge(true);
|
||||
assert(stopped.Poll(5)==S::FAILED && !stopped.m_Ready && stopped.m_Resolved);
|
||||
Transfer timeout; assert(timeout.Poll(8000)==S::FAILED && !timeout.m_Resolved && timeout.m_Invalidated);
|
||||
timeout.source=false;timeout.destination=true;timeout.Acknowledge(true);
|
||||
assert(timeout.Poll(9000)==S::FAILED && timeout.m_Resolved && !timeout.m_Ready); // late success cannot authorize
|
||||
Transfer late;late.source=false;late.destination=true;late.Acknowledge(true);
|
||||
assert(late.Poll(8000)==S::FAILED); // no deadline-boundary success bypass
|
||||
Transfer failed;failed.Acknowledge(false);failed.Acknowledge(true);
|
||||
assert(failed.Poll(1)==S::FAILED && failed.m_Acknowledgement==-1 && failed.m_Resolved);
|
||||
Transfer gone;gone.m_Shell=nullptr;gone.Acknowledge(true);
|
||||
assert(gone.Poll(1)==S::PENDING && !gone.m_Resolved); // ambiguous until deadline, never another move
|
||||
Transfer unauthorized;unauthorized.valid=false;
|
||||
assert(unauthorized.Poll(0)==S::FAILED && !unauthorized.m_Resolved);
|
||||
}
|
||||
''')
|
||||
# Source contract checks constrain integration that cannot run without the engine.
|
||||
b=battery.read_text();sys=(r/'ERR_MortarSystem.c').read_text();f=(r/'ERR_MortarFiring.c').read_text();rd=readiness.read_text();tr=transfer.read_text()
|
||||
update=body(battery,'void Update(float now)')
|
||||
assert update.index('m_Readiness.Poll(now)') < update.index('if (!Operational())')
|
||||
assert 'm_Allocated = true;' in rd and rd.count('TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL')==1
|
||||
assert rd.index('m_Allocated = true;') < rd.index('TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL')
|
||||
assert 'm_aUnitPrefabSlots = new array<ResourceName>()' in rd and 'PurgeSpawnRequestsForGroup(group)' in rd
|
||||
assert 'DeleteSpawned' not in body(readiness,'void Stop(')
|
||||
assert 'battery.Stop();' in body(r/'ERR_MortarSystem.c','void Stop(')
|
||||
assert tr.count('TryMoveItemToStorage(')==1 and 'GetItem()' not in tr.replace('callback.GetItem()','')
|
||||
assert 'm_AttemptedShells.Insert(item)' in b and 'm_AttemptedShells.Contains(item)' in b
|
||||
assert 'm_Transfer.CapturedOrder() == OrderId' in b
|
||||
assert 'battery.PendingSupplyReport' in sys and 'supply == ERR_MortarSupplyState.PENDING) return;' in sys
|
||||
shoot=f[f.index('modded class SCR_AIStaticArtilleryBehavior_ShootingLogic'):]
|
||||
assert shoot.index('battery.SupplyState(')<shoot.index('super.EOnTaskSimulate(')
|
||||
assert shoot.count('super.EOnTaskSimulate(')==1 and 'battery.SupplyGunner()' not in shoot
|
||||
print('PASS: actual startup/generation/ownership/timeout/callback policies and native-counter wait; integration preserves one-shot allocation and pending-crew cancellation')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Exercise the probe's actual native-event evidence gate, not artillery simulation."""
|
||||
from policy_harness import ROOT, body, run
|
||||
p=ROOT/'addons/ai/Scripts/Game/ERR/ERR_NativeMortarFiringProbe.c'
|
||||
s=body(p,'void NativeProjectile(')
|
||||
s=s.replace('IEntity gun, IEntity projectile','Entity* gun, Entity* projectile')
|
||||
s=s.replace('m_Battery.', 'm_Battery->').replace('projectile.GetOrigin()', 'projectile->GetOrigin()')
|
||||
s=s.replace('GetGame().GetWorld().GetWorldTime()', '20000.f')
|
||||
run('''#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
struct Entity { int GetOrigin(){return 0;} };
|
||||
struct Battery {bool CommandOpen=true,Released=true;int SourceId=7,OrderId=1;float AimedAt=19000,FlightTime=20;};
|
||||
struct Items {std::vector<Entity*> values;bool Contains(Entity* p){return std::find(values.begin(),values.end(),p)!=values.end();}void Insert(Entity* p){values.push_back(p);}};
|
||||
template<class... Args> void PrintFormat(Args... args) {}
|
||||
struct Probe {
|
||||
bool m_Stopped=false,m_BadProjectile=false;Battery* m_Battery;Entity* m_Gun;
|
||||
Items m_Projectiles;int m_Events=0,m_Step=1,m_Source=7;std::string m_BadReason;
|
||||
'''+s+'''
|
||||
};
|
||||
int main(){
|
||||
Entity gun,other,projectile;Battery battery;
|
||||
Probe valid;valid.m_Battery=&battery;valid.m_Gun=&gun;
|
||||
valid.NativeProjectile(&other,&projectile);assert(valid.m_Events==0&&!valid.m_BadProjectile);
|
||||
valid.NativeProjectile(&gun,&projectile);assert(valid.m_Events==1&&!valid.m_BadProjectile);
|
||||
valid.NativeProjectile(&gun,&projectile);assert(valid.m_Events==1&&valid.m_BadProjectile);
|
||||
for(int invalid=0;invalid<8;++invalid){
|
||||
battery=Battery();Probe p;p.m_Battery=&battery;p.m_Gun=&gun;
|
||||
if(invalid==0)battery.CommandOpen=false;
|
||||
if(invalid==1)battery.Released=false;
|
||||
if(invalid==2)battery.SourceId=8;
|
||||
if(invalid==3)battery.AimedAt=-1;
|
||||
if(invalid==4)battery.AimedAt=9999; // older than ten seconds
|
||||
if(invalid==5)battery.FlightTime=0;
|
||||
if(invalid==6)p.m_Step=2;
|
||||
if(invalid==7){p.NativeProjectile(&gun,nullptr);assert(p.m_BadProjectile&&p.m_Events==0);continue;}
|
||||
p.NativeProjectile(&gun,&projectile);
|
||||
assert(p.m_Events==1);assert(p.m_BadProjectile);
|
||||
}
|
||||
battery=Battery();Probe stopped;stopped.m_Battery=&battery;stopped.m_Gun=&gun;stopped.m_Stopped=true;
|
||||
stopped.NativeProjectile(&gun,&projectile);assert(stopped.m_Events==0);
|
||||
}
|
||||
''')
|
||||
print('PASS: actual native-event probe gate rejects duplicate/null/wrong-order/uncommitted/un-aimed events; unrelated guns and stopped observers cannot count shots')
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synthetic comparator regression only; no measured performance evidence."""
|
||||
import copy
|
||||
import csv
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location('compare_performance', ROOT / 'tools/compare_performance.py')
|
||||
comparison = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(comparison)
|
||||
|
||||
|
||||
class PerformanceComparisonTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory(prefix='erri-synthetic-performance-')
|
||||
self.addCleanup(self.directory.cleanup)
|
||||
self.root = Path(self.directory.name)
|
||||
template = {
|
||||
'variant': 'baseline', 'repeat_id': 'r1', 'artifact_id': 'synthetic-baseline', 'addon_ids': ['synthetic-base'],
|
||||
'engine_version': 'synthetic-engine',
|
||||
'hardware': {'cpu': 'synthetic-cpu', 'gpu': 'synthetic-gpu', 'ram_gib': 32, 'os': 'synthetic-os', 'driver': 'synthetic-driver'},
|
||||
'workload': {'map': 'synthetic-map', 'route': 'synthetic-route', 'players': 12, 'ai': 36, 'vehicles': 4, 'entities': 100,
|
||||
'settings': {'resolution': [1920, 1080], 'fps_limit': 0, 'vsync': False}, 'warmup_seconds': 30, 'duration_seconds': 60},
|
||||
'capture': {'kind': 'client_frame', 'environment': 'client', 'tool': 'synthetic-generator', 'version': '1'},
|
||||
'sample_file': 'unused.csv',
|
||||
}
|
||||
self.data = {'schema_version': 1, 'sample_origin': 'synthetic', 'runs': []}
|
||||
for repeat in range(1, 4):
|
||||
for variant, frame in [('baseline', 20), ('successor', 10)]:
|
||||
run = copy.deepcopy(template)
|
||||
run.update(variant=variant, repeat_id=f'r{repeat}', artifact_id=f'synthetic-{variant}', sample_file=f'{variant}-{repeat}.csv')
|
||||
run['addon_ids'] = [f'synthetic-{variant}']
|
||||
self.data['runs'].append(run)
|
||||
with (self.root / run['sample_file']).open('w', newline='') as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(comparison.COLUMNS)
|
||||
for index in range(1, 90000 // frame + 1):
|
||||
writer.writerow([index, index * frame, frame, 12, 36, 4, 100, 2])
|
||||
|
||||
def execute(self):
|
||||
manifest = self.root / 'synthetic.json'
|
||||
manifest.write_text(json.dumps(self.data))
|
||||
return comparison.compare(manifest)
|
||||
|
||||
def change_csv(self, transform):
|
||||
path = self.root / self.data['runs'][0]['sample_file']
|
||||
rows = path.read_text().splitlines()
|
||||
path.write_text('\n'.join(transform(rows)) + '\n')
|
||||
|
||||
def replace_frames(self, frames):
|
||||
path = self.root / self.data['runs'][0]['sample_file']
|
||||
with path.open('w', newline='') as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(comparison.COLUMNS)
|
||||
elapsed = 0
|
||||
for index, frame in enumerate(frames, 1):
|
||||
elapsed += frame
|
||||
writer.writerow([index, elapsed, frame, 12, 36, 4, 100, 2])
|
||||
|
||||
def test_paired_arithmetic_is_explicitly_synthetic_and_not_acceptance(self):
|
||||
report = self.execute()
|
||||
self.assertEqual(report['sample_origin'], 'synthetic')
|
||||
self.assertEqual(report['acceptance'], 'not_evaluated')
|
||||
self.assertEqual(report['aggregate']['baseline']['median_fps'], 50)
|
||||
self.assertEqual(report['aggregate']['successor']['p99_frame_ms'], 10)
|
||||
self.assertEqual(report['median_paired_deltas']['median_frame_reduction_percent'], 50)
|
||||
self.assertEqual(report['runs'][0]['samples_measured'], 3000)
|
||||
self.assertEqual(len(report['manifest_sha256']), 64)
|
||||
|
||||
def test_percentiles_and_warmup_exclusion(self):
|
||||
self.assertEqual(comparison.percentile(list(range(1, 101)), .99), 99)
|
||||
self.change_csv(lambda rows: [rows[0]] + [row.rsplit(',', 1)[0] + ',999' if i < 1500 else row for i, row in enumerate(rows[1:])])
|
||||
self.assertEqual(self.execute()['aggregate']['baseline']['median_ai_simulation_ms'], 2)
|
||||
|
||||
def test_rejects_mismatched_conditions(self):
|
||||
original = copy.deepcopy(self.data)
|
||||
mutations = [('hardware', 'cpu', 'other'), ('workload', 'ai', 35), ('workload', 'route', 'other'),
|
||||
('workload', 'settings', {'fps_limit': 30}), ('workload', 'warmup_seconds', 31), ('capture', 'environment', 'workbench_headless')]
|
||||
for group, key, value in mutations:
|
||||
with self.subTest(group=group, key=key):
|
||||
self.data = copy.deepcopy(original)
|
||||
self.data['runs'][1][group][key] = value
|
||||
with self.assertRaisesRegex(ValueError, 'mismatched'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_missing_pair_and_duplicate_repeat(self):
|
||||
self.data['runs'][-1]['repeat_id'] = 'unpaired'
|
||||
with self.assertRaisesRegex(ValueError, 'Every repeat'):
|
||||
self.execute()
|
||||
self.data['runs'][-1]['repeat_id'] = 'r1'
|
||||
with self.assertRaisesRegex(ValueError, 'Duplicate repeat'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_reused_samples(self):
|
||||
self.data['runs'][1]['sample_file'] = self.data['runs'][0]['sample_file']
|
||||
with self.assertRaisesRegex(ValueError, 'reused'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_incomplete_capture(self):
|
||||
self.change_csv(lambda rows: rows[:-1])
|
||||
with self.assertRaisesRegex(ValueError, 'incomplete recording'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_final_stall_hiding_most_of_measurement(self):
|
||||
# Before the coverage check this 1.2-second sample was accepted as a
|
||||
# 60-second measurement with 100 FPS, dropping its 60-second stall.
|
||||
self.replace_frames([20] * 1500 + [10] * 120 + [60000])
|
||||
with self.assertRaisesRegex(ValueError, 'only 2.000%.*boundary stalls'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_warmup_crossing_stall_hiding_measurement(self):
|
||||
# The long frame starts in warm-up and consumes 58.8 measured seconds.
|
||||
self.replace_frames([20] * 1499 + [58820] + [10] * 120)
|
||||
with self.assertRaisesRegex(ValueError, 'only 2.000%.*boundary stalls'):
|
||||
self.execute()
|
||||
|
||||
def test_short_boundary_crossings_are_visible_with_high_coverage(self):
|
||||
self.replace_frames([17] * 5295)
|
||||
run = self.execute()['runs'][0]
|
||||
self.assertGreaterEqual(run['measurement_coverage_fraction'], .99)
|
||||
self.assertLess(run['measurement_coverage_fraction'], 1)
|
||||
self.assertEqual(len(run['boundary_frames_excluded']), 2)
|
||||
overlap = sum(frame['measurement_overlap_ms'] for frame in run['boundary_frames_excluded'])
|
||||
self.assertAlmostEqual(run['measured_frame_seconds'] * 1000 + overlap, 60000)
|
||||
|
||||
def test_rejects_coverage_just_below_threshold(self):
|
||||
self.replace_frames([20] * 1500 + [10] * 5939 + [620])
|
||||
with self.assertRaisesRegex(ValueError, 'only 98.983%'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_missing_or_duplicate_frames(self):
|
||||
self.change_csv(lambda rows: rows[:100] + rows[101:])
|
||||
with self.assertRaisesRegex(ValueError, 'frame index'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_population_drift(self):
|
||||
self.change_csv(lambda rows: rows[:1] + [rows[1].replace(',12,36,', ',12,35,')] + rows[2:])
|
||||
with self.assertRaisesRegex(ValueError, 'ai workload changed'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_nonfinite_sample(self):
|
||||
self.change_csv(lambda rows: rows[:1] + [rows[1].rsplit(',', 1)[0] + ',nan'] + rows[2:])
|
||||
with self.assertRaisesRegex(ValueError, 'invalid frame/time/load'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_extra_columns_and_timing_gaps(self):
|
||||
original = (self.root / self.data['runs'][0]['sample_file']).read_text()
|
||||
self.change_csv(lambda rows: [rows[0], rows[1] + ',extra'] + rows[2:])
|
||||
with self.assertRaisesRegex(ValueError, 'columns'):
|
||||
self.execute()
|
||||
(self.root / self.data['runs'][0]['sample_file']).write_text(original)
|
||||
self.change_csv(lambda rows: [rows[0], rows[1].replace('1,20,20,', '1,30,20,')] + rows[2:])
|
||||
with self.assertRaisesRegex(ValueError, 'elapsed delta'):
|
||||
self.execute()
|
||||
|
||||
def test_rejects_incomplete_metadata_and_infinite_json(self):
|
||||
del self.data['runs'][0]['capture']['version']
|
||||
with self.assertRaisesRegex(ValueError, 'expected fields'):
|
||||
self.execute()
|
||||
self.data['runs'][0]['capture']['version'] = '1'
|
||||
self.data['runs'][0]['workload']['settings']['bad'] = float('inf')
|
||||
with self.assertRaisesRegex(ValueError, 'Non-finite'):
|
||||
self.execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"""Execute source-local admission bodies and the reservation rejection predicate.
|
||||
|
||||
Only vector arithmetic and opaque UUID equality/nullness are substituted. This is
|
||||
not a character collision, controller, callback, reconnect or native engine test.
|
||||
"""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
base = ROOT / 'addons/core/Scripts/Game/ERR'
|
||||
rules = body(base/'ERR_PlayerRoster.c', 'static bool CanEnter(')
|
||||
rules += '\n' + body(base/'ERR_SpawnFinalization.c', 'static bool Allows(')
|
||||
rules += '\n' + body(base/'ERR_SpawnFinalization.c', 'static bool PositionClose(')
|
||||
rules = rules.replace('ERR_Phase.', 'ERR_Phase::').replace('ERR_PlayerLifeRules.', '')
|
||||
rules = rules.replace('vector.', 'vector::').replace('Math.', 'Math::')
|
||||
# Extract the actual condition, rather than maintaining a second implementation.
|
||||
# Native geometry/terrain branches of Clear are deliberately not emulated.
|
||||
clear = body(base/'ERR_SpawnPlacement.c', 'bool Clear(')
|
||||
match = re.search(r'if \((id != ownReservation[^\n]+)\) return false;', clear)
|
||||
assert match, 'Reservation policy moved: update the source-local extraction'
|
||||
predicate = match[1].replace('vector.', 'vector::').replace('Math.', 'Math::')
|
||||
rules += '\nstatic bool ReservationAllows(int id, int ownReservation, vector position, vector reserved) { return !(' + predicate + '); }\n'
|
||||
run('''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
struct UUID {
|
||||
int identity;
|
||||
bool IsNull() const { return identity==0; }
|
||||
bool operator==(UUID other) const { return identity==other.identity; }
|
||||
};
|
||||
struct vector {
|
||||
float x,y,z;
|
||||
float operator[](int axis) const { return axis==0?x:(axis==1?y:z); }
|
||||
static float DistanceXZ(vector a, vector b) {return std::hypot(a.x-b.x,a.z-b.z);}
|
||||
};
|
||||
namespace Math {static float AbsFloat(float value) {return std::abs(value);}}
|
||||
''' + rules + '''
|
||||
int main() {
|
||||
// Complete independent authorization matrix, including invalid phases and null IDs.
|
||||
for (int phase=-1;phase<=7;++phase)
|
||||
for (int flags=0;flags<32;++flags)
|
||||
for (int oldId=0;oldId<=2;++oldId)
|
||||
for (int currentId=0;currentId<=2;++currentId) {
|
||||
bool connected=flags&1,usedLife=flags&2,sameController=flags&4;
|
||||
bool sameRequest=flags&8,noControlledEntity=flags&16;
|
||||
bool expected=phase>=1&&phase<=3&&connected&&!usedLife&&oldId>0&&oldId==currentId&&sameController&&sameRequest&&noControlledEntity;
|
||||
assert(Allows(phase,connected,usedLife,UUID{oldId},UUID{currentId},sameController,sameRequest,noControlledEntity)==expected);
|
||||
}
|
||||
const vector origin={0,0,0};
|
||||
// Inclusive vertical tolerance and just-inside/outside horizontal boundary.
|
||||
for (float y : {-1.001f,-1.f,-0.2f,0.f,0.2f,1.f,1.001f}) {
|
||||
assert(PositionClose(vector{0,y,0},origin)==(std::abs(y)<=1));
|
||||
assert(PositionClose(vector{.599f,y,0},origin)==(std::abs(y)<=1));
|
||||
assert(!PositionClose(vector{.601f,y,0},origin));
|
||||
}
|
||||
assert(PositionClose(vector{.3f,.2f,.3f},origin));
|
||||
assert(!PositionClose(vector{.5f,0,.5f},origin)); // radial, not independent axis allowance
|
||||
assert(!PositionClose(vector{0,-20,0},origin)); // fallen below unchanged XZ
|
||||
assert(!PositionClose(vector{0,20,0},origin)); // displaced above unchanged XZ
|
||||
// Connection IDs1 and13 must conflict identically; only the caller's own slot is exempt.
|
||||
for (int id : {1,13,99})
|
||||
for (int owner : {-1,1,13,99})
|
||||
for (float x : {-1.201f,-1.199f,0.f,1.199f,1.201f})
|
||||
for (float y : {-2.001f,-2.f,-1.999f,0.f,1.999f,2.f,2.001f}) {
|
||||
bool expected=id==owner || std::abs(x)>=1.2 || std::abs(y)>=2;
|
||||
assert(ReservationAllows(id,owner,origin,vector{x,y,0})==expected);
|
||||
}
|
||||
assert(!ReservationAllows(1,13,origin,vector{.8f,0,.8f}));
|
||||
assert(ReservationAllows(1,13,origin,vector{.9f,0,.9f}));
|
||||
assert(ReservationAllows(1,13,origin,vector{1.5f,0,0})); // authored grid spacing
|
||||
// Own reservation is excluded while finalization still checks native geometry separately.
|
||||
assert(ReservationAllows(13,13,origin,origin));
|
||||
assert(!ReservationAllows(13,1,origin,origin));
|
||||
}
|
||||
''')
|
||||
print('PASS: player admission phase/life/identity/controller matrix, preload displacement limits, pending reservation overlap and owner exclusion')
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
"""Execute the actual prison search body with a recording clearance predicate.
|
||||
This verifies bounded search order/termination, not terrain or body clearance.
|
||||
"""
|
||||
from policy_harness import ROOT,body,run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_SpawnPlacement.c'
|
||||
search=(body(source,'bool Prison(').replace('map<int, vector> reservations','map<int, vector>* reservations')
|
||||
.replace('out vector position','vector& position').replace('ERR_Actors.', 'ERR_Actors::').replace('Math.', 'Math::'))
|
||||
run('''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
struct vector {float x,y,z;};
|
||||
vector operator+(vector a,vector b){return {a.x+b.x,a.y+b.y,a.z+b.z};}
|
||||
vector Vector(float x,float y,float z){return {x,y,z};}
|
||||
template<class K,class V> struct map {};
|
||||
namespace ERR_Actors {vector Ground(vector value){return value;}}
|
||||
namespace Math {int AbsInt(int value){return std::abs(value);}}
|
||||
struct Search {
|
||||
std::vector<vector> visited;
|
||||
int accept=-1;
|
||||
bool Clear(vector candidate,map<int,vector>*){visited.push_back(candidate);return int(visited.size())-1==accept;}
|
||||
''' + search + '''
|
||||
};
|
||||
int main(){
|
||||
const vector center={6090,100,7160};
|
||||
vector result={-1,-1,-1};
|
||||
Search blocked;
|
||||
assert(!blocked.Prison(center,nullptr,result));
|
||||
assert(blocked.visited.size()==16);
|
||||
assert(result.x==-1&&result.y==-1&&result.z==-1); // no outside fallback
|
||||
std::set<std::pair<float,float>> unique;
|
||||
for(int i=0;i<16;++i){
|
||||
vector candidate=blocked.visited[i];
|
||||
float x=candidate.x-center.x,z=candidate.z-center.z;
|
||||
assert(std::abs(x)<=4.5f&&std::abs(z)<=4.5f);
|
||||
assert(unique.insert({x,z}).second);
|
||||
assert(std::abs(x)==1.5f||std::abs(x)==4.5f);
|
||||
assert(std::abs(z)==1.5f||std::abs(z)==4.5f);
|
||||
int outer=(std::abs(x)==4.5f)+(std::abs(z)==4.5f);
|
||||
assert(outer==(i<4?0:(i<12?1:2)));
|
||||
if(i<12)assert(std::hypot(x,z)<5); // inner and edge spawns meet prior 5m check
|
||||
for(int j=0;j<i;++j){
|
||||
vector previous=blocked.visited[j];
|
||||
assert(std::hypot(candidate.x-previous.x,candidate.z-previous.z)>=3);
|
||||
}
|
||||
}
|
||||
// Select any first available position; never bypass the native clearance predicate.
|
||||
for(int accepted=0;accepted<16;++accepted){
|
||||
Search search;search.accept=accepted;
|
||||
assert(search.Prison(center,nullptr,result));
|
||||
assert(search.visited.size()==unsigned(accepted+1));
|
||||
vector expected=blocked.visited[accepted];
|
||||
assert(result.x==expected.x&&result.y==expected.y&&result.z==expected.z);
|
||||
}
|
||||
|
||||
}
|
||||
''')
|
||||
print('PASS: prison search packs 16 unique 3m-spaced inside-wall centers, prioritizes inner/edge positions and never bypasses clearance')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Exercise actual radio tuning, defender and continuity policy bodies.
|
||||
Native fixture covers service events and physical character state separately.
|
||||
"""
|
||||
from policy_harness import ROOT, body, run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_RadioDefenceService.c'
|
||||
functions='\n'.join(body(source, signature) for signature in (
|
||||
'static float DefenceRadius(', 'static float TuneRadius(',
|
||||
'static bool CanTune(', 'static bool Defender(', 'static float SinceAfterSample(',
|
||||
)).replace('ERR_Phase.', 'ERR_Phase::')
|
||||
functions+='\n'+body(ROOT/'addons/core/Scripts/Game/ERR/ERR_MissionRules.c','static bool DefenceComplete(')
|
||||
run('''#include <cassert>
|
||||
#include <initializer_list>
|
||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
'''+functions+'''
|
||||
int main() {
|
||||
for (int phase=-1;phase<=7;++phase)
|
||||
for (int flags=0;flags<16;++flags)
|
||||
for (int knowledge=0;knowledge<=2;++knowledge)
|
||||
for (float distance : {-1.f,0.f,4.999f,5.f,5.001f,60.f}) {
|
||||
bool registered=flags&1,radio=flags&2,player=flags&4,conscious=flags&8;
|
||||
bool expected=phase==ERR_Phase::RADIO_SEARCH && registered && radio && knowledge>=2 && player && conscious && distance>=0 && distance<=5;
|
||||
assert(CanTune(phase,registered,radio,knowledge,player,conscious,distance)==expected);
|
||||
}
|
||||
for (int flags=0;flags<4;++flags)
|
||||
for (float distance : {-1.f,0.f,59.999f,60.f,60.001f})
|
||||
assert(Defender(flags&1,flags&2,distance)==(bool(flags&1)&&bool(flags&2)&&distance>=0&&distance<60));
|
||||
assert(DefenceRadius()==60 && TuneRadius()==5);
|
||||
float since=SinceAfterSample(-1,-1,1000,2);
|
||||
assert(since==1000);
|
||||
since=SinceAfterSample(since,1000,6000,1);
|
||||
assert(since==1000); // second person can leave without resetting remaining defender
|
||||
since=SinceAfterSample(since,6000,7000,0);
|
||||
assert(since==-1); // absent/dead/unconscious/empty-server samples reset
|
||||
since=SinceAfterSample(since,7000,8000,1);
|
||||
assert(since==8000);
|
||||
assert(!DefenceComplete(37999,since,30000,true));
|
||||
assert(DefenceComplete(38000,since,30000,true));
|
||||
assert(!DefenceComplete(38000,since,45000,true)); // live setting increased
|
||||
assert(SinceAfterSample(since,38000,30000,1)==30000); // backward clock resets interval
|
||||
assert(SinceAfterSample(since,38000,-1,1)==-1);
|
||||
assert(SinceAfterSample(since,38000,50000,-1)==-1);
|
||||
}
|
||||
''')
|
||||
print('PASS: radio authorization/discovery/phase matrix, exact distance limits, defender handover, interruption, empty samples, clock regression and duration boundary')
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Execute the production presentation-state policy; no network claim."""
|
||||
from policy_harness import ROOT, body, run
|
||||
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_RadioFeedback.c'
|
||||
function=body(source,'static ERR_RadioFeedback Next(').replace('ERR_RadioFeedback.', 'ERR_RadioFeedback::')
|
||||
run('''#include <cassert>
|
||||
enum class ERR_RadioFeedback {INACTIVE, WAITING, TRANSMITTING, INTERRUPTED};
|
||||
'''+function+'''
|
||||
int main() {
|
||||
using S=ERR_RadioFeedback;
|
||||
const S emptyExpected[]={S::WAITING,S::WAITING,S::INTERRUPTED,S::INTERRUPTED};
|
||||
for (int previous=0;previous<4;++previous) {
|
||||
S state=static_cast<S>(previous);
|
||||
// Terminal/new-round state always clears; stale occupancy cannot resurrect it.
|
||||
assert(Next(state,false,false)==S::INACTIVE);
|
||||
assert(Next(state,false,true)==S::INACTIVE);
|
||||
assert(Next(state,true,true)==S::TRANSMITTING);
|
||||
assert(Next(state,true,false)==emptyExpected[previous]);
|
||||
}
|
||||
S state=Next(S::INACTIVE,true,false);
|
||||
assert(state==S::WAITING);
|
||||
for (int sample=0;sample<100;++sample) state=Next(state,true,false);
|
||||
assert(state==S::WAITING); // initial wait is not an interruption
|
||||
state=Next(state,true,true);
|
||||
assert(state==S::TRANSMITTING);
|
||||
state=Next(state,true,false);
|
||||
assert(state==S::INTERRUPTED);
|
||||
for (int sample=0;sample<100;++sample) state=Next(state,true,false);
|
||||
assert(state==S::INTERRUPTED); // do not alternate guide messages while empty
|
||||
state=Next(state,true,true);
|
||||
assert(state==S::TRANSMITTING);
|
||||
state=Next(state,false,true);
|
||||
assert(state==S::INACTIVE);
|
||||
assert(Next(state,true,false)==S::WAITING); // fresh round has no stale interruption
|
||||
}
|
||||
''')
|
||||
print('PASS: actual radio feedback policy, initial wait, interruption persistence, resumption and terminal/new-round reset')
|
||||
@@ -0,0 +1,17 @@
|
||||
from policy_harness import ROOT, body, run
|
||||
p = ROOT / 'addons/core/Scripts/Game/ERR/ERR_RecoveryService.c'
|
||||
rules = body(p, 'static int SurvivorKind(') + '\n' + body(p, 'static bool NeedsRescuer(')
|
||||
run('''#include <cassert>
|
||||
''' + rules + '''
|
||||
int main() {
|
||||
for (int flags=0;flags<16;flags++) {
|
||||
bool alive=flags&1, connected=flags&2, same=flags&4, conscious=flags&8;
|
||||
int expected=!alive?0:(!connected||!same)?1:conscious?2:3;
|
||||
assert(SurvivorKind(alive,connected,same,conscious)==expected);
|
||||
}
|
||||
for (int conscious=0;conscious<=12;conscious++)
|
||||
for (int incapacitated=0;incapacitated<=12;incapacitated++)
|
||||
assert(NeedsRescuer(conscious,incapacitated)==(conscious==0&&incapacitated>0));
|
||||
}
|
||||
''')
|
||||
print('PASS: recovery state excludes dead bodies, preserves reservations, requires controlled rescuers and distinguishes empty/all-dead teams')
|
||||
@@ -0,0 +1,409 @@
|
||||
"""Release-gate bypass regressions. Synthetic evidence is confined to temp dirs."""
|
||||
from pathlib import Path
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import release_check as gate
|
||||
from native_report import analyze
|
||||
from source_manifest import addon_manifest
|
||||
from native_provenance import snapshot, bindings, persist, seal_run, archive_source, qualify_archive, MANIFEST, METADATA, PROVENANCE
|
||||
|
||||
|
||||
class ReleaseAuditTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix='erri-release-test-')
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.write('addons/core/addon.gproj', 'Project {}')
|
||||
self.write('addons/core/Scripts/Game/Test.c', 'class Test {}')
|
||||
self.write('addons/core/Assets/test.png', b'nonempty resource bytes')
|
||||
self.write('docs/evidence/current/manual.txt', 'Synthetic test fixture, not real acceptance.\n')
|
||||
self.log = 'docs/evidence/current/console.log.txt'
|
||||
self.report = 'docs/evidence/current/report.json'
|
||||
self.manifest = 'docs/evidence/current/source-sha256.json'
|
||||
self.write(self.log, 'ERR SMOKE PASS: synthetic\nERR SMOKE COMPLETE: synthetic\n')
|
||||
self.refresh_native()
|
||||
ordinary = {'status': 'passed', 'evidence': ['docs/evidence/current/manual.txt']}
|
||||
native = {'status': 'passed', 'evidence': [self.report, self.manifest],
|
||||
'source_manifest': self.manifest, 'native_report': self.report}
|
||||
self.ledger = {'target': 'Everon, vanilla factions, 12 players',
|
||||
'cases': {name: copy.deepcopy(ordinary) for name in gate.REQUIRED_CASES},
|
||||
'development_checks': {'clean_native_runtime': copy.deepcopy(native)}}
|
||||
self.ledger['cases']['independent_native_build'] = copy.deepcopy(native)
|
||||
|
||||
def write(self, relative, value):
|
||||
path = self.root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(value if isinstance(value, bytes) else value.encode())
|
||||
|
||||
def json(self, relative, value):
|
||||
self.write(relative, json.dumps(value))
|
||||
|
||||
def refresh_native(self):
|
||||
captured = snapshot(self.root, ['synthetic-runner-not-an-engine'])
|
||||
archive = Path(self.manifest).parent
|
||||
self.write(self.manifest, captured[0])
|
||||
self.write(str(archive / MANIFEST), captured[0])
|
||||
self.write(str(archive / METADATA), captured[1])
|
||||
self.json(str(archive / PROVENANCE), {'schema_version': 1, 'kind': 'launch_snapshot'})
|
||||
self.json(str(archive / 'runner.json.txt'), {**bindings(captured), 'started_unix': json.loads(captured[1])['captured_unix'] + 1, 'result': 'smoke-completed', 'rounds_required': 1, 'stopped_unix': json.loads(captured[1])['captured_unix'] + 2, 'source_unchanged_after_run': True, 'end_source_sha256': hashlib.sha256(captured[0]).hexdigest(), 'console_sha256': hashlib.sha256((self.root / self.log).read_bytes()).hexdigest()})
|
||||
self.json(self.report, analyze((self.root / self.log).read_text(), self.log))
|
||||
|
||||
def run_audit(self):
|
||||
self.json('docs/acceptance.json', self.ledger)
|
||||
return gate.audit(self.root)
|
||||
|
||||
def blocked(self, fragment):
|
||||
errors = self.run_audit()
|
||||
self.assertTrue(errors, 'malformed evidence incorrectly qualified')
|
||||
self.assertTrue(any(fragment in error for error in errors), errors)
|
||||
|
||||
def test_complete_synthetic_fixture_qualifies(self):
|
||||
self.assertEqual(self.run_audit(), [])
|
||||
|
||||
def test_failed_clean_runtime_blocks_even_when_all_cases_pass(self):
|
||||
self.ledger['development_checks']['clean_native_runtime']['status'] = 'failed'
|
||||
self.blocked('clean_native_runtime: status is failed')
|
||||
|
||||
def test_every_required_case_is_mandatory(self):
|
||||
for name in gate.REQUIRED_CASES:
|
||||
with self.subTest(name=name):
|
||||
saved = self.ledger['cases'].pop(name)
|
||||
self.blocked(name + ': required first-release case is missing')
|
||||
self.ledger['cases'][name] = saved
|
||||
|
||||
def test_unknown_case_rejected(self):
|
||||
self.ledger['cases']['fake_replacement'] = {'status': 'passed', 'evidence': [self.log]}
|
||||
self.blocked('unknown first-release case')
|
||||
|
||||
def test_missing_runtime_gate_rejected(self):
|
||||
self.ledger['development_checks'].clear()
|
||||
self.blocked('required clean-runtime gate is missing')
|
||||
|
||||
def test_unknown_and_malformed_statuses_rejected_everywhere(self):
|
||||
entry = self.ledger['cases']['escape_loop']
|
||||
for status in ('PASS', 'waived', '', None, True, {}, []):
|
||||
with self.subTest(status=status):
|
||||
entry['status'] = status
|
||||
self.blocked('unknown or malformed status')
|
||||
entry['status'] = 'passed'
|
||||
self.ledger['development_checks']['historical'] = {'status': 'waived', 'evidence': []}
|
||||
self.blocked('unknown or malformed status')
|
||||
|
||||
def test_malformed_sections_entries_and_evidence(self):
|
||||
original = copy.deepcopy(self.ledger)
|
||||
for section in ('cases', 'development_checks'):
|
||||
self.ledger = copy.deepcopy(original)
|
||||
self.ledger[section] = []
|
||||
self.blocked(section + ' must be an object')
|
||||
for entry in (None, [], 'passed'):
|
||||
self.ledger = copy.deepcopy(original)
|
||||
self.ledger['cases']['escape_loop'] = entry
|
||||
self.blocked('entry must be an object')
|
||||
for evidence in (None, self.log, {}, [None], [42]):
|
||||
self.ledger = copy.deepcopy(original)
|
||||
self.ledger['cases']['escape_loop']['evidence'] = evidence
|
||||
self.blocked('evidence')
|
||||
|
||||
def test_passed_requires_nonempty_evidence(self):
|
||||
self.ledger['cases']['escape_loop']['evidence'] = []
|
||||
self.blocked('requires nonempty evidence')
|
||||
|
||||
def test_empty_files_and_whitespace_cannot_qualify(self):
|
||||
for value in (b'', b' \r\n\t'):
|
||||
self.write('docs/evidence/empty.txt', value)
|
||||
self.ledger['cases']['escape_loop']['evidence'] = ['docs/evidence/empty.txt']
|
||||
self.blocked('evidence is empty')
|
||||
|
||||
def test_missing_and_directory_evidence_rejected(self):
|
||||
for path in ('docs/evidence/missing.txt', 'docs/evidence/current'):
|
||||
self.ledger['cases']['escape_loop']['evidence'] = [path]
|
||||
self.blocked('not an existing regular file')
|
||||
|
||||
def test_unsafe_paths_rejected(self):
|
||||
for path in ('/etc/passwd', '../outside', 'docs/../docs/evidence/current/manual.txt',
|
||||
'./docs/evidence/current/manual.txt', 'docs//evidence/current/manual.txt',
|
||||
'C:\\evidence.txt', '.git/config', 'docs/evidence/\x00invalid'):
|
||||
with self.subTest(path=path):
|
||||
self.ledger['cases']['escape_loop']['evidence'] = [path]
|
||||
self.assertTrue(self.run_audit())
|
||||
|
||||
def test_symlinks_even_inside_repository_rejected(self):
|
||||
for target in (self.root / self.log, Path('/etc/passwd')):
|
||||
link = self.root / 'docs/evidence/link.txt'
|
||||
link.symlink_to(target)
|
||||
self.ledger['cases']['escape_loop']['evidence'] = ['docs/evidence/link.txt']
|
||||
self.assertTrue(self.run_audit())
|
||||
link.unlink()
|
||||
|
||||
def test_symlink_loop_returns_error_without_crash(self):
|
||||
link = self.root / 'docs/evidence/loop'
|
||||
link.symlink_to(link)
|
||||
self.ledger['cases']['escape_loop']['evidence'] = ['docs/evidence/loop']
|
||||
self.assertTrue(self.run_audit())
|
||||
|
||||
def test_duplicate_paths_rejected(self):
|
||||
self.ledger['cases']['escape_loop']['evidence'] = [self.log, self.log]
|
||||
self.blocked('duplicate evidence path')
|
||||
|
||||
def test_missing_explicit_native_fields_rejected(self):
|
||||
for field in ('source_manifest', 'native_report'):
|
||||
value = self.ledger['cases']['independent_native_build'].pop(field)
|
||||
self.blocked('independent_native_build.' + field)
|
||||
self.ledger['cases']['independent_native_build'][field] = value
|
||||
|
||||
def test_explicit_native_fields_must_be_listed_as_evidence(self):
|
||||
self.ledger['cases']['independent_native_build']['evidence'] = [self.log]
|
||||
self.blocked('must also appear')
|
||||
|
||||
def test_current_native_qualification_rejects_changed_added_deleted_files(self):
|
||||
path = self.root / 'addons/core/Scripts/Game/Test.c'
|
||||
original = path.read_bytes()
|
||||
path.write_text('class Changed {}')
|
||||
self.blocked('changed=1')
|
||||
path.write_bytes(original)
|
||||
added = self.root / 'addons/core/Assets/new.custom-resource'
|
||||
added.write_bytes(b'resource')
|
||||
self.blocked('missing=1')
|
||||
added.unlink()
|
||||
path.unlink()
|
||||
self.blocked('extra=1')
|
||||
|
||||
def test_clean_runtime_also_requires_current_manifest(self):
|
||||
self.ledger['cases']['independent_native_build']['status'] = 'pending'
|
||||
self.write('addons/core/Scripts/Game/Test.c', 'class Changed {}')
|
||||
self.blocked('clean_native_runtime.source_manifest: manifest does not match')
|
||||
|
||||
def test_incomplete_and_malformed_manifest_rejected(self):
|
||||
for manifest in ({}, [], {'addons/core/addon.gproj': 'not-sha256'}):
|
||||
self.json(self.manifest, manifest)
|
||||
self.blocked('manifest must be a nonempty')
|
||||
self.json(self.manifest, {'addons/core/addon.gproj': '0' * 64})
|
||||
self.blocked('manifest does not match')
|
||||
|
||||
def test_native_report_cannot_hide_raw_errors(self):
|
||||
self.write(self.log, 'ERR SMOKE PASS: synthetic\nWORLD (E): retained error\nERR SMOKE COMPLETE: synthetic\n')
|
||||
self.blocked('does not match analysis')
|
||||
self.refresh_native()
|
||||
self.blocked('no script, engine or end-screen errors')
|
||||
|
||||
def test_each_native_failure_category_blocks(self):
|
||||
for failure in ('ERR SMOKE FAIL: assertion', 'Virtual Machine Exception',
|
||||
'SCRIPT (E): exception', 'ANIMATION (E): unknown resource', 'ENGINE (F): Crashed', 'Could not find expected endscreen',
|
||||
"entity won't take damage and wont replicate"):
|
||||
self.write(self.log, 'ERR SMOKE PASS: test\n' + failure + '\nERR SMOKE COMPLETE: test\n')
|
||||
self.refresh_native()
|
||||
self.blocked('native raw log must contain')
|
||||
|
||||
def test_truncated_and_assertion_free_logs_rejected(self):
|
||||
for text in ('ERR SMOKE PASS: test\n', 'ERR SMOKE COMPLETE: test\n', 'unrelated log\n'):
|
||||
self.write(self.log, text)
|
||||
self.refresh_native()
|
||||
self.blocked('successful completion/assertions')
|
||||
|
||||
def test_native_report_types_and_missing_fields_rejected(self):
|
||||
report = analyze((self.root / self.log).read_text(), self.log)
|
||||
for bad in ({}, [], dict(report, smoke_completions=True), dict(report, engine_errors='')):
|
||||
self.json(self.report, bad)
|
||||
self.blocked('native_report')
|
||||
|
||||
def test_native_artifacts_cannot_be_mixed_between_archives(self):
|
||||
self.write('docs/evidence/other/source-sha256.json', (self.root / self.manifest).read_bytes())
|
||||
entry = self.ledger['cases']['independent_native_build']
|
||||
entry['source_manifest'] = 'docs/evidence/other/source-sha256.json'
|
||||
entry['evidence'].append(entry['source_manifest'])
|
||||
self.blocked('same evidence archive')
|
||||
|
||||
def test_historical_development_manifests_need_not_match_current(self):
|
||||
self.json('docs/evidence/historical/source-sha256.json', {'old/path.c': '0' * 64})
|
||||
self.ledger['development_checks']['historical'] = {
|
||||
'status': 'passed', 'evidence': ['docs/evidence/historical/source-sha256.json']}
|
||||
self.ledger['development_checks']['expected_fault'] = {'status': 'failed', 'evidence': []}
|
||||
self.assertEqual(self.run_audit(), [])
|
||||
|
||||
def test_generated_state_excluded_but_new_resource_formats_included(self):
|
||||
self.write('addons/core/resourceDatabase.rdb', 'generated')
|
||||
self.write('addons/core/resources.pak', 'generated')
|
||||
self.write('addons/core/profiles/runtime.json', 'generated')
|
||||
self.assertEqual(self.run_audit(), [])
|
||||
self.write('addons/core/Scripts/Game/resourceDatabase.rdb_fake.c', 'class New {}')
|
||||
self.blocked('missing=1')
|
||||
|
||||
def test_source_symlink_cannot_hide_from_manifest(self):
|
||||
(self.root / 'addons/core/Scripts/Game/Alias.c').symlink_to(self.root / 'addons/core/Scripts/Game/Test.c')
|
||||
self.blocked('source/resource symlink is not supported')
|
||||
|
||||
def test_duplicate_json_keys_and_invalid_json_fail_closed(self):
|
||||
for raw in ('{"cases":{},"cases":{}}', '{', '[]', '{"target":NaN}'):
|
||||
self.write('docs/acceptance.json', raw)
|
||||
self.assertTrue(gate.audit(self.root))
|
||||
|
||||
def test_symlinked_ledger_rejected(self):
|
||||
self.json('docs/real-ledger.json', self.ledger)
|
||||
(self.root / 'docs/acceptance.json').symlink_to(self.root / 'docs/real-ledger.json')
|
||||
self.assertTrue(gate.audit(self.root))
|
||||
|
||||
def test_checker_is_read_only(self):
|
||||
self.json('docs/acceptance.json', self.ledger)
|
||||
before = {str(path.relative_to(self.root)): path.read_bytes()
|
||||
for path in self.root.rglob('*') if path.is_file()}
|
||||
self.assertEqual(gate.audit(self.root), [])
|
||||
after = {str(path.relative_to(self.root)): path.read_bytes()
|
||||
for path in self.root.rglob('*') if path.is_file()}
|
||||
self.assertEqual(after, before)
|
||||
|
||||
def test_archive_command_uses_shared_manifest_and_report(self):
|
||||
tools = self.root / 'tools'
|
||||
tools.mkdir()
|
||||
for name in ('archive_native.py', 'native_report.py', 'source_manifest.py', 'native_provenance.py'):
|
||||
shutil.copyfile(ROOT / 'tools' / name, tools / name)
|
||||
session = self.root / 'synthetic-session'
|
||||
session.mkdir()
|
||||
(session / 'console.log').write_text((self.root / self.log).read_text())
|
||||
archive = self.root / Path(self.manifest).parent
|
||||
for name in (MANIFEST, METADATA):
|
||||
shutil.copyfile(archive / name, session / name)
|
||||
shutil.copyfile(archive / 'runner.json.txt', session / 'runner.json')
|
||||
subprocess.run([sys.executable, str(tools / 'archive_native.py'), str(session), 'new-archive'],
|
||||
capture_output=True, text=True, check=True)
|
||||
archive = self.root / 'docs/evidence/new-archive'
|
||||
self.assertEqual(json.loads((archive / 'source-sha256.json').read_text()), addon_manifest(self.root))
|
||||
report = json.loads((archive / 'report.json').read_text())
|
||||
self.assertEqual(report, analyze((self.root / report['log']).read_text(), report['log']))
|
||||
|
||||
def test_native_report_command_compatibility(self):
|
||||
result = subprocess.run([sys.executable, str(ROOT / 'tools/native_report.py'), str(self.root / self.log)],
|
||||
capture_output=True, text=True, check=True)
|
||||
self.assertEqual(json.loads(result.stdout), analyze((self.root / self.log).read_text(), self.root / self.log))
|
||||
|
||||
def test_archive_time_provenance_cannot_qualify_native_gates(self):
|
||||
self.json('docs/evidence/current/' + PROVENANCE, {'schema_version': 1, 'kind': 'archive_time_only'})
|
||||
self.blocked('requires launch_snapshot provenance')
|
||||
|
||||
def test_missing_launch_snapshot_is_not_current_evidence(self):
|
||||
(self.root / 'docs/evidence/current' / METADATA).unlink()
|
||||
self.blocked('launch_provenance')
|
||||
|
||||
def test_modified_launch_bytes_and_runner_binding_rejected(self):
|
||||
path = self.root / 'docs/evidence/current' / MANIFEST
|
||||
path.write_bytes(path.read_bytes() + b'\n')
|
||||
self.blocked('launch manifest byte hash')
|
||||
self.refresh_native()
|
||||
runner = self.root / 'docs/evidence/current/runner.json.txt'
|
||||
data = json.loads(runner.read_text())
|
||||
data['launch_id'] = '00000000-0000-0000-0000-000000000000'
|
||||
runner.write_text(json.dumps(data))
|
||||
self.blocked('runner launch binding')
|
||||
|
||||
def test_snapshot_after_process_start_rejected(self):
|
||||
runner = self.root / 'docs/evidence/current/runner.json.txt'
|
||||
data = json.loads(runner.read_text())
|
||||
data['started_unix'] = 1
|
||||
runner.write_text(json.dumps(data))
|
||||
self.blocked('snapshot must precede process launch')
|
||||
|
||||
def test_runner_failure_and_incomplete_repeated_rounds_cannot_qualify(self):
|
||||
runner = self.root / 'docs/evidence/current/runner.json.txt'
|
||||
data = json.loads(runner.read_text())
|
||||
data['result'] = 'process-exited'
|
||||
runner.write_text(json.dumps(data))
|
||||
self.blocked('completed runner session')
|
||||
data['result'] = 'smoke-completed'
|
||||
data['rounds_required'] = 2
|
||||
runner.write_text(json.dumps(data))
|
||||
self.blocked('successful completion/assertions')
|
||||
|
||||
def test_prelaunch_snapshot_remains_old_when_persisted_after_edit(self):
|
||||
captured = snapshot(self.root, ['synthetic'])
|
||||
self.write('addons/core/Scripts/Game/Test.c', 'class ChangedAfterLaunch {}')
|
||||
session = self.root / 'synthetic-late-session'
|
||||
session.mkdir()
|
||||
persist(session, captured)
|
||||
self.assertEqual((session / MANIFEST).read_bytes(), captured[0])
|
||||
(session / 'console.log').write_text('Synthetic old run')
|
||||
self.json('synthetic-late-session/runner.json', {**bindings(captured), 'started_unix': json.loads(captured[1])['captured_unix'] + 1, 'stopped_unix': json.loads(captured[1])['captured_unix'] + 2, 'source_unchanged_after_run': True, 'end_source_sha256': hashlib.sha256(captured[0]).hexdigest(), 'console_sha256': hashlib.sha256((session / 'console.log').read_bytes()).hexdigest()})
|
||||
with self.assertRaisesRegex(ValueError, 'differ from pre-launch snapshot'):
|
||||
archive_source(session, self.root)
|
||||
|
||||
def test_legacy_archive_requires_opt_in_and_is_marked_historical(self):
|
||||
session = self.root / 'synthetic-legacy'
|
||||
session.mkdir()
|
||||
with self.assertRaisesRegex(ValueError, 'no pre-launch source snapshot'):
|
||||
archive_source(session, self.root)
|
||||
manifest, provenance, launched = archive_source(session, self.root, allow_legacy=True)
|
||||
self.assertFalse(launched)
|
||||
self.assertEqual(provenance['kind'], 'archive_time_only')
|
||||
self.assertEqual(provenance['qualification'], 'historical_only')
|
||||
self.assertEqual(json.loads(manifest), addon_manifest(self.root))
|
||||
|
||||
def test_partial_launch_evidence_cannot_fall_back_to_legacy(self):
|
||||
session = self.root / 'synthetic-partial'
|
||||
session.mkdir()
|
||||
(session / METADATA).write_text('{}')
|
||||
with self.assertRaisesRegex(ValueError, 'missing or symlinked launch evidence'):
|
||||
archive_source(session, self.root, allow_legacy=True)
|
||||
|
||||
def test_source_manifest_copy_must_match_exact_launch_bytes(self):
|
||||
path = self.root / self.manifest
|
||||
path.write_bytes(path.read_bytes() + b'\n')
|
||||
self.blocked('differs from exact launch snapshot bytes')
|
||||
|
||||
def test_recomputed_report_cannot_relabel_a_different_old_raw_log(self):
|
||||
self.write(self.log, 'ERR SMOKE PASS: different synthetic run\nERR SMOKE COMPLETE: other\n')
|
||||
self.json(self.report, analyze((self.root / self.log).read_text(), self.log))
|
||||
self.blocked('raw native log differs from stopped runner log hash')
|
||||
|
||||
def test_report_cannot_use_an_unsealed_neighbor_log(self):
|
||||
other = 'docs/evidence/current/other.log.txt'
|
||||
self.write(other, 'ERR SMOKE PASS: unrelated\nERR SMOKE COMPLETE: unrelated\n')
|
||||
self.json(self.report, analyze((self.root / other).read_text(), other))
|
||||
self.blocked('exact console.log.txt sealed by the stopped runner')
|
||||
|
||||
def test_stopped_source_change_blocks_even_if_current_files_match_launch(self):
|
||||
runner = self.root / 'docs/evidence/current/runner.json.txt'
|
||||
data = json.loads(runner.read_text())
|
||||
data['source_unchanged_after_run'] = False
|
||||
data['end_source_sha256'] = '0' * 64
|
||||
runner.write_text(json.dumps(data))
|
||||
self.blocked('changed between launch and stopped-run snapshot')
|
||||
|
||||
def test_seal_uses_stopped_sources_and_exact_log_bytes(self):
|
||||
session = self.root / 'synthetic-seal'
|
||||
session.mkdir()
|
||||
captured = snapshot(self.root, ['synthetic'])
|
||||
(session / 'console.log').write_bytes(b'exact synthetic log\r\n')
|
||||
sealed = seal_run(self.root, session, captured)
|
||||
self.assertTrue(sealed['source_unchanged_after_run'])
|
||||
self.assertEqual(sealed['console_sha256'], hashlib.sha256(b'exact synthetic log\r\n').hexdigest())
|
||||
self.write('addons/core/Scripts/Game/Test.c', 'class ChangedDuringRun {}')
|
||||
self.assertFalse(seal_run(self.root, session, captured)['source_unchanged_after_run'])
|
||||
|
||||
def test_archive_cli_requires_explicit_legacy_import(self):
|
||||
tools = self.root / 'tools'
|
||||
tools.mkdir()
|
||||
for name in ('archive_native.py', 'native_report.py', 'source_manifest.py', 'native_provenance.py'):
|
||||
shutil.copyfile(ROOT / 'tools' / name, tools / name)
|
||||
session = self.root / 'synthetic-legacy-command'
|
||||
session.mkdir()
|
||||
(session / 'console.log').write_text((self.root / self.log).read_text())
|
||||
command = [sys.executable, str(tools / 'archive_native.py'), str(session), 'legacy-archive']
|
||||
rejected = subprocess.run(command, capture_output=True, text=True)
|
||||
self.assertEqual(rejected.returncode, 2)
|
||||
self.assertFalse((self.root / 'docs/evidence/legacy-archive').exists())
|
||||
subprocess.run(command + ['--allow-legacy-archive-time-source'], capture_output=True, text=True, check=True)
|
||||
provenance = json.loads((self.root / 'docs/evidence/legacy-archive' / PROVENANCE).read_text())
|
||||
self.assertEqual(provenance['kind'], 'archive_time_only')
|
||||
with self.assertRaisesRegex(ValueError, 'requires launch_snapshot provenance'):
|
||||
qualify_archive(self.root / 'docs/evidence/legacy-archive', addon_manifest(self.root))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
import importlib.util,json,tempfile
|
||||
from policy_harness import ROOT,body,run
|
||||
spec=importlib.util.spec_from_file_location('import_settings',ROOT/'tools/import_settings.py')
|
||||
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
|
||||
assert result==dict(reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0)
|
||||
assert deferred==['ai:convoyCount']
|
||||
assert m.merge()[0]=={k:v[0] for k,v in m.SCHEMA.items()}
|
||||
for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
if key not in m.AI_KEYS and key not in m.INTEL_KEYS:continue
|
||||
for invalid in (lo-1,hi+1,True,False,None,'10',lo+0.5,float('nan'),float('inf')):
|
||||
try:m.merge(**({'ai':{key:invalid}} if key in m.AI_KEYS else {'intel':{key:invalid}}))
|
||||
except ValueError:pass
|
||||
else:raise AssertionError((key,invalid))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest=Path(tmp)/'rereintroduced.json'
|
||||
m.write_new(dest,json.dumps(result));before=dest.read_bytes()
|
||||
try:m.write_new(dest,'overwrite')
|
||||
except FileExistsError:pass
|
||||
else:raise AssertionError('Existing destination overwritten')
|
||||
assert dest.read_bytes()==before
|
||||
assert sorted(p.name for p in Path(tmp).iterdir())==['rereintroduced.json']
|
||||
# Test the actual runtime validator against the migration schema at each boundary.
|
||||
validator=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c','static bool Valid(').replace('string key','std::string key')
|
||||
checks=[]
|
||||
for key,(_,lo,hi) in m.SCHEMA.items():
|
||||
for value,expected in [(lo-1,False),(lo,True),(hi,True),(hi+1,False)]:
|
||||
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')
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Execute production discovery bounds; native LOS/identity remain runtime checks."""
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
source = ROOT / 'addons/core/Scripts/Game/ERR/ERR_SiteDiscoveryRules.c'
|
||||
methods = '\n'.join(body(source, sig) for sig in (
|
||||
'static float RadioRadiusLimit(', 'static bool ValidRadius(', 'static bool InRange('
|
||||
)).replace('ERR_LocationKind.', 'ERR_LocationKind::')
|
||||
run('''#include <cassert>
|
||||
enum class ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT };
|
||||
''' + methods.replace('int kind,', 'ERR_LocationKind kind,') + '''
|
||||
int main() {
|
||||
using K=ERR_LocationKind;
|
||||
assert(ValidRadius(K::RADIO,0) && ValidRadius(K::RADIO,8));
|
||||
assert(!ValidRadius(K::RADIO,8.01f) && !ValidRadius(K::RADIO,-1));
|
||||
assert(ValidRadius(K::DEPOT,60) && ValidRadius(K::OUTPOST,60));
|
||||
assert(!ValidRadius(K::DEPOT,60.01f));
|
||||
assert(ValidRadius(K::PRISON,0) && ValidRadius(K::EXTRACTION,0));
|
||||
assert(!ValidRadius(K::PRISON,1) && !ValidRadius(K::EXTRACTION,1));
|
||||
assert(!ValidRadius(static_cast<K>(42),0));
|
||||
assert(InRange(8,8,8) && InRange(8,0,0));
|
||||
assert(!InRange(0,0,0) && !InRange(8,8.01f,0));
|
||||
assert(!InRange(8,0,8.01f) && !InRange(8,-1,0) && !InRange(8,0,-1));
|
||||
}
|
||||
''')
|
||||
print('PASS: authored radio/optional-site bounds and physical horizontal/vertical boundaries')
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Run actual clock parsing/formatting and progression policy bodies as C++.
|
||||
Native world authority, JSON serialization and progression need native probes.
|
||||
"""
|
||||
from policy_harness import ROOT, body, run
|
||||
admin = ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
|
||||
service = ROOT/'addons/core/Scripts/Game/ERR/ERR_TimeService.c'
|
||||
settings = ROOT/'addons/core/Scripts/Game/ERR/ERR_Settings.c'
|
||||
|
||||
def translate(source):
|
||||
return (source.replace('string ', 'std::string ').replace('out int ', 'int& ')
|
||||
.replace('text.Length()', 'int(text.size())').replace('.IsEmpty()', '.empty()')
|
||||
.replace('.Substring(', '.substr(')
|
||||
.replace('"0123456789".IndexOf(', 'std::string("0123456789").find(')
|
||||
.replace(' < 0) return false;', ' == std::string::npos) return false;')
|
||||
.replace('text.ToInt()', 'std::stoi(text)')
|
||||
.replace('hours.ToString()', 'std::to_string(hours)')
|
||||
.replace('remainder.ToString()', 'std::to_string(remainder)')
|
||||
.replace('ERR_CommandArguments.Integer', 'Integer')
|
||||
.replace('ERR_Settings.Valid', 'Valid'))
|
||||
|
||||
functions = '\n'.join(translate(body(path, signature)) for path, signature in (
|
||||
(settings, 'static bool Valid('),
|
||||
(settings, 'static int AddedDefault('),
|
||||
(admin, 'static bool Integer('),
|
||||
(service, 'static bool ParseClock('),
|
||||
(service, 'static string Clock('),
|
||||
(service, 'static float DayDuration('),
|
||||
))
|
||||
run('#include <cassert>\n#include <cmath>\n#include <string>\n' + functions + r'''
|
||||
int main() {
|
||||
for (int minute=0; minute<1440; ++minute) {
|
||||
int parsed=-1;
|
||||
assert(ParseClock(Clock(minute),parsed));
|
||||
assert(parsed==minute);
|
||||
}
|
||||
assert(Clock(0)=="00:00" && Clock(1439)=="23:59" && Clock(375)=="06:15");
|
||||
assert(Clock(-1)=="unavailable" && Clock(1440)=="unavailable");
|
||||
for (std::string input : {"", "0:00", "00:0", "24:00", "12:60", "-1:00", "1a:00", "00:0b", "06:15junk", " 06:15", "06:15 ", "06.15"}) {
|
||||
int parsed=-1; assert(!ParseClock(input,parsed));
|
||||
}
|
||||
assert(AddedDefault("startTimeMinutes")==840);
|
||||
assert(AddedDefault("timeMultiplier")==1);
|
||||
assert(AddedDefault("radioDefenceSeconds")==-1);
|
||||
assert(AddedDefault("unknown")==-1);
|
||||
for (int multiplier=1; multiplier<=24; ++multiplier)
|
||||
assert(std::abs(DayDuration(multiplier)*multiplier-86400.0)<0.01);
|
||||
assert(DayDuration(0)==-1 && DayDuration(25)==-1 && DayDuration(-1)==-1);
|
||||
}
|
||||
''')
|
||||
print('PASS: every minute round-trips, malformed clocks rejected, defaults limited to added keys, progression bounds and day duration aligned')
|
||||
Reference in New Issue
Block a user