N°3171 - Friendly name and obsolescence flag not refreshed (#151)

- Compute any type of expression on server side
- Recompute friendly name and obsolescence flag on server side (DBOBject)
- Bonus : compute dependency for external keys
This commit is contained in:
Romain Quetiez
2020-07-10 17:26:37 +02:00
committed by GitHub
parent b1fa429234
commit acf0548c4c
9 changed files with 1435 additions and 51 deletions

View File

@@ -6653,6 +6653,23 @@ class AttributeExternalKey extends AttributeDBFieldVoid
return (int)$proposedValue;
}
public function GetPrerequisiteAttributes($sClass = null)
{
$aAttributes = parent::GetPrerequisiteAttributes($sClass);
$oExpression = DBSearch::FromOQL($this->GetValuesDef()->GetFilterExpression())->GetCriteria();
foreach ($oExpression->GetParameters('this') as $sAttCode)
{
// Skip the id as it cannot change anyway
if ($sAttCode =='id') continue;
if (!in_array($sAttCode, $aAttributes))
{
$aAttributes[] = $sAttCode;
}
}
return $aAttributes;
}
public function GetMaximumComboLength()
{
return $this->GetOptional('max_combo_length', MetaModel::GetConfig()->Get('max_combo_length'));
@@ -11561,7 +11578,17 @@ class AttributeFriendlyName extends AttributeDefinition
public function GetPrerequisiteAttributes($sClass = null)
{
return $this->GetOptional("depends_on", array());
// Code duplicated with AttributeObsolescenceFlag
$aAttributes = $this->GetOptional("depends_on", array());
$oExpression = $this->GetOQLExpression();
foreach ($oExpression->ListRequiredFields() as $sClass => $sAttCode)
{
if (!in_array($sAttCode, $aAttributes))
{
$aAttributes[] = $sAttCode;
}
}
return $aAttributes;
}
public static function IsScalar()
@@ -12793,7 +12820,17 @@ class AttributeObsolescenceFlag extends AttributeBoolean
public function GetPrerequisiteAttributes($sClass = null)
{
return $this->GetOptional("depends_on", array());
// Code duplicated with AttributeFriendlyName
$aAttributes = $this->GetOptional("depends_on", array());
$oExpression = $this->GetOQLExpression();
foreach ($oExpression->ListRequiredFields() as $sClass => $sAttCode)
{
if (!in_array($sAttCode, $aAttributes))
{
$aAttributes[] = $sAttCode;
}
}
return $aAttributes;
}
public function IsDirectField()

View File

@@ -569,43 +569,41 @@ abstract class DBObject implements iDisplay
$this->Reload();
}
if ($oAttDef->IsExternalKey())
if ($oAttDef->IsExternalKey() && is_object($value))
{
if (is_object($value))
// Setting an external key with a whole object (instead of just an ID)
// let's initialize also the external fields that depend on it
// (useful when building objects in memory and not from a query)
/** @var \AttributeExternalKey $oAttDef */
if ((get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass())))
{
// Setting an external key with a whole object (instead of just an ID)
// let's initialize also the external fields that depend on it
// (useful when building objects in memory and not from a query)
/** @var \AttributeExternalKey $oAttDef */
if ( (get_class($value) != $oAttDef->GetTargetClass()) && (!is_subclass_of($value, $oAttDef->GetTargetClass())))
{
throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored");
}
throw new CoreUnexpectedValue("Trying to set the value of '$sAttCode', to an object of class '".get_class($value)."', whereas it's an ExtKey to '".$oAttDef->GetTargetClass()."'. Ignored");
}
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
{
/** @var \AttributeExternalField $oDef */
if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
{
/** @var \AttributeExternalField $oDef */
if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
{
/** @var \DBObject $value */
$this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode());
$this->m_aLoadedAtt[$sCode] = true;
}
/** @var \DBObject $value */
$this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode());
$this->m_aLoadedAtt[$sCode] = true;
}
elseif (in_array($sAttCode, $oDef->GetPrerequisiteAttributes(get_class($this))))
{
$this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode);
unset($this->m_aLoadedAtt[$sCode]);
}
}
else if ($this->m_aCurrValues[$sAttCode] != $value)
}
else if ($this->m_aCurrValues[$sAttCode] !== $value)
{
// Invalidate dependent fields so that they get reloaded in case they are needed (See Get())
//
foreach (MetaModel::GetDependentAttributes(get_class($this), $sAttCode) as $sCode)
{
// Setting an external key, but no any other information is available...
// Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get())
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
{
/** @var \AttributeExternalKey $oDef */
if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
{
$this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode);
unset($this->m_aLoadedAtt[$sCode]);
}
}
$this->m_aCurrValues[$sCode] = $this->GetDefaultValue($sCode);
unset($this->m_aLoadedAtt[$sCode]);
}
}
if ($oAttDef->IsLinkSet() && ($value != null))
@@ -787,20 +785,20 @@ abstract class DBObject implements iDisplay
{
// Standard case... we have the information directly
}
elseif ($this->m_bIsInDB && !$this->m_bDirty)
elseif ($this->m_bIsInDB && !$this->m_bFullyLoaded && !$this->m_bDirty)
{
// Lazy load (polymorphism): complete by reloading the entire object
// #@# non-scalar attributes.... handle that differently?
$oKPI = new ExecutionKPI();
$this->Reload();
$oKPI->ComputeStats('Reload', get_class($this).'/'.$sAttCode);
}
elseif ($sAttCode == 'friendlyname')
elseif ($oAttDef->IsBasedOnOQLExpression())
{
// The friendly name is not computed and the object is dirty
// Todo: implement the computation of the friendly name based on sprintf()
//
$this->m_aCurrValues[$sAttCode] = '';
// Recompute -which is likely to call Get()
//
/** @var AttributeFriendlyName|\AttributeObsolescenceFlag $oAttDef */
$this->m_aCurrValues[$sAttCode] = $this->EvaluateExpression($oAttDef->GetOQLExpression());
$this->m_aLoadedAtt[$sAttCode] = true;
}
else
{
@@ -5358,5 +5356,33 @@ abstract class DBObject implements iDisplay
break;
}
}
public function EvaluateExpression(Expression $oExpression)
{
$aFields = $oExpression->ListRequiredFields();
$aArgs = array();
foreach ($aFields as $sFieldDesc)
{
$aFieldParts = explode('.', $sFieldDesc);
if (count($aFieldParts) == 2)
{
$sClass = $aFieldParts[0];
$sAttCode = $aFieldParts[1];
}
else
{
$sClass = get_class($this);
$sAttCode = $aFieldParts[0];
}
if (get_class($this) != $sClass) continue;
if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode)) continue;
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$aSQLValues = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
$value = reset($aSQLValues);
$aArgs[$sFieldDesc] = $value;
}
return $oExpression->Evaluate($aArgs);
}
}

