Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
"""Execute production incremental audio preparation with clock/file/audio doubles.
|
||||
|
||||
These regressions prove bounded script work and lifecycle behavior, not native
|
||||
audio latency; the separate Workbench fixture measures native execution.
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
SOURCE = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_WelcomeVoice.c'
|
||||
|
||||
|
||||
def cpp(text):
|
||||
for old, new in [
|
||||
('protected ', ''), ('override ', ''), ('ref ', ''),
|
||||
('null', 'nullptr'), ('Math.Min', 'std::min'),
|
||||
('Math.Max', 'std::max'), ('System.GetTickCount', 'System::GetTickCount'),
|
||||
('array<int> bytes', 'Array<int>& bytes'),
|
||||
]:
|
||||
text = text.replace(old, new)
|
||||
return text
|
||||
|
||||
|
||||
PREAMBLE = r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
namespace System {
|
||||
int now=0, increment=0;
|
||||
int GetTickCount(){int result=now;now+=increment;return result;}
|
||||
}
|
||||
struct string:std::string {
|
||||
using std::string::string;
|
||||
string(std::string value):std::string(value){}
|
||||
bool IsEmpty() const{return empty();}
|
||||
int Length() const{return size();}
|
||||
int ToAscii(int offset=0) const{return static_cast<unsigned char>(at(offset));}
|
||||
string Substring(int offset,int count) const{return substr(offset,count);}
|
||||
int IndexOf(string value) const{auto p=find(value);return p==npos?-1:int(p);}
|
||||
};
|
||||
template<class T>struct Array:std::vector<T> {
|
||||
using std::vector<T>::vector;
|
||||
int Count() const{return this->size();}
|
||||
bool IsEmpty() const{return this->empty();}
|
||||
void Clear(){this->clear();}
|
||||
void Insert(T value){this->push_back(value);}
|
||||
bool Contains(T value) const{return std::find(this->begin(),this->end(),value)!=this->end();}
|
||||
int Find(T value) const{auto p=std::find(this->begin(),this->end(),value);return p==this->end()?-1:int(p-this->begin());}
|
||||
// Native Enforce Remove replaces the removed slot with the last element.
|
||||
// Its explicitly ordered variants preserve FIFO order.
|
||||
void Remove(int index){(*this)[index]=this->back();this->pop_back();}
|
||||
void RemoveItem(T value){int index=Find(value);if(index>=0)Remove(index);}
|
||||
void RemoveOrdered(int index){this->erase(this->begin()+index);}
|
||||
void RemoveItemOrdered(T value){int index=Find(value);if(index>=0)RemoveOrdered(index);}
|
||||
};
|
||||
#define Print(...)
|
||||
#define PrintFormat(...)
|
||||
'''
|
||||
|
||||
|
||||
decoder = cpp(body(SOURCE, 'class ERR_WelcomeVoiceDecoder')).replace(
|
||||
'class ERR_WelcomeVoiceDecoder', 'struct ERR_WelcomeVoiceDecoder') + ';'
|
||||
|
||||
cases = []
|
||||
for size in [44, 45, 46, 47, 255, 256, 257, 4094, 4095, 4096, 8192]:
|
||||
expected = random.Random(size).randbytes(size)
|
||||
encoded = base64.b64encode(expected).decode()
|
||||
cases.append('Check(' + json.dumps(encoded) + ', Array<int>{' +
|
||||
','.join(map(str, expected)) + '});')
|
||||
|
||||
run(PREAMBLE + decoder + r'''
|
||||
Array<int> DecodeAll(string text,int limit=5460) {
|
||||
ERR_WelcomeVoiceDecoder decoder;assert(decoder.Begin(text));
|
||||
Array<int> result,chunk;int slices=0,status=0;
|
||||
while(status==0){
|
||||
status=decoder.Step(chunk,limit,2);
|
||||
assert(status>=0&&chunk.Count()<=4095);
|
||||
for(int value:chunk)result.Insert(value);
|
||||
assert(++slices<10000);
|
||||
}
|
||||
assert(status==1);return result;
|
||||
}
|
||||
void Check(string encoded,Array<int> expected) {
|
||||
for(int limit:{4,1024,5460})assert(DecodeAll(encoded,limit)==expected);
|
||||
}
|
||||
int main(){
|
||||
''' + '\n'.join(cases) + r'''
|
||||
// Maximum PCM transport payload and both legal padding forms survive slicing.
|
||||
for(int size:{2097150,2097151,2097152}){
|
||||
string encoded((size+2)/3*4,'A');
|
||||
if(size%3==1){encoded[encoded.size()-2]='=';encoded.back()='=';}
|
||||
if(size%3==2)encoded.back()='=';
|
||||
auto bytes=DecodeAll(encoded);assert(bytes.Count()==size);
|
||||
for(int value:bytes)assert(value==0);
|
||||
}
|
||||
// This encoded length is allowed, but its unpadded result exceeds 2 MiB.
|
||||
ERR_WelcomeVoiceDecoder over;assert(over.Begin(string(2796204,'A')));
|
||||
Array<int> chunk;int status=0;
|
||||
while(status==0)status=over.Step(chunk);
|
||||
assert(status==-1);
|
||||
// Bad alphabet and padding are rejected even in a later slice/block.
|
||||
for(int offset:{0,1023,1024,5459,5460,7999}){
|
||||
ERR_WelcomeVoiceDecoder bad;string encoded(8000,'A');encoded[offset]='?';
|
||||
assert(bad.Begin(encoded));status=0;
|
||||
while(status==0)status=bad.Step(chunk);
|
||||
assert(status==-1);
|
||||
}
|
||||
for(string encoded:{string(59,'A'),string(61,'A'),string(2796208,'A'),string()}){
|
||||
ERR_WelcomeVoiceDecoder bad;assert(!bad.Begin(encoded));
|
||||
}
|
||||
for(string tail:{"AA=A","AB==","AAB=","=AAA","A=AA"}){
|
||||
ERR_WelcomeVoiceDecoder bad;assert(bad.Begin(string(7996,'A')+tail));status=0;
|
||||
while(status==0)status=bad.Step(chunk);
|
||||
assert(status==-1);
|
||||
}
|
||||
{ERR_WelcomeVoiceDecoder bad;string encoded(8000,'A');encoded[1023]='=';
|
||||
assert(bad.Begin(encoded));status=0;while(status==0)status=bad.Step(chunk);assert(status==-1);}
|
||||
// The byte ceiling is independent of the clock. The elapsed-time guard also
|
||||
// yields when individual engine operations cost time.
|
||||
ERR_WelcomeVoiceDecoder slow;assert(slow.Begin(string(8000,'A')));
|
||||
System::increment=1;status=slow.Step(chunk);System::increment=0;
|
||||
assert(status==0&&chunk.Count()>0&&chunk.Count()<4095);
|
||||
slow.Clear();assert(slow.Begin(string(60,'A')));assert(slow.Step(chunk)==1&&chunk.Count()==45);
|
||||
}
|
||||
''')
|
||||
|
||||
print('PASS: production incremental decoder preserves binary data, bounds each slice, yields on elapsed time and rejects malformed or oversized payloads')
|
||||
|
||||
|
||||
def client_cpp(text):
|
||||
text = cpp(text)
|
||||
for old, new in [
|
||||
('ERR_WelcomeVoiceRules.', 'Rules::'), ('ERR_GameMode.Get()', 'ERR_GameMode::Get()'),
|
||||
('FileIO.', 'FileIO::'), ('AudioSystem.', 'AudioSystem::'),
|
||||
('AudioHandle.Invalid', 'AudioHandle::Invalid'),
|
||||
('FileMode.', 'FileMode::'), ('RestContext context', 'RestContext* context'),
|
||||
('RestCallback callback', 'RestCallback* callback'),
|
||||
('FileHandle file', 'File* file'), ('Array<int>& bytes = {}', 'Array<int> bytes'),
|
||||
('GetGame().', 'GetGame()->'), ('GetRestApi().', 'GetRestApi()->'),
|
||||
('m_Callback.SetOnSuccess(Downloaded)', 'm_Callback.SetOnSuccess(0)'),
|
||||
('m_Callback.SetOnError(Failed)', 'm_Callback.SetOnError(0)'),
|
||||
]:
|
||||
text = text.replace(old, new)
|
||||
for name in ['m_Connection', 'm_Callback', 'm_Decoder', 'm_Output',
|
||||
'm_Mode', 'callback', 'context', 'file']:
|
||||
text = text.replace(name + '.', name + '->')
|
||||
return text
|
||||
|
||||
|
||||
client_methods = '\n'.join(client_cpp(body(SOURCE, signature)) for signature in [
|
||||
'protected bool Current()', 'bool PreparingDownload()', 'void Tick(',
|
||||
'protected void Downloaded(', 'protected void CancelDownload()',
|
||||
'bool PrepareDownload()', 'void Stop()',
|
||||
]).replace('void Tick(', 'virtual void Tick(').replace('void Stop()', 'virtual void Stop()')
|
||||
soldier_source = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeech.c'
|
||||
soldier_methods = '\n'.join(client_cpp(body(soldier_source, signature)) for signature in [
|
||||
'override void Tick(', 'override void Stop()',
|
||||
]).replace('super.Tick', 'Client::Tick').replace('super.Stop', 'Client::Stop')
|
||||
queue_methods = []
|
||||
for signature in ['void ERR_QueueVoiceDownload(', 'void ERR_CancelVoiceDownload(',
|
||||
'protected void ERR_PrepareVoiceDownloads()', 'protected void ERR_StopVoiceDownloads()']:
|
||||
method = cpp(body(SOURCE, signature)).replace('ERR_WelcomeVoiceClient client', 'Client* client').replace('client.', 'client->')
|
||||
method = re.sub(r'^(void) (ERR_\w+)', r'\1 Game::\2', method)
|
||||
queue_methods.append(method)
|
||||
connection = 'struct Connection { bool m_Attempted=false;' + '\n'.join(
|
||||
body(SOURCE, signature) for signature in ['void MainMenu()', 'bool Claim()', 'bool Attempted()']) + '};'
|
||||
|
||||
# The native WAV parser remains outside this policy harness. A RIFF/WAVE byte
|
||||
# fixture exercises the production header/length checks and exact file output.
|
||||
wav = b'RIFF' + (9992).to_bytes(4, 'little') + b'WAVE' + random.Random(71).randbytes(9988)
|
||||
wav64 = json.dumps(base64.b64encode(wav).decode())
|
||||
expected = 'Array<int>{' + ','.join(map(str, wav)) + '}'
|
||||
run(PREAMBLE + decoder + connection + r'''
|
||||
namespace AudioHandle {const int Invalid=-1;}
|
||||
namespace AudioSystem {int stopped=0;void TerminateSound(int){stopped++;}}
|
||||
struct ERR_GameMode {static inline ERR_GameMode* live=nullptr;bool active=true;
|
||||
static ERR_GameMode* Get(){return live;}bool Active(){return active;}};
|
||||
struct RestCallback {string data;void SetOnSuccess(int){}void SetOnError(int){}string GetData(){return data;}};
|
||||
int requests=0;
|
||||
struct RestContext {void SetTimeout(int){}void GET(RestCallback*,string){requests++;}};
|
||||
struct Api {RestContext context;RestContext* GetContext(string){return &context;}};
|
||||
struct Rules {static bool HTTPReady(){return true;}
|
||||
''' + body(SOURCE, 'static bool URL(') + r'''
|
||||
};
|
||||
namespace FileMode {enum Type {WRITE,READ};}
|
||||
int writes=0,opens=0,deletes=0,closes=0,maxWrite=0;
|
||||
bool openWrite=true,openRead=true,shortWrite=false;
|
||||
int lengthOverride=-1;
|
||||
struct File {
|
||||
bool exists=false;Array<int> data;int cursor=0;
|
||||
int WriteArray(Array<int>& values,int elementSize){
|
||||
assert(exists&&elementSize==1);writes++;maxWrite=std::max(maxWrite,values.Count());
|
||||
int count=values.Count()-(shortWrite?1:0);
|
||||
for(int i=0;i<count;i++)data.Insert(values[i]);
|
||||
return count;
|
||||
}
|
||||
void Close(){closes++;}
|
||||
int GetLength(){return lengthOverride>=0?lengthOverride:data.Count();}
|
||||
void Read(string& result,int count){result="";for(int i=0;i<count&&cursor<data.Count();i++)result+=char(data[cursor++]);}
|
||||
void Seek(int offset){cursor=offset;}
|
||||
};
|
||||
namespace FileIO {
|
||||
std::map<string,File> files;std::map<string,int> deleteCounts;
|
||||
void DeleteFile(string path){deletes++;deleteCounts[path]++;auto& file=files[path];file.exists=false;file.data.Clear();}
|
||||
File* OpenFile(string path,FileMode::Type mode){
|
||||
opens++;auto& file=files[path];
|
||||
if(mode==FileMode::WRITE){if(!openWrite)return nullptr;file.exists=true;file.data.Clear();}
|
||||
else if(!openRead||!file.exists)return nullptr;
|
||||
file.cursor=0;return &file;
|
||||
}
|
||||
}
|
||||
struct Client;
|
||||
struct Game {
|
||||
Api api;Array<Client*> m_ERRVoiceDownloads;Api* GetRestApi(){return &api;}
|
||||
void ERR_QueueVoiceDownload(Client*);void ERR_CancelVoiceDownload(Client*);
|
||||
void ERR_PrepareVoiceDownloads();void ERR_StopVoiceDownloads();
|
||||
} game;
|
||||
Game* GetGame(){return &game;}
|
||||
struct Client {
|
||||
ERR_GameMode* m_Mode=nullptr;string m_Path,m_Text;Connection connection;Connection* m_Connection=&connection;
|
||||
bool m_Stopped=false,m_Downloaded=false,m_Started=false;float m_Wait=0;
|
||||
RestCallback* m_Callback=nullptr;ERR_WelcomeVoiceDecoder* m_Decoder=nullptr;File* m_Output=nullptr;
|
||||
int m_PreparationBytes=0,m_PreparationFrames=0,m_PreparationLongest=0,m_Handle=AudioHandle::Invalid,plays=0;
|
||||
explicit Client(string path):m_Mode(ERR_GameMode::live),m_Path(path){}
|
||||
bool Permit(){return true;}virtual void Play(){plays++;}
|
||||
''' + client_methods + r'''
|
||||
void Receive(string encoded){Tick(3,true,"http://voice/clip","hello");RestCallback response;response.data=encoded;Downloaded(&response);}
|
||||
};
|
||||
''' + '\n'.join(queue_methods) + r'''
|
||||
struct Soldier:Client {
|
||||
using Client::Client;bool Prepared=false;float m_Age=0;string GraphPath;
|
||||
void Play() override{plays++;Prepared=true;} // Native graph/audio boundary.
|
||||
''' + soldier_methods + r'''
|
||||
};
|
||||
void Drain(){int frames=0;while(!game.m_ERRVoiceDownloads.IsEmpty()){game.ERR_PrepareVoiceDownloads();assert(++frames<2000);}}
|
||||
int main(){
|
||||
ERR_GameMode mode;ERR_GameMode::live=&mode;
|
||||
string wav=WAV64;Array<int> expected=EXPECTED;
|
||||
// HTTP completion only enqueues; decoding and filesystem work wait for update.
|
||||
Client first("first"),second("second");first.Receive(wav);second.Receive(wav);
|
||||
assert(game.m_ERRVoiceDownloads.Count()==2&&writes==0&&opens==0);
|
||||
assert(first.PreparingDownload()&&second.PreparingDownload());
|
||||
// Duplicate advertisements cannot cause multiple preparations in one frame.
|
||||
game.ERR_QueueVoiceDownload(&first);assert(game.m_ERRVoiceDownloads.Count()==2);
|
||||
game.ERR_PrepareVoiceDownloads();assert(writes==1&&first.m_PreparationFrames==1&&second.m_PreparationFrames==0);
|
||||
first.Tick(1,true,"","");assert(first.plays==0&&!first.m_Downloaded);
|
||||
game.ERR_PrepareVoiceDownloads();assert(writes==2&&first.m_PreparationFrames==1&&second.m_PreparationFrames==1);
|
||||
int beforeWrites=writes;game.ERR_PrepareVoiceDownloads();assert(writes-beforeWrites==1);
|
||||
Drain();assert(maxWrite<=4095&&FileIO::files["first"].data==expected&&FileIO::files["second"].data==expected);
|
||||
assert(first.m_Downloaded&&second.m_Downloaded&&first.PreparingDownload()&&first.plays==0);
|
||||
first.Tick(1,false,"","");assert(first.plays==0);
|
||||
first.Tick(1,true,"","");first.Tick(1,true,"","");assert(first.plays==1&&!first.PreparingDownload());
|
||||
// Four simultaneous clips expose native unordered-removal behavior which a
|
||||
// two-client queue cannot distinguish. Every live client gets one turn before
|
||||
// any gets another, even after a middle client is cancelled.
|
||||
Client fairA("fairA"),fairB("fairB"),fairC("fairC"),fairD("fairD");
|
||||
fairA.Receive(wav);fairB.Receive(wav);fairC.Receive(wav);fairD.Receive(wav);
|
||||
game.ERR_PrepareVoiceDownloads();
|
||||
assert(fairA.m_PreparationFrames==1&&fairB.m_PreparationFrames==0&&fairC.m_PreparationFrames==0&&fairD.m_PreparationFrames==0);
|
||||
assert(game.m_ERRVoiceDownloads==Array<Client*>({&fairB,&fairC,&fairD,&fairA}));
|
||||
fairC.Stop();assert(game.m_ERRVoiceDownloads==Array<Client*>({&fairB,&fairD,&fairA}));
|
||||
game.ERR_PrepareVoiceDownloads();game.ERR_PrepareVoiceDownloads();
|
||||
assert(fairA.m_PreparationFrames==1&&fairB.m_PreparationFrames==1&&fairC.m_PreparationFrames==0&&fairD.m_PreparationFrames==1);
|
||||
for(int i=0;i<3;i++)game.ERR_PrepareVoiceDownloads();
|
||||
assert(fairA.m_PreparationFrames==2&&fairB.m_PreparationFrames==2&&fairD.m_PreparationFrames==2);
|
||||
assert(!fairA.m_Downloaded&&!fairB.m_Downloaded&&!fairD.m_Downloaded);
|
||||
Drain();assert(fairA.m_Downloaded&&fairB.m_Downloaded&&fairD.m_Downloaded&&!fairC.m_Downloaded);
|
||||
assert(FileIO::files["fairA"].data==expected&&FileIO::files["fairB"].data==expected&&FileIO::files["fairD"].data==expected);
|
||||
// Cancelling one job closes/deletes its partial file and preserves other jobs.
|
||||
Client stopped("stopped"),survivor("survivor");stopped.Receive(wav);survivor.Receive(wav);
|
||||
game.ERR_PrepareVoiceDownloads();int beforeCloses=closes;stopped.Stop();
|
||||
assert(closes==beforeCloses+1&&!FileIO::files["stopped"].exists&&!stopped.PreparingDownload());
|
||||
assert(game.m_ERRVoiceDownloads.Count()==1);beforeWrites=writes;
|
||||
stopped.Stop();RestCallback late;late.data=wav;stopped.Downloaded(&late);
|
||||
stopped.Tick(1,true,"","");assert(stopped.plays==0&&writes==beforeWrites&&game.m_ERRVoiceDownloads.Count()==1);
|
||||
Drain();assert(survivor.m_Downloaded);
|
||||
// Each failure path removes the partial file and never announces readiness.
|
||||
for(int failure=0;failure<7;failure++){
|
||||
Client bad("bad"+std::to_string(failure));string input=wav;
|
||||
if(failure==0)input[input.size()-5]='?'; // malformed final slice
|
||||
if(failure==1)input[0]='A'; // decodes, but RIFF signature is invalid
|
||||
bad.Receive(input);game.ERR_PrepareVoiceDownloads();
|
||||
if(failure==2)shortWrite=true;
|
||||
if(failure==3)openRead=false;
|
||||
if(failure==4)lengthOverride=43;
|
||||
if(failure==5)lengthOverride=2097153;
|
||||
if(failure==6)mode.active=false;
|
||||
Drain();bad.Tick(1,true,"","");
|
||||
assert(!bad.m_Downloaded&&!bad.PreparingDownload()&&bad.plays==0&&!FileIO::files[bad.m_Path].exists);
|
||||
shortWrite=false;openRead=true;lengthOverride=-1;mode.active=true;
|
||||
}
|
||||
Client noFile("noFile");noFile.Receive(wav);openWrite=false;Drain();openWrite=true;
|
||||
assert(!noFile.m_Downloaded&&!noFile.PreparingDownload()&&!FileIO::files["noFile"].exists);
|
||||
Client invalidSize("invalidSize");invalidSize.Receive("AAAA");assert(!invalidSize.PreparingDownload()&&game.m_ERRVoiceDownloads.IsEmpty());
|
||||
// World/main-menu teardown empties every queued job without preparing it.
|
||||
Client oldWorld("oldWorld"),otherOldWorld("otherOldWorld");oldWorld.Receive(wav);otherOldWorld.Receive(wav);
|
||||
game.ERR_PrepareVoiceDownloads();beforeWrites=writes;game.ERR_StopVoiceDownloads();
|
||||
assert(game.m_ERRVoiceDownloads.IsEmpty()&&oldWorld.m_Stopped&&otherOldWorld.m_Stopped&&writes==beforeWrites);
|
||||
assert(!FileIO::files["oldWorld"].exists&&!FileIO::files["otherOldWorld"].exists);
|
||||
// A stale world is also rejected if its queue entry survives until a tick.
|
||||
Client stale("stale");stale.Receive(wav);ERR_GameMode::live=nullptr;Drain();
|
||||
assert(!stale.PreparingDownload()&&!stale.m_Downloaded&&!FileIO::files["stale"].exists);
|
||||
ERR_GameMode::live=&mode;
|
||||
// A completed download may spend more than 30 seconds in the shared queue or
|
||||
// wait for playback readiness. Neither state is a stalled HTTP request.
|
||||
Soldier soldier("soldier");soldier.Receive(wav);soldier.Tick(31,true,"","");
|
||||
assert(!soldier.m_Stopped&&soldier.PreparingDownload()&&!soldier.Prepared&&soldier.plays==0);
|
||||
Drain();assert(soldier.m_Downloaded);
|
||||
soldier.Tick(31,false,"","");assert(!soldier.m_Stopped&&soldier.m_Downloaded&&soldier.plays==0);
|
||||
soldier.Tick(1,true,"","");assert(soldier.Prepared&&soldier.plays==1&&!soldier.m_Stopped);
|
||||
soldier.Tick(120,true,"","");assert(soldier.Prepared&&!soldier.m_Stopped&&soldier.plays==1);
|
||||
// An HTTP request that never delivers and a failed preparation still expire.
|
||||
Soldier stalled("stalled");stalled.Tick(3,true,"http://voice/clip","");
|
||||
assert(stalled.m_Started&&!stalled.PreparingDownload());stalled.Tick(28,true,"","");assert(stalled.m_Stopped);
|
||||
Soldier failed("failed");failed.Receive("AAAA");failed.Tick(28,true,"","");assert(failed.m_Stopped);
|
||||
// A subclass stop invoked through the queue closes both partial WAV and graph
|
||||
// exactly once; retained stopped clips are harmless on subsequent updates.
|
||||
Soldier stopping("stopping");stopping.GraphPath="stopping.acp";
|
||||
FileIO::files[stopping.GraphPath].exists=true;stopping.Receive(wav);game.ERR_PrepareVoiceDownloads();
|
||||
stopping.m_Handle=3;int beforeAudioStops=AudioSystem::stopped;
|
||||
game.ERR_StopVoiceDownloads();assert(stopping.m_Stopped&&!stopping.Prepared);
|
||||
assert(!FileIO::files[stopping.GraphPath].exists&&FileIO::deleteCounts[stopping.GraphPath]==1);
|
||||
int beforeDeletes=deletes;stopping.Stop();stopping.Tick(60,true,"","");
|
||||
assert(deletes==beforeDeletes&&AudioSystem::stopped==beforeAudioStops+1&&game.m_ERRVoiceDownloads.IsEmpty());
|
||||
}
|
||||
'''.replace('WAV64', wav64).replace('EXPECTED', expected))
|
||||
|
||||
print('PASS: production client callbacks only queue; shared updates prepare one bounded slice, preserve bytes, handle file failures and cancel safely before playback')
|
||||
print('PASS: production soldier timeout preserves queued/completed preparation, expires stalled requests and removes stopped graphs only once')
|
||||
Reference in New Issue
Block a user