128 lines
11 KiB
Python
128 lines
11 KiB
Python
"""Production service/state transitions with native HTTP and profile IO substituted."""
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpeechService.c'
|
|
w=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_WelcomeVoice.c'
|
|
def adapt(s):
|
|
s=s.replace('protected ', '').replace('ref ', '').replace('null','nullptr')
|
|
for cls in ['ERR_AdminSession','ERR_SpeechRequest','ERR_SpeechReceiver','ERR_GameMode','JsonSaveContext','JsonLoadContext']:
|
|
for name in ['editor','request','expired','receiver','mode','json']:
|
|
s=s.replace(cls+' '+name,cls+'* '+name)
|
|
s=s.replace('array<string> values','Array* values')
|
|
for name in ['json','m_Mode','m_Request','request','expired','editor','receiver','values']:
|
|
s=s.replace(name+'.',name+'->')
|
|
for name in ['Editor','Receiver']:
|
|
s=s.replace(name+'.',name+'->')
|
|
for cls in ['ERR_WelcomeVoiceRules','ERR_SpeechRules','ERR_ProfileConfig','Replication','ERR_GameMode']:
|
|
s=s.replace(cls+'.',cls+'::')
|
|
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
|
|
return s
|
|
rules='struct ERR_SpeechRules {\n'+'\n'.join(adapt(body(p,s)) for s in ['static bool Endpoint(', 'static string Normalize(', 'static bool Token(', 'static bool Text('])+'\n};'
|
|
methods=['bool Bundled()', 'string StaticURL()', 'bool Ready()', 'bool Busy()', 'bool Current()', 'bool IdleFor(',
|
|
'void ERR_SpeechService(', 'void Snapshot(', 'protected bool Save(',
|
|
'void SetRecordings(', 'void Disable(', 'void TestAndEnable(', 'protected void Begin(',
|
|
'bool Request(', 'void Tick()', 'void Finished(', 'void Stop()']
|
|
code='\n'.join(adapt(body(p,s)) for s in methods).replace('void ERR_SpeechService(', 'void Init(')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
struct string:std::string { using std::string::string; string(std::string s):std::string(s){}
|
|
int Length(){return size();} bool IsEmpty(){return empty();} int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
|
|
bool EndsWith(string s){return size()>=s.size()&&compare(size()-s.size(),s.size(),s)==0;}
|
|
void TrimInPlace(){auto a=find_first_not_of(" \r\n\t");if(a==npos){clear();return;}auto b=find_last_not_of(" \r\n\t");*this=substr(a,b-a+1);}
|
|
};
|
|
struct Array:std::vector<string>{void Insert(string s){push_back(s);}};
|
|
bool http=true,server=true,loadOK=true,saveOK=true;int saves=0,starts=0;
|
|
std::map<string,string> profile,saved;
|
|
struct ERR_WelcomeVoiceRules { static bool HTTPReady(){return http;}
|
|
'''+body(w,'static bool URL(')+r'''
|
|
};
|
|
'''+rules+r'''
|
|
struct JsonLoadContext { bool ReadValue(string k,string& v){if(!profile.count(k))return false;v=profile[k];return true;} bool ReadValue(string k,bool& v){if(!profile.count(k))return false;v=profile[k]=="true";return true;} };
|
|
struct JsonSaveContext {std::map<string,string> data;void EnableTypeDiscriminator(bool){} void WriteValue(string k,string v){data[k]=v;}void WriteValue(string k,bool v){data[k]=v?"true":"false";} };
|
|
namespace ERR_ProfileConfig {string VOICE="voice";bool Load(string,JsonLoadContext*){return loadOK;}bool Save(string,JsonSaveContext* json){if(!saveOK)return false;saves++;saved=json->data;return true;} }
|
|
namespace Replication {bool IsServer(){return server;}}
|
|
struct ERR_GameMode {static ERR_GameMode* live;static ERR_GameMode* Get(){return live;}bool active=true;int restored=0,published=0;bool pending=false;string lastURL,lastText;
|
|
bool recordings=true;bool ERR_RecordingsEnabled(){return recordings;}void ERR_SetRecordingsEnabled(bool v){recordings=v;}
|
|
bool bundled=false;bool ERR_UsesBundledWelcome(){return bundled;}
|
|
bool Active(){return active;}string ERR_CurrentWelcomeText(){return "Welcome to Nogova.";}
|
|
void ERR_RestoreStaticWelcome(){restored++;pending=false;}
|
|
void ERR_PublishWelcome(string url,string text){published++;lastURL=url;lastText=text;pending=false;}
|
|
void ERR_SetWelcomePending(bool value){pending=value;}
|
|
};ERR_GameMode* ERR_GameMode::live=nullptr;
|
|
struct Game {float now=100000;Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
|
|
Game* GetGame(){return &game;}
|
|
struct ERR_AdminSession {bool valid=true;Array replies;int previews=0;bool Valid(){return valid;}void SpeechReply(string s){replies.Insert(s);}void SpeechPreview(string,string){previews++;}};
|
|
struct ERR_SpeechReceiver {int completed=0,failed=0;void Complete(string,string){completed++;}void Failed(){failed++;}};
|
|
struct ERR_SpeechService;
|
|
struct ERR_SpeechRequest {string Text,Endpoint,Token;bool Enable=false,Boot=false,cancelled=false;ERR_AdminSession* Editor=nullptr;ERR_SpeechReceiver* Receiver=nullptr;
|
|
void Start(ERR_SpeechService*){starts++;}void Cancel(){cancelled=true;Token="";Editor=nullptr;Receiver=nullptr;}
|
|
};
|
|
struct ERR_SpeechService {
|
|
ERR_GameMode* m_Mode=nullptr;string m_Endpoint,m_Token,m_StaticURL,m_Status="Not configured";
|
|
bool m_Bundled=true;bool m_Enabled=false,m_Ready=false,m_BootAttempted=false,m_Stopped=false;
|
|
ERR_SpeechRequest* m_Request=nullptr;float m_Deadline=0,m_LastTest=-60000,m_LastRequest=-5000;
|
|
'''+code+r'''
|
|
};
|
|
int main(){
|
|
assert(ERR_SpeechRules::Endpoint("https://speech.example/"));
|
|
for(string url:{"", "https://user:key@host/", "https://host/?key=secret", "https://host/#fragment", "https://host/|oops", "http://bad\tpath"}) assert(!ERR_SpeechRules::Endpoint(url));
|
|
assert(ERR_SpeechRules::Normalize(" http://lan:18947 ")=="http://lan:18947/");
|
|
assert(ERR_SpeechRules::Token(string(32,'A')));assert(!ERR_SpeechRules::Token(string(31,'A')));assert(!ERR_SpeechRules::Token(string(257,'A')));
|
|
assert(!ERR_SpeechRules::Text("\n"));assert(!ERR_SpeechRules::Text(string(801,'A')));
|
|
ERR_GameMode mode;ERR_GameMode::live=&mode;ERR_AdminSession admin,other;
|
|
profile={{"welcomeUrl","http://old/welcome.b64"}};
|
|
ERR_SpeechService service;service.Init(&mode);assert(service.StaticURL()=="http://old/welcome.b64");service.Tick();assert(starts==0&&saves==0&&!service.Ready());
|
|
mode.bundled=true;int beforeStarts=starts;service.Tick();assert(service.Bundled()&&starts==beforeStarts&&!service.Busy());mode.bundled=false;
|
|
ERR_SpeechReceiver receiver;assert(!service.Request("Hello",&receiver));
|
|
string secret="AbCdefghijklmnopqrstuvwxyz0123456789";
|
|
admin.valid=false;service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==0);admin.valid=true;
|
|
service.TestAndEnable(&admin,"", "");assert(starts==0&&saves==0);
|
|
http=false;service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==0);http=true;
|
|
service.TestAndEnable(&admin,"https://speech",secret);auto first=service.m_Request;assert(first&&starts==1&&first->Token==secret&&!service.Ready()&&saves==0);
|
|
service.TestAndEnable(&other,"https://other/",secret);assert(starts==1);
|
|
service.Finished(first,"");assert(saves==0&&!service.Ready()&&service.StaticURL()=="http://old/welcome.b64");
|
|
service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==1);game.now+=60001;
|
|
service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;saveOK=false;service.Finished(first,"https://speech/clip");assert(!service.Ready()&&saves==0);saveOK=true;
|
|
game.now+=60001;service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;
|
|
admin.valid=false;service.Finished(first,"https://speech/clip");assert(saves==0&&!service.Ready());admin.valid=true;
|
|
game.now+=60001;service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;
|
|
service.Finished(first,"https://speech/clip");assert(saves==1&&service.Ready()&&admin.previews==1&&mode.published==1);
|
|
assert(!service.Bundled()&&saved["bundledSpeech"]=="false");
|
|
assert(saved["briefingToken"]==secret&&saved["welcomeUrl"]=="http://old/welcome.b64"&&saved["ttsEnabled"]=="true");
|
|
Array snapshot;service.Snapshot(&snapshot);for(string line:snapshot)assert(line.find(secret)==string::npos);
|
|
game.now+=60001;service.TestAndEnable(&admin,"https://other/", "");assert(!service.Busy()&&saves==1);
|
|
service.TestAndEnable(&admin,"https://speech/", "");assert(service.m_Request->Token==secret);first=service.m_Request;
|
|
service.Finished(first,"");assert(service.Ready()&&saves==1); // failed candidate preserves working config
|
|
game.now+=60001;service.TestAndEnable(&admin,"https://speech/", "");first=service.m_Request;
|
|
service.Disable(&other);assert(!service.Ready()&&!service.Busy()&&first->cancelled&&saved["ttsEnabled"]=="false");
|
|
service.Finished(first,"https://speech/late");assert(!service.Ready()&&admin.previews==1); // late success cannot re-enable
|
|
assert(service.Bundled()&&saved["bundledSpeech"]=="true");
|
|
profile=saved;ERR_SpeechService disabled;disabled.Init(&mode);disabled.Tick();assert(!disabled.Ready()&&!disabled.Busy());
|
|
profile["ttsEnabled"]="true";profile["bundledSpeech"]="false";ERR_SpeechService boot;boot.Init(&mode);int previous=starts;http=false;boot.Tick();assert(starts==previous);http=true;
|
|
boot.Tick();assert(boot.Busy()&&mode.pending);first=boot.m_Request;assert(first->Boot);
|
|
boot.Finished(first,"https://speech/cached");assert(boot.Ready()&&!mode.pending&&saves==2);
|
|
assert(!boot.IdleFor(65000));game.now+=65000;assert(boot.IdleFor(65000));
|
|
game.now+=6000;assert(boot.Request("Enemy patrol nearby.",&receiver));first=boot.m_Request;assert(!boot.Request("more",&receiver));
|
|
boot.Finished(first,"https://speech/line");assert(receiver.completed==1&&boot.Ready());
|
|
game.now+=6000;assert(boot.Request("second",&receiver));first=boot.m_Request;
|
|
game.now+=100001;boot.Tick();assert(boot.Ready()&&!boot.Busy()&&receiver.failed==1&&first->cancelled);
|
|
boot.Finished(first,"https://speech/late");assert(receiver.completed==1&&boot.Ready());
|
|
ERR_SpeechService badBoot;badBoot.Init(&mode);badBoot.Tick();assert(badBoot.Busy());
|
|
badBoot.Finished(badBoot.m_Request,"");assert(!badBoot.Ready()&&!badBoot.Busy());
|
|
// A recordings toggle preserves live preferences and credentials, and commits only on successful save.
|
|
assert(mode.recordings);int beforeSaves=saves;
|
|
admin.valid=false;boot.SetRecordings(&admin,false);assert(mode.recordings&&saves==beforeSaves);admin.valid=true;
|
|
saveOK=false;boot.SetRecordings(&admin,false);assert(mode.recordings&&saves==beforeSaves);saveOK=true;
|
|
boot.SetRecordings(&admin,false);assert(!mode.recordings&&saved["recordingsEnabled"]=="false"&&saved["bundledSpeech"]=="false"&&saved["ttsEnabled"]=="true"&&saved["briefingToken"]==secret);
|
|
Array muted;boot.Snapshot(&muted);bool found=false;for(string line:muted)if(line=="recordingsEnabled=0")found=true;assert(found);
|
|
profile=saved;ERR_SpeechService reload;reload.Init(&mode);assert(!mode.recordings);
|
|
reload.Disable(&admin);assert(!mode.recordings&&saved["recordingsEnabled"]=="false");
|
|
reload.SetRecordings(&admin,true);assert(mode.recordings&&saved["recordingsEnabled"]=="true"&&saved["bundledSpeech"]=="true");
|
|
profile.erase("recordingsEnabled");mode.recordings=false;ERR_SpeechService legacy;legacy.Init(&mode);assert(mode.recordings);
|
|
boot.Stop();assert(!boot.Current()&&!boot.IdleFor(0));
|
|
}
|
|
''')
|
|
print('PASS: production TTS settings/test/enable/failure/disable/timeout flows preserve fallback, secrets, authorization, saved state and late-callback cancellation')
|