View File

@@ -944,11 +944,6 @@ abstract class MetaModel
{
self::_check_subclass($sClass);
$oAtt = self::GetAttributeDef($sClass, $sAttCode);
// Temporary implementation: later, we might be able to compute
// the dependencies, based on the attributes definition
// (allowed values and default values)
// Even non-writable attributes (like ExternalFields) can now have Prerequisites
return $oAtt->GetPrerequisiteAttributes();
}

View File

@@ -21,6 +21,9 @@ class MissingQueryArgument extends CoreException
{
}
class NotYetEvaluatedExpression extends CoreException
{
}
/**
* @method Check($oModelReflection, array $aAliases, $sSourceQuery)
@@ -109,6 +112,47 @@ abstract class Expression
*/
abstract public function RenderExpression($bForSQL = false, &$aArgs = null, $bRetrofitParams = false);
/**
* Collect parameters, i.e. :parameter
*
* @param null $sParentFilter
*
* @return array
*/
public function GetParameters($sParentFilter = null)
{
$aParameters = array();
$unused = $this->RenderExpression(false, $aParameters, true);
if (!is_null($sParentFilter)) $sParentFilter .= '->';
$aRet = array();
foreach($aParameters as $sParameter => $unused)
{
if (is_null($sParentFilter))
{
$aRet[] = $sParameter;
}
else
{
if (substr($sParameter, 0, strlen($sParentFilter)) == $sParentFilter)
{
$aRet[] = substr($sParameter, strlen($sParentFilter));
}
}
}
return $aRet;
}
/**
* Evaluate the value of the expression
*
* @param array $aArgs
*
* @throws \Exception if terms cannot be evaluated as scalars
*/
abstract public function Evaluate(array $aArgs);
/**
* Recursively renders the expression as a structure (array) suitable for a JSON export
*
@@ -319,6 +363,16 @@ class SQLExpression extends Expression
return $this->m_sSQL;
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
throw new Exception('a nested query cannot be evaluated');
}
// recursive rendering
public function toJSON(&$aArgs = null, $bRetrofitParams = false)
{
@@ -466,6 +520,149 @@ class BinaryExpression extends Expression
return "($sLeft $sOperator $sRight)";
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @return mixed
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
$mLeft = $this->GetLeftExpr()->Evaluate($aArgs);
$mRight = $this->GetRightExpr()->Evaluate($aArgs);
$sOperator = $this->GetOperator();
$sType = null;
switch($sOperator)
{
case '+':
case '-':
case '*':
case '/':
$sType = 'maths';
break;
case '=':
case '!=':
case '<>':
$sType = 'comp';
break;
case '>':
case '>=':
case '<':
case '<=':
$sType = 'numcomp';
break;
case 'OR':
case 'AND':
$sType = 'logical';
break;
case 'LIKE':
$sType = 'like';
break;
default:
throw new Exception("Operator '$sOperator' not yet supported");
}
switch ($sType){
case 'logical':
$bLeft = static::CastToBool($mLeft);
$bRight = static::CastToBool($mRight);
switch ($sOperator)
{
case 'OR':
$result = (int)($bLeft || $bRight);
break;
case 'AND':
$result = (int)($bLeft && $bRight);
break;
default:
throw new Exception("Logic: unknown operator '$sOperator'");
}
break;
case 'maths':
$iLeft = (int) $mLeft;
$iRight = (int) $mRight;
switch ($sOperator)
{
case '+' : $result = $iLeft + $iRight; break;
case '-' : $result = $iLeft - $iRight; break;
case '*' : $result = $iLeft * $iRight; break;
case '/' : $result = $iLeft / $iRight; break;
default:
throw new Exception("Logic: unknown operator '$sOperator'");
}
break;
case 'comp':
$left = $mLeft;
$right = $mRight;
switch ($sOperator)
{
case '=' : $result = ($left == $right); break;
case '!=' : $result = ($left != $right); break;
case '<>' : $result = ($left != $right); break;
default:
throw new Exception("Logic: unknown operator '$sOperator'");
}
break;
case 'numcomp':
$iLeft = static::ComparableValue($mLeft);
$iRight = static::ComparableValue($mRight);
switch ($sOperator)
{
case '=' : $result = ($iLeft == $iRight); break;
case '>' : $result = ($iLeft > $iRight); break;
case '<' : $result = ($iLeft < $iRight); break;
case '>=' : $result = ($iLeft >= $iRight); break;
case '<=' : $result = ($iLeft <= $iRight); break;
case '!=' : $result = ($iLeft != $iRight); break;
case '<>' : $result = ($iLeft != $iRight); break;
default:
throw new Exception("Logic: unknown operator '$sOperator'");
}
break;
case 'like':
$sEscaped = preg_quote($mRight, '/');
$sEscaped = str_replace(array('%', '_', '\\\\.*', '\\\\.'), array('.*', '.', '%', '_'), $sEscaped);
$result = (int) preg_match("/$sEscaped/i", $mLeft);
break;
}
return $result;
}
static protected function CastToBool($mValue)
{
if (is_string($mValue))
{
if (is_numeric($mValue))
{
return abs($mValue) > 0;
}
return false;
}
return (bool)$mValue;
}
static protected function ComparableValue($mixed)
{
if (is_string($mixed))
{
$oDate = new \DateTime($mixed);
if (($oDate->format('Y-m-d') == $mixed) || ($oDate->format('Y-m-d H:i:s') == $mixed))
{
$iRet = $oDate->format('U');
}
else
{
$iRet = (int) $mixed;
}
}
else
{
$iRet = $mixed;
}
return $iRet;
}
/**
* {@inheritDoc}
* @throws \MissingQueryArgument
@@ -865,6 +1062,16 @@ class MatchExpression extends BinaryExpression
return $sRet;
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
throw new Exception('evaluation of MATCHES not implemented yet');
}
public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
{
/** @var \FieldExpression $oLeft */
@@ -903,6 +1110,16 @@ class UnaryExpression extends Expression
return CMDBSource::Quote($this->m_value);
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
return $this->m_value;
}
/**
* {@inheritDoc}
* @throws \MissingQueryArgument
@@ -1049,6 +1266,16 @@ class ScalarExpression extends UnaryExpression
return $sRet;
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
return $this->m_value;
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -1357,6 +1584,21 @@ class FieldExpression extends UnaryExpression
return "`{$this->m_sParent}`.`{$this->m_sName}`";
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
$sKey = empty($this->m_sParent) ? $this->m_sName : "{$this->m_sParent}.{$this->m_sName}";
if (!array_key_exists($sKey, $aArgs))
{
throw new Exception("Missing field '$sKey' from context");
}
return $aArgs[$sKey];
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -1412,7 +1654,8 @@ class FieldExpression extends UnaryExpression
public function ListRequiredFields()
{
return array($this->m_sParent.'.'.$this->m_sName);
$sField = empty($this->m_sParent) ? $this->m_sName : "{$this->m_sParent}.{$this->m_sName}";
return array($sField);
}
public function CollectUsedParents(&$aTable)
@@ -1793,6 +2036,16 @@ class VariableExpression extends UnaryExpression
}
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
throw new Exception('not implemented yet');
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -1933,6 +2186,16 @@ class ListExpression extends Expression
return '('.implode(', ', $aRes).')';
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
throw new Exception('list expression not yet supported');
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -2140,6 +2403,16 @@ class NestedQueryExpression extends Expression
}
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
throw new Exception('a nested query cannot be evaluated');
}
public function Browse(Closure $callback)
{
$callback($this);
@@ -2240,6 +2513,252 @@ class FunctionExpression extends Expression
return $this->m_sVerb.'('.implode(', ', $aRes).')';
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
switch($this->m_sVerb)
{
case 'CONCAT':
$sRet = '';
foreach ($this->m_aArgs as $iPos => $oExpr)
{
$item = $oExpr->Evaluate($aArgs);
if (is_null($item)) return null;
$sRet .= $item;
}
return $sRet;
case 'CONCAT_WS':
if (count($this->m_aArgs) < 3)
{
throw new \Exception("Function {$this->m_sVerb} requires at least 3 arguments");
}
$sSeparator = $this->m_aArgs[0]->Evaluate($aArgs);
foreach ($this->m_aArgs as $iPos => $oExpr)
{
if ($iPos == 0) continue;
$item = $oExpr->Evaluate($aArgs);
if (is_null($item)) return null;
$aStrings[] = $item;
}
$sRet = implode($sSeparator, $aStrings);
return $sRet;
case 'SUBSTR':
if (count($this->m_aArgs) < 2)
{
throw new \Exception("Function {$this->m_sVerb} requires at least 2 arguments");
}
$sString = $this->m_aArgs[0]->Evaluate($aArgs);
$iRawPos = $this->m_aArgs[1]->Evaluate($aArgs);
$iPos = $iRawPos > 0 ?
$iRawPos - 1// 0-based in PHP (1-based in SQL)
: $iRawPos; // Negative
if (count($this->m_aArgs) == 2)
{
// Up to the end of the string
$sRet = substr($sString, $iPos);
}
else
{
// Length specified
$iLen = $this->m_aArgs[2]->Evaluate($aArgs);
$sRet = substr($sString, $iPos, $iLen);
}
return $sRet;
case 'TRIM':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$sRet = trim($this->m_aArgs[0]->Evaluate($aArgs));
return $sRet;
case 'INET_ATON':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$sRet = ip2long($this->m_aArgs[0]->Evaluate($aArgs));
return $sRet;
case 'INET_NTOA':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$sRet = long2ip($this->m_aArgs[0]->Evaluate($aArgs));
return $sRet;
case 'ISNULL':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$sRet = is_null($this->m_aArgs[0]->Evaluate($aArgs));
return $sRet;
case 'COALESCE':
if (count($this->m_aArgs) < 1)
{
throw new \Exception("Function {$this->m_sVerb} requires at least 1 argument");
}
$ret = null;
foreach($this->m_aArgs as $iPos => $oExpr)
{
$ret = $oExpr->Evaluate($aArgs);
if (!is_null($ret)) break;
}
return $ret;
case 'IF':
if (count($this->m_aArgs) != 3)
{
throw new \Exception("Function {$this->m_sVerb} requires 3 arguments");
}
$bCond = $this->m_aArgs[0]->Evaluate($aArgs);
if ($bCond)
{
$ret = $this->m_aArgs[1]->Evaluate($aArgs);
}
else
{
$ret = $this->m_aArgs[2]->Evaluate($aArgs);
}
return $ret;
case 'ELT':
if (count($this->m_aArgs) < 2)
{
throw new \Exception("Function {$this->m_sVerb} requires at least 2 arguments");
}
// First argument is the 1-based position
$iPosition = (int) $this->m_aArgs[0]->Evaluate($aArgs);
if (($iPosition == 0) || ($iPosition >= count($this->m_aArgs)))
{
// Out of range
$ret = null;
}
else
{
$ret = $this->m_aArgs[$iPosition]->Evaluate($aArgs);
}
return $ret;
case 'DATE':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$sRet = date('Y-m-d', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
return $sRet;
case 'YEAR':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$iRet = (int) date('Y', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
return $iRet;
case 'MONTH':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$iRet = (int) date('m', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
return $iRet;
case 'DAY':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$iRet = (int) date('d', strtotime($this->m_aArgs[0]->Evaluate($aArgs)));
return $iRet;
case 'DATE_FORMAT':
if (count($this->m_aArgs) != 2)
{
throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
}
$oDate = new DateTime($this->m_aArgs[0]->Evaluate($aArgs));
$sFormat = $this->m_aArgs[1]->Evaluate($aArgs);
$sFormat = str_replace(
array('%y', '%x', '%w', '%W', '%v', '%T', '%S', '%r', '%p', '%M', '%l', '%k', '%I', '%h', '%b', '%a', '%D', '%c', '%e', '%Y', '%d', '%m', '%H', '%i', '%s'),
array('y', 'o', 'w', 'l', 'W', 'H:i:s', 's', 'h:i:s A', 'A', 'F', 'g', 'H', 'h', 'h','M', 'D', 'jS', 'n', 'j', 'Y', 'd', 'm', 'H', 'i', 's'),
$sFormat);
if (preg_match('/%j/', $sFormat))
{
$sFormat = str_replace('%j', date_format($oDate, 'z') + 1, $sFormat);
}
if (preg_match('/%[fUuVX]/', $sFormat))
{
throw new NotYetEvaluatedExpression("Expression ".$this->RenderExpression().' cannot be evaluated (known limitation)');
}
$sRet = date_format($oDate, $sFormat);
return $sRet;
case 'TO_DAYS':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$oDate = new DateTime($this->m_aArgs[0]->Evaluate($aArgs));
$oZero = new DateTime('1582-01-01');
$iRet = (int) $oDate->diff($oZero)->format('%a') + 577815;
return $iRet;
case 'FROM_DAYS':
if (count($this->m_aArgs) != 1)
{
throw new \Exception("Function {$this->m_sVerb} requires 1 argument");
}
$iSince1582 = $this->m_aArgs[0]->Evaluate($aArgs) - 577814;
$oDate = new DateTime("1582-01-01 +$iSince1582 days");
$sRet = $oDate->format('Y-m-d');
return $sRet;
case 'NOW':
$sRet = date('Y-m-d H:i:s');
return $sRet;
case 'CURRENT_DATE':
$sRet = date('Y-m-d');
return $sRet;
case 'DATE_ADD':
if (count($this->m_aArgs) != 2)
{
throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
}
$sStartDate = $this->m_aArgs[0]->Evaluate($aArgs);
$sInterval = $this->m_aArgs[1]->Evaluate($aArgs);
$oDate = new DateTime("$sStartDate +$sInterval");
$sRet = $oDate->format('Y-m-d H:i:s');
return $sRet;
case 'DATE_SUB':
if (count($this->m_aArgs) != 2)
{
throw new \Exception("Function {$this->m_sVerb} requires 2 arguments");
}
$sStartDate = $this->m_aArgs[0]->Evaluate($aArgs);
$sInterval = $this->m_aArgs[1]->Evaluate($aArgs);
$oDate = new DateTime("$sStartDate -$sInterval");
$sRet = $oDate->format('Y-m-d H:i:s');
return $sRet;
default:
throw new Exception("Function {$this->m_sVerb} cannot be evaluated -unhandled yet");
}
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -2566,6 +3085,17 @@ class IntervalExpression extends Expression
return 'INTERVAL '.$this->m_oValue->RenderExpression($bForSQL, $aArgs, $bRetrofitParams).' '.$this->m_sUnit;
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
$iValue = $this->m_oValue->Evaluate($aArgs);
return "$iValue {$this->m_sUnit}";
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -2684,6 +3214,21 @@ class CharConcatExpression extends Expression
return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)";
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
$sRet = '';
foreach ($this->m_aExpressions as $oExpr)
{
$sRet .= $oExpr->Evaluate($aArgs);
}
return $sRet;
}
/**
* {@inheritDoc}
* @see Expression::ToJSON()
@@ -2825,6 +3370,21 @@ class CharConcatWSExpression extends CharConcatExpression
return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)";
}
/**
* Evaluate the value of the expression
* @param array $aArgs
* @throws \Exception if terms cannot be evaluated as scalars
*/
public function Evaluate(array $aArgs)
{
$aRes = array();
foreach ($this->m_aExpressions as $oExpr)
{
$aRes .= $oExpr->Evaluate($aArgs);
}
return implode($this->m_separator, $aRes);
}
public function Browse(Closure $callback)
{
$callback($this);

View File

@@ -610,9 +610,6 @@
</field>
<field id="manager_id" xsi:type="AttributeExternalKey">
<filter><![CDATA[SELECT Person]]></filter>
<dependencies>
<attribute id="org_id"/>
</dependencies>
<sql>manager_id</sql>
<target_class>Person</target_class>
<is_null_allowed>true</is_null_allowed>

View File

@@ -713,7 +713,7 @@ class ItopDataTestCase extends ItopTestCase
$iId = $oLnk->Get('functionalci_id');
if (!empty($aWaitedCIList))
{
$this->assertTrue(array_key_exists($iId, $aWaitedCIList));
$this->assertArrayHasKey($iId, $aWaitedCIList);
$this->assertEquals($aWaitedCIList[$iId], $oLnk->Get('impact_code'));
}
}
@@ -737,7 +737,7 @@ class ItopDataTestCase extends ItopTestCase
$iId = $oLnk->Get('contact_id');
if (!empty($aWaitedContactList))
{
$this->assertTrue(array_key_exists($iId, $aWaitedContactList));
$this->assertArrayHasKey($iId, $aWaitedContactList);
foreach ($aWaitedContactList[$iId] as $sAttCode => $oValue)
{
if (MetaModel::IsValidAttCode(get_class($oTicket), $sAttCode))
@@ -756,5 +756,29 @@ class ItopDataTestCase extends ItopTestCase
$this->iTestOrgId = $oOrg->GetKey();
}
/**
* Assert that a series of operations will trigger a given number of MySL queries
*
* @param $iExpectedCount Number of MySQL queries that should be executed
* @param callable $oFunction Operations to perform
*
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
*/
protected static function assertDBQueryCount($iExpectedCount, callable $oFunction)
{
$iInitialCount = (int) CMDBSource::QueryToScalar("SHOW SESSION STATUS LIKE 'Queries'", 1);
$oFunction();
$iFinalCount = (int) CMDBSource::QueryToScalar("SHOW SESSION STATUS LIKE 'Queries'", 1);
$iCount = $iFinalCount - 1 - $iInitialCount;
if ($iCount != $iExpectedCount)
{
static::fail("Expected $iExpectedCount queries. $iCount have been executed.");
}
else
{
// Otherwise PHP Unit will consider that no assertion has been made
static::assertTrue(true);
}
}
}

