Files
EscapistsReintroduced/tests/test_terrain_selection.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

200 lines
12 KiB
Python

"""Execute the saved-terrain write/recovery rules with failing filesystem operations.
Native discovery, transitions and multiplayer are separate engine checks.
"""
from policy_harness import ROOT, body, run
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
key = body(core/'ERR_TerrainCatalog.c', 'static string Key(')
key = key.replace('"0123456789abcdef".IndexOf', 'string("0123456789abcdef").IndexOf')
pref = core/'ERR_TerrainPreference.c'
code = '\n'.join(body(pref, s) for s in ['void ERR_TerrainPreference(', 'ResourceName Scenario(', 'protected bool Read(', 'static bool Parse(', 'bool Save('])
for a,b in [('void ERR_TerrainPreference(', 'Preference('), ('protected ', ''), ('out ResourceName ', 'ResourceName& '), ('Replication.', 'Replication::'), ('ERR_TerrainCatalog.', 'Catalog::'), ('FileIO.', 'FileIO::'), ('LogLevel.', 'LogLevel::'), ('JsonLoadContext json = new JsonLoadContext();', 'JsonLoadContext json;'), ('JsonSaveContext json = new JsonSaveContext();', 'JsonSaveContext json;')]:
code = code.replace(a,b)
code = code.replace('ERR_ProfileConfig.', 'ERR_ProfileConfig::').replace('m_Path == PATH', 'm_Path == \"$profile:EscapistsTerrain.json\"')
run(r'''
#include <cassert>
#include <string>
#include <map>
#include <algorithm>
struct string:std::string {
using std::string::string; string(std::string s):std::string(s){}
bool IsEmpty()const{return empty();} int Length()const{return size();}
string Substring(int a,int b)const{return substr(a,b);}
int IndexOf(string s)const{auto p=find(s);return p==npos?-1:int(p);}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
};
using ResourceName=string;
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace LogLevel {int ERROR=1;}
void Print(string,int){}
std::map<string,string> files;
string failWrite,failCopy,corruptCopy;
struct JsonLoadContext {
string path;
bool LoadFromFile(string p){path=p;return files.count(p)&&files[p]!="CORRUPT";}
bool ReadValue(string,ResourceName& v){v=files[path];return true;}
};
struct JsonSaveContext {
string value;
void EnableTypeDiscriminator(bool){} void WriteValue(string,string v){value=v;}
bool SaveToFile(string p){if(p==failWrite)return false;files[p]=value;return true;}
};
namespace FileIO {
bool FileExists(string p){return files.count(p);}
bool CopyFile(string a,string b){if(b==failCopy)return false;if(!files.count(a))return false;files[b]=b==corruptCopy?"CORRUPT":files[a];return true;}
void DeleteFile(string p){files.erase(p);}
}
// Isolated custom paths retain standalone transactions; unified default paths
// are covered by the central store regression and native fixture.
namespace ERR_ProfileConfig {
bool Load(string path,JsonLoadContext& json){return json.LoadFromFile(path);}
bool Exists(string path){return FileIO::FileExists(path);}
bool Save(string,JsonSaveContext&){assert(false);return false;}
}
struct Catalog {''' + key + r'''};
const string PATH="terrain.json";
struct Preference {
string m_Path;ResourceName m_Scenario;bool m_ReadOnly=false;
''' + code + r'''
};
int main(){
const string everon="{C1E20667DCE324ED}Missions/ERR_Everon.conf";
const string nogova="{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf";
assert(Catalog::Key(everon)=="c1e20667dce324ed");
for(string bad:{"", "../outside.conf", "{ZZZZZZZZZZZZZZZZ}Missions/x.conf", "{C1E20667DCE324E}x"})assert(Catalog::Key(bad).empty());
Preference fresh;assert(fresh.Scenario().empty()&&files.empty());
assert(fresh.Save(everon));Preference restart;assert(restart.Scenario()==everon);
assert(!restart.Save("../outside.conf")&&restart.Scenario()==everon);
assert(restart.Save(nogova));assert(files[PATH+".bak"]==everon);
Preference again;assert(again.Scenario()==nogova);
// Temp-write, backup-copy and final-copy failure must not change the selected map.
failWrite=PATH+".tmp";assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failWrite="";
failCopy=PATH+".bak";assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failCopy="";
failCopy=PATH;assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failCopy="";
// Readback detects a damaged final write; a retained backup permits recovery.
corruptCopy=PATH;assert(!again.Save(everon)&&again.Scenario()==nogova);corruptCopy="";
Preference recovered;assert(recovered.Scenario()==nogova);
assert(recovered.Save(""));Preference cleared;assert(cleared.Scenario().empty());
files[PATH]="CORRUPT";files[PATH+".bak"]="CORRUPT";Preference damaged;
assert(damaged.Scenario().empty()&&!damaged.Save(everon)&&files[PATH]=="CORRUPT");
files.clear();failCopy=PATH;Preference firstFailure;assert(!firstFailure.Save(everon)&&!files.count(PATH));failCopy="";
Replication::server=false;Preference client;assert(!client.Save(everon)&&!files.count(PATH));
}
''')
print('PASS: saved terrain GUIDs, defaults, restart persistence, clearing, backup recovery, corrupt-file preservation, write failures, and client write denial')
prepare = body(core/'ERR_TerrainSelection.c', 'bool ERR_PrepareTerrainSelection(')
for a,b in [('World world', 'World* world'), ('SCR_MissionHeader header', 'SCR_MissionHeader* header'), ('SCR_MissionHeader.Cast', 'SCR_MissionHeader::Cast'), ('header.', 'header->'), ('Replication.', 'Replication::'), ('SCR_Global.', 'SCR_Global::'), ('ERR_TerrainCatalog.', 'Catalog::'), ('LogLevel.', 'LogLevel::'), ('ERR_Terrains().', 'ERR_Terrains()->')]:
prepare=prepare.replace(a,b)
run(r'''
#include <cassert>
#include <string>
struct string:std::string {using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
using ResourceName=string;
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace SCR_Global{bool editor=false;bool IsEditMode(){return editor;}}
namespace LogLevel{int ERROR=1;}
void Print(string,int){}
struct World{};
struct SCR_MissionHeader {
ResourceName resource="nogova";
ResourceName GetHeaderResourceName(){return resource;}
static SCR_MissionHeader* Cast(SCR_MissionHeader* p){return p;}
};
struct Catalog{static string Key(ResourceName r){return r;}};
struct Selection{ResourceName destination;ResourceName Destination(){return destination;}};
struct Game{
World* m_ERRTerrainSelectionWorld=nullptr;
ResourceName m_ERRTerrainDestination,m_ERRLastTerrainRedirect;
float m_ERRTerrainSelectionWait=0;
Selection selection;bool mode=true;SCR_MissionHeader* header=nullptr;
Selection* ERR_Terrains(){return &selection;}
bool GetGameMode(){return mode;}SCR_MissionHeader* GetMissionHeader(){return header;}
''' + prepare + r'''
};
int main(){
World world;SCR_MissionHeader h;Game g;g.header=&h;
// Empty or unavailable saved destination preserves the server-selected scenario.
assert(!g.ERR_PrepareTerrainSelection(&world));
g.selection.destination="everon";
Replication::server=false;assert(!g.ERR_PrepareTerrainSelection(&world));Replication::server=true;
SCR_Global::editor=true;assert(!g.ERR_PrepareTerrainSelection(&world));SCR_Global::editor=false;
g.mode=false;assert(!g.ERR_PrepareTerrainSelection(&world));g.mode=true;
g.header=nullptr;assert(!g.ERR_PrepareTerrainSelection(&world));g.header=&h;
assert(g.ERR_PrepareTerrainSelection(&world));
assert(g.m_ERRTerrainDestination=="everon"&&g.m_ERRTerrainSelectionWorld==&world&&g.m_ERRTerrainSelectionWait==0);
// Failing to reach a requested destination cannot cause an infinite reload.
assert(!g.ERR_PrepareTerrainSelection(&world));
h.resource="everon";assert(!g.ERR_PrepareTerrainSelection(&world)&&g.m_ERRLastTerrainRedirect.empty());
// Saving in a running world does not mutate the captured startup destination.
g.selection.destination="nogova";assert(g.m_ERRTerrainDestination=="everon");
assert(g.ERR_PrepareTerrainSelection(&world)&&g.m_ERRTerrainDestination=="nogova");
}
''')
print('PASS: startup terrain capture protects client/editor/menu worlds, default behavior, same-map loads and reload-loop recovery')
update = body(core/'ERR_TerrainSelection.c', 'override void OnUpdate(')
for a,b in [('override ', ''), ('BaseWorld world', 'World* world'), ('super.', 'Base::'), ('Replication.', 'Replication::'), ('GameStateTransitions.', 'Transitions::'), ('GameProject.', 'Projects::'), ('PlayerManagerKickReason.', 'KickReason::'), ('LogLevel.', 'LogLevel::'), ('PlayerManager players', 'PlayerManager* players'), ('players.', 'players->'), ('array<string>', 'Array<string>'), ('array<int>', 'Array<int>'), ('foreach (', 'for (')]:
update=update.replace(a,b)
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct string:std::string {using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
using ResourceName=string;
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace LogLevel{int ERROR=1;}
namespace KickReason{int KICK=1;}
void Print(string,int){}template<class... T>void PrintFormat(T...){}
struct World{};
namespace Projects{
Array<string> loaded={"vanilla","escape","nogova","equipment"};
void GetLoadedAddons(Array<string>& out){out=loaded;}
bool IsVanillaAddon(string id){return id=="vanilla";}
}
namespace Transitions{
bool busy=false;int requests=0;string target,addons;
bool IsTransitionRequestedOrInProgress(){return busy;}
bool RequestScenarioChangeTransition(string d,string,string mods){requests++;target=d;addons=mods;return true;}
}
struct PlayerManager{
Array<int> connected;int kicks=0;
int GetPlayerCount(){return connected.size();}
void GetPlayers(Array<int>& out){out=connected;}
void KickPlayer(int,int reason,int timeout){assert(reason==KickReason::KICK&&timeout==0);kicks++;connected.clear();}
};
struct Base{void OnUpdate(World*,float){}};
struct Game:Base{
string m_ERRTerrainDestination="everon";
World* m_ERRTerrainSelectionWorld=nullptr;World* current=nullptr;
float m_ERRTerrainSelectionWait=0,m_ERRTerrainDisconnectWait=0,m_ERRTerrainEmptyWait=0;
PlayerManager players;
World* GetWorld(){return current;}PlayerManager* GetPlayerManager(){return &players;}
''' + update + r'''
};
int main(){
World w,other;
Game g;g.current=&w;g.m_ERRTerrainSelectionWorld=&w;g.players.connected={1,2};
for(int i=0;i<12;i++)g.OnUpdate(&w,.25f);
assert(!Transitions::requests&&!g.players.kicks);
// A terrain change must never carry connected clients into the new world.
for(int i=0;i<4;i++)g.OnUpdate(&w,.25f);
assert(g.players.kicks==2&&!Transitions::requests);
for(int i=0;i<4;i++)g.OnUpdate(&w,.25f);
assert(Transitions::requests==1&&Transitions::target=="everon");
assert(Transitions::addons=="escape,nogova,equipment");
for(int i=0;i<100;i++)g.OnUpdate(&w,.25f);
assert(Transitions::requests==1); // Consumed once, no live polling of menu saves.
Game busy;busy.current=&w;busy.m_ERRTerrainSelectionWorld=&w;Transitions::busy=true;
for(int i=0;i<64;i++)busy.OnUpdate(&w,.25f);
assert(busy.m_ERRTerrainDestination.empty()&&Transitions::requests==1);Transitions::busy=false;
Game stale;stale.current=&other;stale.m_ERRTerrainSelectionWorld=&w;stale.OnUpdate(&w,4);
assert(stale.m_ERRTerrainDestination.empty()&&Transitions::requests==1);
Game client;client.current=&w;client.m_ERRTerrainSelectionWorld=&w;Replication::server=false;client.OnUpdate(&w,4);
assert(client.m_ERRTerrainDestination.empty()&&Transitions::requests==1);Replication::server=true;
Projects::loaded={"vanilla"};Game empty;empty.current=&w;empty.m_ERRTerrainSelectionWorld=&w;empty.OnUpdate(&w,4);
assert(empty.m_ERRTerrainDestination.empty()&&Transitions::requests==1);
}
''')
print('PASS: actual transition waits for native startup, disconnects without bans, requires an empty session, retains every loaded mod, bounds waiting and consumes requests once')