// Ryzom - MMORPG Framework
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
//-----------------------------------------------------------------------------
// include
//-----------------------------------------------------------------------------
// nel
#include "nel/misc/types_nl.h"
#include "nel/misc/common.h"
#include "nel/misc/sstring.h"
#include "nel/misc/smart_ptr.h"
#include "nel/misc/singleton.h"
#include "nel/misc/command.h"
#include "nel/misc/file.h"
// game share
#include "game_share/utils.h"
// local
#include "patchman_tester.h"
//-----------------------------------------------------------------------------
// namespaces
//-----------------------------------------------------------------------------
using namespace std;
using namespace NLMISC;
//-----------------------------------------------------------------------------
// namespace PATCHMAN
//-----------------------------------------------------------------------------
namespace PATCHMAN
{
//-----------------------------------------------------------------------------
// class CVariableSet
//-----------------------------------------------------------------------------
class CVariableSet
{
public:
// clear out the set of variables
void clear();
// set the value of a variable
void set(const CSString& varName,const CSString& value);
// get the value of a variable
const CSString& get(const CSString& varName) const;
// expand the variable names in a string out to their values (may do simple expression eval later too)
// variable names are assumed to be of the form: '$' eg: "$MYVAR"
CSString expand(CSString inputString) const;
// dump the variable set to a given log
void dump(CLog& log) const;
private:
typedef map TVariables;
TVariables _Variables;
};
//-----------------------------------------------------------------------------
// class CScriptLine
//-----------------------------------------------------------------------------
class CScriptLine
{
public:
// ctors
CScriptLine();
CScriptLine(const CSString& context, const CSString& keyword, const CSString& args);
// accessors
bool isValid() const;
CSString toString() const;
// execution - returns false if script should abort, otherwise true
// if there is an error then the error message will be not empty on return
bool execute(CVariableSet& vars, CSString& errorMsg) const;
private:
enum TActions
{
// the invalid operation 'no op'
ACT_NOP,
// conditional - stop execution of this routine with 'success' return value if condition fails
ACT_ONLYIF, ACT_ONLYIF_NOT,
// assert
ACT_ASSERT, ACT_ASSERT_NOT,
// set, inc or dec a variable
ACT_SET, ACT_INC, ACT_DEC,
// display a debug, info or warning
ACT_DEBUG, ACT_INFO, ACT_WARN,
// execute an NLMISC command
ACT_CMD,
};
CSString _RawCmdLine;
CSString _Context;
TActions _Action;
CVectorSString _Args;
};
//-----------------------------------------------------------------------------
// class CScriptRoutine
//-----------------------------------------------------------------------------
class CScriptRoutine
{
public:
// ctor
CScriptRoutine(const CSString& name=CSString());
// building up the script routine from input...
// returns true if line is valid, otherwise false
bool addLine(const CSString& context, const CSString& keyword,const CSString& args);
// execute the script
// return true if execution went OK, false if errors encountered / script asserts triggered
bool execute(CVariableSet& vars) const;
// accessors
bool isEmpty() const;
CSString getName() const;
private:
// the script name (the name of the event that triggers it)
CSString _Name;
// the script lines
typedef vector TLines;
TLines _Lines;
};
//-----------------------------------------------------------------------------
// class CPatchmanTesterImplementation
//-----------------------------------------------------------------------------
class CPatchmanTesterImplementation: public CPatchmanTester, public CRefCount
{
public:
// clear everything
void clear();
// clear loaded script(s) but leave state intact
void clearScript();
// clear state variables but leave scripts intact
void clearState();
// load a script file
void loadScript(const NLMISC::CSString& fileName);
// set a state variable
void set(const NLMISC::CSString& variableName,const NLMISC::CSString& value);
void set(const NLMISC::CSString& variableName,sint32 value);
// trigger an event
void trigger(const NLMISC::CSString& eventName);
// debug routine - display internal state info
void dump(NLMISC::CLog& log);
// debugging routine - displays the syntax help for the patchtest script files
void help(NLMISC::CLog& log);
private:
// private methods
typedef std::set TFileNameSet;
void _readScriptFile(const NLMISC::CSString& fileName,uint32& errors,TFileNameSet& fileNameSet);
private:
// state variables (map of var name to value)
CVariableSet _VariableSet;
// scripts (vector of routines)
typedef vector TScripts;
TScripts _Scripts;
};
//-----------------------------------------------------------------------------
// methods CVariableSet
//-----------------------------------------------------------------------------
void CVariableSet::clear()
{
_Variables.clear();
}
void CVariableSet::set(const CSString& varName,const CSString& value)
{
if (value.empty())
{
_Variables.erase(varName);
return;
}
_Variables[varName]= value;
}
const CSString& CVariableSet::get(const CSString& varName) const
{
// setup an empty string to use if the requestsed variable doesn't exist
static CSString emptyString;
// lookup the requested variable in the variable map
TVariables::const_iterator it= _Variables.find(varName);
// if the variable was found then return its value else return the empty string
return (it== _Variables.end())? emptyString: it->second;
}
CSString CVariableSet::expand(CSString inputString) const
{
// setup a result buffer
CSString result;
while (!inputString.empty())
{
// add all text up to the next variable name to our result buffer
result+= inputString.splitTo('$',true,false);
// if we've just absorbed the entire remainder of the input then drop out of the while loop
if (inputString.empty()) break;
// skip the '$' sign
inputString= inputString.leftCrop(1);
// extract the variable name
CSString varName= inputString.splitToOneOfSeparators(" \t:=$\"\'`&|~^+-*/()<>[]{};,.?!",true,false,false,false,false);
// if a variable name was found...
if (!varName.empty())
{
// add the value of the variable to the result
result+= get(varName);
}
else
{
// no variable name found so keep the '$' intact in the result expression (to help debugging)
result+='$';
}
}
return result;
}
void CVariableSet::dump(CLog& log) const
{
// iterate over the variables to determine the length of the longest variable name
uint32 maxlen=0;
for (TVariables::const_iterator it= _Variables.begin(); it!=_Variables.end(); ++it)
{
maxlen=max(maxlen,(uint32)it->first.size());
}
// display the variables
for (TVariables::const_iterator it= _Variables.begin(); it!=_Variables.end(); ++it)
{
log.displayNL("%*s = %s",maxlen,it->first.c_str(),it->second.c_str());
}
}
//-----------------------------------------------------------------------------
// methods CScriptLine
//-----------------------------------------------------------------------------
CScriptLine::CScriptLine(): _Action(ACT_NOP)
{
}
CScriptLine::CScriptLine(const CSString& context, const CSString& keyword, const CSString& args): _Action(ACT_NOP), _Context(context), _RawCmdLine(keyword+" "+args)
{
if (keyword=="assert")
{
CSString argTxt= args;
CSString varName= argTxt.firstWord(true).strip();
CSString op= argTxt.firstWord(true).strip(); // should be a '!' or a '='
op+= argTxt.firstWord(true).strip(); // should be a '='
DROP_IF(op!="==" && op!="!=",_Context+": Missing '==' or '!=' in line: "+keyword+" "+args,return);
_Args.push_back(varName);
_Args.push_back(argTxt.strip());
_Action= (op=="==")? ACT_ASSERT: ACT_ASSERT_NOT;
}
else if (keyword=="onlyif")
{
CSString argTxt= args;
CSString varName= argTxt.firstWord(true).strip();
CSString op= argTxt.firstWord(true).strip(); // should be a '!' or a '='
op+= argTxt.firstWord(true).strip(); // should be a '='
DROP_IF(op!="==" && op!="!=",_Context+": Missing '==' or '!=' in line: "+keyword+" "+args,return);
_Args.push_back(varName);
_Args.push_back(argTxt.strip());
_Action= (op=="==")? ACT_ONLYIF: ACT_ONLYIF_NOT;
}
else if (keyword=="set")
{
CSString argTxt= args;
CSString varName= argTxt.firstWord(true).strip();
CSString op= argTxt.firstWord(true).strip();
DROP_IF(op!="=",_Context+": Missing '=' in line: "+keyword+" "+args,return);
_Args.push_back(varName);
_Args.push_back(argTxt.strip());
_Action= ACT_SET;
}
else if (keyword=="inc") { _Args.push_back(args); _Action= ACT_INC; }
else if (keyword=="dec") { _Args.push_back(args); _Action= ACT_DEC; }
else if (keyword=="debug") { _Args.push_back(args); _Action= ACT_DEBUG; }
else if (keyword=="info") { _Args.push_back(args); _Action= ACT_INFO; }
else if (keyword=="warn") { _Args.push_back(args); _Action= ACT_WARN; }
else if (keyword=="cmd") { _Args.push_back(args); _Action= ACT_CMD; }
else
{
DROP(context+": Unable to build script line from input: "+keyword+" "+args,return);
}
}
bool CScriptLine::isValid() const
{
return (_Action!=ACT_NOP);
}
CSString CScriptLine::toString() const
{
return _RawCmdLine;
}
bool CScriptLine::execute(CVariableSet& vars, CSString& errorMsg) const
{
// start by clearing out the error message, to avoid confusion
errorMsg.clear();
switch (_Action)
{
case ACT_NOP:
BOMB("Trying to execute bad script command: "+_RawCmdLine,return false);
case ACT_ONLYIF:
nlassert(_Args.size()==2);
if (vars.get(_Args[0]) != _Args[1])
{
return false;
}
break;
case ACT_ONLYIF_NOT:
nlassert(_Args.size()==2);
if (vars.get(_Args[0]) == _Args[1])
{
return false;
}
break;
case ACT_ASSERT:
nlassert(_Args.size()==2);
if (vars.get(_Args[0]) != _Args[1])
{
errorMsg= _Context+": Assert Failed: "+_RawCmdLine;
return false;
}
break;
case ACT_ASSERT_NOT:
nlassert(_Args.size()==2);
if (vars.get(_Args[0]) == _Args[1])
{
errorMsg= _Context+": Assert Failed: "+_RawCmdLine;
return false;
}
break;
case ACT_SET:
nlassert(_Args.size()==2);
vars.set(_Args[0], vars.expand(_Args[1]));
break;
case ACT_INC:
nlassert(_Args.size()==1);
vars.set(_Args[0], NLMISC::toString("%i",vars.get(_Args[0]).atosi()+1));
break;
case ACT_DEC:
nlassert(_Args.size()==1);
vars.set(_Args[0], NLMISC::toString("%i",vars.get(_Args[0]).atosi()-1));
break;
case ACT_DEBUG:
nlassert(_Args.size()==1);
nldebug("%s",vars.expand(_Args[0]).c_str());
break;
case ACT_INFO:
nlassert(_Args.size()==1);
nlinfo("%s",vars.expand(_Args[0]).c_str());
break;
case ACT_WARN:
nlassert(_Args.size()==1);
nlwarning("%s",vars.expand(_Args[0]).c_str());
break;
case ACT_CMD:
nlassert(_Args.size()==1);
NLMISC::ICommand::execute(vars.expand(_Args[0]),*NLMISC::InfoLog);
break;
}
// all went well so return true ... the script can continue
return true;
}
//-----------------------------------------------------------------------------
// methods CScriptRoutine
//-----------------------------------------------------------------------------
CScriptRoutine::CScriptRoutine(const CSString& name): _Name(name)
{
}
bool CScriptRoutine::addLine(const CSString& context, const CSString& keyword,const CSString& args)
{
// try to build a new script line from the supplied text...
CScriptLine theNewLine(context,keyword,args);
// if we failed to create a valid line from the given text then just return false
// there should already have been an explication of the problem in the CScriptLine ctor
if (!theNewLine.isValid())
{
return false;
}
// the line is valid so append it to our _Lines vector
_Lines.push_back(theNewLine);
return true;
}
bool CScriptRoutine::execute(CVariableSet& vars) const
{
for (uint32 i=0; i<_Lines.size(); ++i)
{
// sertup a variable to hold an error message in case of failed assert, etc
CSString errorMsg;
// execute the line
bool ok= _Lines[i].execute(vars,errorMsg);
// if the execution returned true then continue to the next line
if (ok) continue;
// the execute returned false so see if we have an error...
if (!errorMsg.empty())
{
nlwarning("Test script failed: %s",errorMsg.c_str());
}
// return success or fail depending on whether we have an error message...
return errorMsg.empty();
}
// the script executed successfully through to the end
return true;
}
bool CScriptRoutine::isEmpty() const
{
return _Lines.empty();
}
CSString CScriptRoutine::getName() const
{
return _Name;
}
//-----------------------------------------------------------------------------
// methods CPatchmanTesterImplementation
//-----------------------------------------------------------------------------
void CPatchmanTesterImplementation::clear()
{
clearScript();
clearState();
}
void CPatchmanTesterImplementation::clearScript()
{
_Scripts.clear();
}
void CPatchmanTesterImplementation::clearState()
{
_VariableSet.clear();
}
void CPatchmanTesterImplementation::loadScript(const NLMISC::CSString& fileName)
{
// setup variables used by recursive script file reader
uint32 errors=0;
TFileNameSet fileNameSet;
// try reading ht e script file
_readScriptFile(fileName,errors,fileNameSet);
// if there were errors then abort the read...
if (errors!=0)
{
nlwarning("Script parse failed: %u errors encountered",errors);
clear();
}
}
void CPatchmanTesterImplementation::_readScriptFile(const NLMISC::CSString& fileName,uint32& errors,CPatchmanTesterImplementation::TFileNameSet& fileNameSet)
{
// read in the src file
NLMISC::CSString fileContents;
fileContents.readFromFile(fileName);
DROP_IF(fileContents.empty(),"File not found: "+fileName, ++errors;return);
// split the file into lines
NLMISC::CVectorSString lines;
fileContents.splitLines(lines);
// process the lines one by one
for (uint32 i=0;i' but found: "+line, ++errors;continue);
bool ok= _Scripts.back().addLine(context,keyword,args);
if (!ok) ++errors;
}
}
}
void CPatchmanTesterImplementation::set(const NLMISC::CSString& variableName,const NLMISC::CSString& value)
{
_VariableSet.set(variableName,value);
trigger(variableName);
}
void CPatchmanTesterImplementation::set(const NLMISC::CSString& variableName,sint32 value)
{
set(variableName,NLMISC::toString("%i",value));
}
void CPatchmanTesterImplementation::trigger(const NLMISC::CSString& eventName)
{
for (uint32 i=0;i <_Scripts.size(); ++i)
{
if (_Scripts[i].getName()==eventName)
{
_Scripts[i].execute(_VariableSet);
}
}
}
void CPatchmanTesterImplementation::dump(NLMISC::CLog& log)
{
log.displayNL("---------------------------------------------");
log.displayNL("dump of patchman's patchtest state variables");
log.displayNL("---------------------------------------------");
_VariableSet.dump(log);
log.displayNL("---------------------------------------------");
log.displayNL("list of triggerable scripts");
log.displayNL("---------------------------------------------");
// build a set of script triggers to eliminate duplicates
typedef std::set TTheSet;
TTheSet theSet;
for (uint32 i=0;i<_Scripts.size();++i)
{
theSet.insert(_Scripts[i].getName());
}
// run through the set we just built, displaying scrip names
for (TTheSet::iterator it= theSet.begin(); it!=theSet.end(); ++it)
{
log.displayNL("on %s",it->c_str());
}
log.displayNL("---------------------------------------------");
}
void CPatchmanTesterImplementation::help(NLMISC::CLog& log)
{
log.displayNL("---------------------------------------------");
log.displayNL("patchman's patchtest script");
log.displayNL("---------------------------------------------");
log.displayNL("include ");
log.displayNL("on |");
log.displayNL(" cmd ");
log.displayNL(" assert '=='|'!=' |");
log.displayNL(" onlyif '=='|'!=' |");
log.displayNL(" set '=' |");
log.displayNL(" inc ");
log.displayNL(" dec ");
log.displayNL(" warn ");
log.displayNL(" info ");
log.displayNL(" debug ");
log.displayNL("---------------------------------------------");
log.displayNL("Note: variable name substitution example:");
log.displayNL(" set toto abc d");
log.displayNL(" info hello $toto$toto $toto");
log.displayNL("=> \"hello abc dabc d abc d\"");
log.displayNL("---------------------------------------------");
}
//-----------------------------------------------------------------------------
// methods CPatchmanTester
//-----------------------------------------------------------------------------
CPatchmanTester& CPatchmanTester::getInstance()
{
static CSmartPtr theInstance;
if (theInstance==NULL)
{
theInstance= new CPatchmanTesterImplementation;
}
return *theInstance;
}
} // end of namespace
NLMISC_CATEGORISED_COMMAND(patchman,patchtestClear,"clear out the patchtest singleton","")
{
if (args.size()!=0)
return false;
PATCHMAN::CPatchmanTester::getInstance().clear();
return true;
}
NLMISC_CATEGORISED_COMMAND(patchman,patchtestDump,"dump the state of the patchtest singleton","")
{
if (args.size()!=0)
return false;
PATCHMAN::CPatchmanTester::getInstance().dump(log);
return true;
}
NLMISC_CATEGORISED_COMMAND(patchman,patchtestLoad,"load a patchtest script","")
{
if (args.size()!=1)
return false;
PATCHMAN::CPatchmanTester::getInstance().loadScript(args[0]);
return true;
}
NLMISC_CATEGORISED_COMMAND(patchman,patchtestHelp,"display a list of keywords for the patch script","")
{
if (args.size()!=0)
return false;
PATCHMAN::CPatchmanTester::getInstance().help(log);
return true;
}
NLMISC_CATEGORISED_COMMAND(patchman,patchtestSet,"set a patchtest variable"," ")
{
if (args.size()!=2)
return false;
PATCHMAN::CPatchmanTester::getInstance().set(args[0],args[1]);
return true;
}
NLMISC_CATEGORISED_COMMAND(patchman,patchtestTrigger,"trigger a patchtest event","")
{
if (args.size()!=1)
return false;
PATCHMAN::CPatchmanTester::getInstance().trigger(args[0]);
return true;
}