View File

@@ -48,6 +48,7 @@ class DBObjectTest extends ItopDataTestCase
/**
* Test default page name
* @covers DBObject::GetUIPage
*/
public function testGetUIPage()
{
@@ -81,6 +82,9 @@ class DBObjectTest extends ItopDataTestCase
array('PHP_INT_MIN', false));
}
/**
* @covers DBObject::GetOriginal
*/
public function testGetOriginal()
{
$oObject = $this->CreateUserRequest(190664);
@@ -88,4 +92,114 @@ class DBObjectTest extends ItopDataTestCase
static::assertNull($oObject->GetOriginal('sla_tto_passed'));
}
/**
* @covers DBObject::NewObject
* @covers DBObject::Get
* @covers DBObject::Set
*/
public function testAttributeRefresh_FriendlyName()
{
$oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2));
static::assertEquals('John Foo', $oObject->Get('friendlyname'));
$oObject->Set('name', 'Who');
static::assertEquals('John Who', $oObject->Get('friendlyname'));
}
/**
* @covers MetaModel::GetObject
* @covers DBObject::Get
* @covers DBObject::Set
*/
public function testAttributeRefresh_FriendlyNameFromDB()
{
$oObject = \MetaModel::NewObject('Person', array('name' => 'Gary', 'first_name' => 'Romain', 'org_id' => 3, 'location_id' => 2));
$oObject->DBInsert();
$iObjKey = $oObject->GetKey();
$oObject = \MetaModel::GetObject('Person', $iObjKey);
static::assertEquals('Romain Gary', $oObject->Get('friendlyname'));
$oObject->Set('name', 'Duris');
static::assertEquals('Romain Duris', $oObject->Get('friendlyname'));
}
/**
* @covers DBObject::NewObject
* @covers DBObject::Get
* @covers DBObject::Set
*/
public function testAttributeRefresh_ObsolescenceFlag()
{
$oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2));
static::assertEquals(false, (bool)$oObject->Get('obsolescence_flag'));
$oObject->Set('status', 'inactive');
static::assertEquals(true, (bool)$oObject->Get('obsolescence_flag'));
}
/**
* @covers DBObject::NewObject
* @covers DBObject::Get
* @covers DBObject::Set
*/
public function testAttributeRefresh_ExternalKeysAndFields()
{
static::assertDBQueryCount(0, function() use (&$oObject){
$oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2));
});
static::assertDBQueryCount(2, function() use (&$oObject){
static::assertEquals('Demo', $oObject->Get('org_id_friendlyname'));
static::assertEquals('Grenoble', $oObject->Get('location_id_friendlyname'));
});
// External key given as an id
static::assertDBQueryCount(1, function() use (&$oObject){
$oObject->Set('org_id', 2);
static::assertEquals('IT Department', $oObject->Get('org_id_friendlyname'));
});
// External key given as an object
static::assertDBQueryCount(1, function() use (&$oBordeaux){
$oBordeaux = \MetaModel::GetObject('Location', 1);
});
static::assertDBQueryCount(0, function() use (&$oBordeaux, &$oObject){
$oObject->Set('location_id', $oBordeaux);
static::assertEquals('IT Department', $oObject->Get('org_id_friendlyname'));
static::assertEquals('IT Department', $oObject->Get('org_name'));
static::assertEquals('Bordeaux', $oObject->Get('location_id_friendlyname'));
});
}
public function testSetExtKeyUnsetDependentAttribute()
{
$oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2));
$oOrg = \MetaModel::GetObject('Organization', 2);
$oObject->Set('org_id', $oOrg);
static::assertEquals(0, $oObject->Get('location_id'));
}
/**
* @group Integration
*/
public function testModelExpressions()
{
foreach (\MetaModel::GetClasses() as $sClass)
{
if (\MetaModel::IsAbstract($sClass)) continue;
$oObject = \MetaModel::NewObject($sClass);
foreach (\MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
if ($oAttDef->IsBasedOnOQLExpression())
{
$this->debug("$sClass::$sAttCode");
static::assertDBQueryCount(0, function() use (&$oObject, &$oAttDef){
$oObject->EvaluateExpression($oAttDef->GetOQLExpression());
});
}
}
}
}
}

View File

@@ -0,0 +1,535 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Core;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\iTopDataTestCase;
use DateInterval;
use DateTime;
use Expression;
use FunctionExpression;
use MetaModel;
use ScalarExpression;
class ExpressionEvaluateTest extends iTopDataTestCase
{
const USE_TRANSACTION = false;
/**
* @covers Expression::GetParameters()
* @dataProvider GetParametersProvider
*
* @param $sExpression
* @param $sParentFilter
* @param $aExpectedParameters
*
* @throws \OQLException
*/
public function testGetParameters($sExpression, $sParentFilter, $aExpectedParameters)
{
$oExpression = Expression::FromOQL($sExpression);
$aParameters = $oExpression->GetParameters($sParentFilter);
sort($aExpectedParameters);
sort($aParameters);
static::assertEquals($aExpectedParameters, $aParameters);
}
public function GetParametersProvider()
{
return array(
array('1 AND 0 OR :hello + :world', null, array('hello', 'world')),
array('1 AND 0 OR :hello + :world', 'this', array()),
array(':this->left + :this->right', null, array('this->left', 'this->right')),
array(':this->left + :this->right', 'this', array('left', 'right')),
array(':this->left + :this->right', 'that', array()),
array(':this_left + :this_right', 'this', array()),
);
}
/**
* 100x quicker to execute than testExpressionEvaluate
*
* @covers Expression::Evaluate()
* @covers Expression::FromOQL()
* @relies-on-dataProvider VariousExpressions
* @throws \OQLException
*/
public function _testExpressionEvaluateAllAtOnce()
{
$aTestCases = $this->VariousExpressionsProvider();
foreach ($aTestCases as $sCaseId => $aTestArgs)
{
$this->debug("Case $sCaseId:");
$this->testVariousExpressions($aTestArgs[0], $aTestArgs[1]);
}
}
/**
* @covers Expression::Evaluate()
* @covers Expression::FromOQL()
* @dataProvider VariousExpressionsProvider
*
* @param string $sExpression
* @param string $expectedValue
*
* @throws \OQLException
* @throws \Exception
*/
public function testVariousExpressions($sExpression, $expectedValue)
{
$oExpression = Expression::FromOQL($sExpression);
$value = $oExpression->Evaluate(array());
static::assertEquals($expectedValue, $value);
}
public function VariousExpressionsProvider()
{
if (false)
{
$aExpressions = array(
// Test case to isolate for troubleshooting purposes
array('1+1', 2),
);
}
else
{
$aExpressions = array(
// The bare minimum
array('"blah"', 'blah'),
array('"\\\\"', '\\'),
// Arithmetics
array('2+2', 4),
array('2+2-2', 2),
array('2*(3+4)', 14),
array('(2*3)+4', 10),
array('2*3+4', 10),
// Strings
array("CONCAT('hello', 'world')", 'helloworld'),
// Not yet parsed - array("CONCAT_WS(' ', 'hello', 'world')", 'hello world'),
array("SUBSTR('abcdef', 2, 3)", 'bcd'),
array("TRIM(' Sin dolor ')", 'Sin dolor'),
// Comparison operators
array('1 = 1', 1),
array('1 != 1', 0),
array('0 = 1', 0),
array('0 != 1', 1),
array('2 > 1', 1),
array('2 < 1', 0),
array('1 > 2', 0),
array('2 > 1', 1),
array('2 >= 1', 1),
array('2 >= 2', 1),
array("'the quick brown dog' LIKE '%QUICK%'", 1),
array("'the quick brown dog' LIKE '%SLOW%'", 0),
array("'the quick brown dog' LIKE '%QU_CK%'", 1),
array("'the quick brown dog' LIKE '%QU_ICK%'", 0),
array('"400 (km/h)" LIKE "400%"', 1),
array('"400 (km/h)" LIKE "100%"', 0),
array('"2020-06-12" > "2020-06-11"', 1),
array('"2020-06-12" < "2020-06-11"', 0),
array('" 2020-06-12" > "2020-06-11"', 0), // Leading spaces => a string
array('" 2020-06-12 " > "2020-06-11"', 0), // Trailing spaces => a string
array('"2020-06-12 17:35:13" > "2020-06-12 17:35:12"', 1),
array('"2020-06-12 17:35:13" < "2020-06-12 17:35:12"', 0),
array('"2020-06-12 17:35:13" > "2020-06-12"', 1),
array('"2020-06-12 17:35:13" < "2020-06-12"', 0),
array('"2020-06-12 00:00:00" = "2020-06-12"', 0),
// Logical operators
array('0 AND 0', 0),
array('1 AND 0', 0),
array('0 AND 1', 0),
array('1 AND 1', 1),
array('0 OR 0', 0),
array('0 OR 1', 1),
array('1 OR 0', 1),
array('1 OR 1', 1),
array('1 AND 0 OR 1', 1),
// Casting
array('1 AND "blah"', 0),
array('1 AND "1"', 1),
array('1 AND "2"', 1),
array('1 AND "0"', 0),
array('1 AND "-1"', 1),
// Null
array('NULL', null),
array('1 AND NULL', null),
array('CONCAT("Great but...", NULL)', null),
array('COALESCE(NULL, 123)', 123),
array('COALESCE(321, 123)', 321),
array('ISNULL(NULL)', 1),
array('ISNULL(123)', 0),
// Date functions
array("DATE('2020-03-12 13:18:30')", '2020-03-12'),
array("DATE_FORMAT('2009-10-04 22:23:00', '%Y %m %d %H %i %s')", '2009 10 04 22 23 00'),
array("DATE(NOW()) = CURRENT_DATE()", 1), // Could fail if executed around midnight!
array("TO_DAYS('2020-01-02')", 737791),
array("FROM_DAYS(737791)", '2020-01-02'),
array("YEAR('2020-05-03')", 2020),
array("MONTH('2020-05-03')", 5),
array("DAY('2020-05-03')", 3),
array("DATE_ADD('2020-02-28 18:00:00', INTERVAL 1 HOUR)", '2020-02-28 19:00:00'),
array("DATE_ADD('2020-02-28 18:00:00', INTERVAL 1 DAY)", '2020-02-29 18:00:00'),
array("DATE_SUB('2020-03-01 18:00:00', INTERVAL 1 HOUR)", '2020-03-01 17:00:00'),
array("DATE_SUB('2020-03-01 18:00:00', INTERVAL 1 DAY)", '2020-02-29 18:00:00'),
// Misc. functions
array('IF(1, 123, 567)', 123),
array('IF(0, 123, 567)', 567),
array('ELT(3, "a", "b", "c")', 'c'),
array('ELT(0, "a", "b", "c")', null),
array('ELT(4, "a", "b", "c")', null),
array('INET_ATON("128.0.0.1")', 2147483649),
array('INET_NTOA(2147483649)', '128.0.0.1'),
);
}
// Build a comprehensive index
$aRet = array();
foreach ($aExpressions as $aExp)
{
$aRet[$aExp[0]] = $aExp;
}
return $aRet;
}
/**
* @covers Expression::Evaluate()
* @dataProvider NotYetParsableExpressionsProvider
*
* @param string $sExpression
* @param string $expectedValue
*/
public function testNotYetParsableExpressions($sExpression, $expectedValue)
{
$sNewExpression = "return $sExpression;";
$oExpression = eval($sNewExpression);
$res = $oExpression->Evaluate(array());
static::assertEquals($expectedValue, $res);
}
public function NotYetParsableExpressionsProvider()
{
$aExpressions = array(
array("new \\FunctionExpression('CONCAT_WS', array(new \\ScalarExpression(' '), new \\ScalarExpression('Hello'), new \ScalarExpression('world!')))", 'Hello world!'),
array("new \\ScalarExpression('windows\\system32')", 'windows\\system32'),
array("new \\BinaryExpression(new \\ScalarExpression('100%'), 'LIKE', new \\ScalarExpression('___\%'))", 1),
array("new \\BinaryExpression(new \ScalarExpression('1000'), 'LIKE', new \ScalarExpression('___\%'))", 0),
// Net yet parsed - array("TIME(NOW()) = CURRENT_TIME()", 1), // Not relevant
// Not yet parsed - array("DATE_ADD('2020-02-28 18:00:00', INTERVAL 1 WEEK)", '2020-03-06 18:00:00'),
// Not yet parsed - array("DATE_SUB('2020-03-01 18:00:00', INTERVAL 1 WEEK)", '2020-02-23 18:00:00'),
// Not yet parsed - array('ROUND(1.2345, 2)', 1.23),
// Not yet parsed - array('FLOOR(1.2)', 1),
);
// Build a comprehensive index
$aRet = array();
foreach ($aExpressions as $aExp)
{
$aRet[$aExp[0]] = $aExp;
}
return $aRet;
}
/**
* Check that the test data would give the same result when evaluated by MySQL
* It uses the data provider ExpressionProvider, and checks every test case in one single query
*
* @throws \MySQLException
*/
public function testMySQLEvaluateAllAtOnce()
{
// Expressions given as an OQL
$aTests = array_values($this->VariousExpressionsProvider());
// Expressions given as a PHP statement
foreach (array_values($this->NotYetParsableExpressionsProvider()) as $i => $aTest)
{
$sNewExpression = "return {$aTest[0]};";
$oExpression = eval($sNewExpression);
$sExpression = $oExpression->RenderExpression(true);
$aTests[] = array($sExpression, $aTest[1]);
}
$aExpressions = array();
foreach ($aTests as $i => $aTest)
{
$aExpressions[] = "{$aTest[0]} as test_$i";
}
$sSelects = implode(', ', $aExpressions);
$sQuery = "SELECT $sSelects";
$this->debug($sQuery);
$aResults = CMDBSource::QueryToArray($sQuery);
foreach ($aTests as $i => $aTest)
{
$value = $aResults[0]["test_$i"];
$expectedValue = $aTest[1];
$this->debug("Test #$i: {$aTests[$i][0]} => ".var_export($value, true));
static::assertEquals($expectedValue, $value);
}
}
/**
* @covers DBObject::EvaluateExpression
* @dataProvider ExpressionsWithObjectFieldsProvider
*
* @param $sClass
* @param $aValues
* @param $sExpression
* @param $expected
*
* @throws \CoreException
* @throws \OQLException
*/
public function testExpressionsWithObjectFields($sClass, $aValues, $sExpression, $expected)
{
$oObject = MetaModel::NewObject($sClass, $aValues);
$oExpression = Expression::FromOQL($sExpression);
$res = $oObject->EvaluateExpression($oExpression);
static::assertEquals($expected, $res);
}
public function ExpressionsWithObjectFieldsProvider()
{
return array(
array('Location', array('name' => 'Grenoble', 'org_id' => 2), 'org_id', 2),
array('Location', array('name' => 'Grenoble', 'org_id' => 2), 'CONCAT(SUBSTR(name, 4), " cause")', 'noble cause'),
);
}
/**
* @dataProvider ExpressionWithParametersProvider
*
* @param $sExpression
* @param $aParameters
* @param $expected
*
* @throws \OQLException
* @throws \Exception
*/
public function testExpressionWithParameters($sExpression, $aParameters, $expected)
{
$oExpression = Expression::FromOQL($sExpression);
$res = $oExpression->Evaluate($aParameters);
static::assertEquals($expected, $res);
}
public function ExpressionWithParametersProvider()
{
return array(
array('CONCAT(SUBSTR(name, 4), " cause")', array('name' => 'noble'), 'le cause'),
);
}
/**
* Check Expression::IfTrue
*
* @covers Expression::FromOQL
* @covers Expression::IsTrue
* @dataProvider TrueExpressionsProvider
*
* @param $sExpression
* @param $bExpectTrue
*
* @throws \OQLException
*/
public function testTrueExpressions($sExpression, $bExpectTrue)
{
$oExpression = Expression::FromOQL($sExpression);
$res = $oExpression->IsTrue();
if ($bExpectTrue)
{
static::assertTrue($res, 'arg: '.$sExpression);
}
else
{
static::assertFalse($res, 'arg: '.$sExpression);
}
}
public function TrueExpressionsProvider()
{
$aExpressions = array(
array('1', true),
array('0 OR 0', false),
array('1 AND 1', true),
array('1 AND (1 OR 0)', true)
);
// Build a comprehensive index
$aRet = array();
foreach ($aExpressions as $aExp)
{
$aRet[$aExp[0]] = $aExp;
}
return $aRet;
}
/**
* @covers FunctionExpression::Evaluate()
* @dataProvider TimeFormatsProvider
*
* @param $sFormat
* @param $bProcessed
* @param $sValueOrException
*
* @throws \CoreException
* @throws \MySQLException
* @throws \MySQLQueryHasNoResultException
* @throws \Exception
*/
public function testTimeFormat($sFormat, $bProcessed, $sValueOrException)
{
$sDate = '2009-06-04 21:23:24';
$oExpression = new FunctionExpression('DATE_FORMAT', array(new ScalarExpression($sDate), new ScalarExpression("%$sFormat")));
if ($bProcessed)
{
$sqlValue = CMDBSource::QueryToScalar("SELECT DATE_FORMAT('$sDate', '%$sFormat')");
static::assertEquals($sqlValue, $sValueOrException, 'Check test against MySQL');
$res = $oExpression->Evaluate(array());
static::assertEquals($sValueOrException, $res, 'Check evaluation');
}
else
{
static::expectException($sValueOrException);
$oExpression->Evaluate(array());
}
}
public function TimeFormatsProvider()
{
$aTests = array(
array('a', true, 'Thu'),
array('b', true, 'Jun'),
array('c', true, '6'),
array('D', true, '4th'),
array('d', true, '04'),
array('e', true, '4'),
array('f', false, 'NotYetEvaluatedExpression'), // microseconds: no way!
array('H', true, '21'),
array('h', true, '09'),
array('I', true, '09'),
array('i', true, '23'),
array('j', true, '155'), // day of the year
array('k', true, '21'),
array('l', true, '9'),
array('M', true, 'June'),
array('m', true, '06'),
array('p', true, 'PM'),
array('r', true, '09:23:24 PM'),
array('S', true, '24'),
array('s', true, '24'),
array('T', true, '21:23:24'),
array('U', false, 'NotYetEvaluatedExpression'), // Week sunday based (mode 0)
array('u', false, 'NotYetEvaluatedExpression'), // Week monday based (mode 1)
array('V', false, 'NotYetEvaluatedExpression'), // Week sunday based (mode 2)
array('v', true, '23'), // Week monday based (mode 3 - ISO-8601)
array('W', true, 'Thursday'),
array('w', true, '4'),
array('X', false, 'NotYetEvaluatedExpression'),
array('x', true, '2009'), // to be used with %v (ISO - 8601)
array('Y', true, '2009'),
array('y', true, '09'),
);
$aRes = array();
foreach ($aTests as $aTest)
{
$aRes["Format %{$aTest[0]}"] = $aTest;
}
return $aRes;
}
/**
* Systematically check all supported format specs, for a given date
*
* @covers FunctionExpression::Evaluate()
* @dataProvider EveryTimeFormatProvider
*
* @param $sDate
*
* @throws \CoreException
* @throws \MySQLException
* @throws \Exception
*/
public function testEveryTimeFormat($sDate)
{
$aFormats = $this->TimeFormatsProvider();
$aSelects = array();
foreach ($aFormats as $sFormatDesc => $aFormatSpec)
{
$sFormat = $aFormatSpec[0];
$bProcessed = $aFormatSpec[1];
if ($bProcessed)
{
$aSelects["%$sFormat"] = "DATE_FORMAT('$sDate', '%$sFormat') AS `$sFormat`";
}
}
$sSelects = "SELECT ".implode(', ', $aSelects);
$aRes = CMDBSource::QueryToArray($sSelects);
$aRow = $aRes[0];
foreach ($aFormats as $sFormatDesc => $aFormatSpec)
{
$sFormat = $aFormatSpec[0];
$bProcessed = $aFormatSpec[1];
if ($bProcessed)
{
$oExpression = new FunctionExpression('DATE_FORMAT', array(new ScalarExpression($sDate), new ScalarExpression("%$sFormat")));
$res = $oExpression->Evaluate(array());
static::assertEquals($aRow[$sFormat], $res, "Format %$sFormat not matching MySQL for '$sDate'");
}
}
}
public function EveryTimeFormatProvider()
{
return array(
array('1971-07-19 8:40:00'),
array('1999-12-31 23:59:59'),
array('2000-01-01 00:00:00'),
array('2009-06-04 21:23:24'),
array('2020-02-29 23:59:59'),
array('2030-10-21 23:59:59'),
array('2050-12-21 23:59:59'),
);
}
/**
* Systematically check all supported format specs, for a range of dates
*
* @covers FunctionExpression::Evaluate()
* @dataProvider EveryTimeFormatOnDateRangeProvider
*
* @param $sStartDate
* @param $sInterval
* @param $iRepeat
*
* @throws \CoreException
* @throws \MySQLException
* @throws \Exception
*/
public function testEveryTimeFormatOnDateRange($sStartDate, $sInterval, $iRepeat)
{
$oDate = new DateTime($sStartDate);
for ($i = 0 ; $i < $iRepeat ; $i++)
{
$sDate = date_format($oDate, 'Y-m-d, H:i:s');
$this->debug("Checking '$sDate'");
$this->testEveryTimeFormat($sDate);
$oDate->add(new DateInterval($sInterval));
}
}
public function EveryTimeFormatOnDateRangeProvider()
{
return array(
'10 years, day by day' => array('2000-01-01', 'P1D', 365 * 10),
'1 day, hour by hour' => array('2000-01-01 00:01:02', 'PT1H', 24),
'1 hour, minute by minute' => array('2000-01-01 00:01:02', 'PT1M', 60),
'1 minute, second by second' => array('2000-01-01 00:01:02', 'PT1S', 60),
);
}
}

View File

@@ -83,4 +83,100 @@ class MetaModelTest extends ItopDataTestCase
),
);
}
/**
* @covers MetaModel::GetDependentAttributes()
* @dataProvider GetDependentAttributesProvider
*
* @param string $sClass
* @param string $sAttCode
* @param array $aExpectedAttCodes
*
* @throws \Exception
*/
public function testGetDependentAttributes($sClass, $sAttCode, array $aExpectedAttCodes)
{
$aRes = MetaModel::GetDependentAttributes($sClass, $sAttCode);
// The order doesn't matter
sort($aRes);
sort($aExpectedAttCodes);
static::assertEquals($aExpectedAttCodes, $aRes);
}
public function GetDependentAttributesProvider()
{
$aRawCases = array(
array('Person', 'org_id', array('location_id', 'org_name', 'org_id_friendlyname', 'org_id_obsolescence_flag')),
array('Person', 'name', array('friendlyname')),
array('Person', 'status', array('obsolescence_flag')),
);
$aRet = array();
foreach ($aRawCases as $i => $aData)
{
$aRet[$aData[0].'::'.$aData[1]] = $aData;
}
return $aRet;
}
/**
* @covers MetaModel::GetPrerequisiteAttributes()
* @dataProvider GetPrerequisiteAttributesProvider
*
* @param string $sClass
* @param string $sAttCode
* @param array $aExpectedAttCodes
*
* @throws \Exception
*/
public function testGetPrerequisiteAttributes($sClass, $sAttCode, array $aExpectedAttCodes)
{
$aRes = MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode);
// The order doesn't matter
sort($aRes);
sort($aExpectedAttCodes);
static::assertEquals($aRes, $aExpectedAttCodes);
}
public function GetPrerequisiteAttributesProvider()
{
$aRawCases = array(
array('Person', 'friendlyname', array('name', 'first_name')),
array('Person', 'obsolescence_flag', array('status')),
array('Person', 'org_id_friendlyname', array('org_id')),
array('Person', 'org_id', array()),
array('Person', 'org_name', array('org_id')),
);
$aRet = array();
foreach ($aRawCases as $i => $aData)
{
$aRet[$aData[0].'::'.$aData[1]] = $aData;
}
return $aRet;
}
/**
* To be removed as soon as the dependencies on external fields are obsoleted
* @Group Integration
*/
public function testManualVersusAutomaticDependenciesOnExtKeys()
{
foreach (\MetaModel::GetClasses() as $sClass)
{
if (\MetaModel::IsAbstract($sClass)) continue;
foreach (\MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
if (\MetaModel::GetAttributeOrigin($sClass, $sAttCode) != $sClass) continue;
if (!$oAttDef instanceof \AttributeExternalKey) continue;
$aManual = $oAttDef->Get('depends_on');
$aAuto = \MetaModel::GetPrerequisiteAttributes($sClass, $sAttCode);
// The order doesn't matter
sort($aAuto);
sort($aManual);
static::assertEquals($aManual, $aAuto, "Class: $sClass, Attribute: $sAttCode");
}
}
}
}