/* ** ZABBIX ** Copyright (C) 2000-2005 SIA Zabbix ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 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 General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. **/ #include #include #include #include #include "expression.h" #include "functions.h" #include "evalfunc.h" #include "common.h" #include "db.h" #include "log.h" #include "zlog.h" /****************************************************************************** * * * Function: str2double * * * * Purpose: convert string to double * * * * Parameters: str - string to convert * * * * Return value: converted double value * * * * Author: Alexei Vladishev * * * * Comments: the function automatically processes prefixes 'K','M','G' * * * ******************************************************************************/ double str2double(char *str) { if(str[strlen(str)-1] == 'K') { str[strlen(str)-1] = 0; return (double)1024*atof(str); } else if(str[strlen(str)-1] == 'M') { str[strlen(str)-1] = 0; return (double)1024*1024*atof(str); } else if(str[strlen(str)-1] == 'G') { str[strlen(str)-1] = 0; return (double)1024*1024*1024*atof(str); } return atof(str); } /****************************************************************************** * * * Function: delete_spaces * * * * Purpose: delete all spaces * * * * Parameters: c - string to delete spaces * * * * Return value: the string wtihout spaces * * * * Author: Alexei Vladishev * * * * Comments: * * * ******************************************************************************/ void delete_spaces(char *c) { int i,j; zabbix_log( LOG_LEVEL_DEBUG, "Before deleting spaces:%s", c ); j=0; for(i=0;c[i]!=0;i++) { if( c[i] != ' ') { c[j]=c[i]; j++; } } c[j]=0; zabbix_log(LOG_LEVEL_DEBUG, "After deleting spaces:%s", c ); } /****************************************************************************** * * * Function: evaluate_simple * * * * Purpose: evaluate simple expression * * * * Parameters: exp - expression string * * * * Return value: SUCCEED - evaluated succesfully, result - value of the exp * * FAIL - otherwise * * * * Author: Alexei Vladishev * * * * Comments: format: or * * * * It is recursive function! * * * ******************************************************************************/ int evaluate_simple (double *result,char *exp,char *error,int maxerrlen) { double value1,value2; char first[MAX_STRING_LEN],second[MAX_STRING_LEN]; char *p; zabbix_log( LOG_LEVEL_DEBUG, "Evaluating simple expression [%s]", exp ); /* Remove left and right spaces */ lrtrim_spaces(exp); if( is_double_prefix(exp) == SUCCEED ) { /* *result=atof(exp);*/ /* str2double support prefixes */ *result=str2double(exp); return SUCCEED; } if( (p = strstr(exp,"|")) != NULL ) { zabbix_log( LOG_LEVEL_DEBUG, "| is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); /* l=find_char(exp,'|'); strscpy( first, exp ); first[l]=0; j=0; for(i=l+1;exp[i]!=0;i++) { second[j]=exp[i]; j++; } second[j]=0;*/ if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( value1 == 1) { *result=value1; return SUCCEED; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( value2 == 1) { *result=value2; return SUCCEED; } *result=0; return SUCCEED; } if( (p = strstr(exp,"&")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "& is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); zabbix_log(LOG_LEVEL_DEBUG, "[%s] [%s]",first,second ); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( (value1 == 1) && (value2 == 1) ) { *result=1; } else { *result=0; } return SUCCEED; } if( (p = strstr(exp,">")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "> is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( value1 > value2 ) { *result=1; } else { *result=0; } return SUCCEED; } if( (p = strstr(exp,"<")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "< is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); zabbix_log(LOG_LEVEL_DEBUG, "[%s] [%s]",first,second ); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( value1 < value2 ) { *result=1; } else { *result=0; } zabbix_log(LOG_LEVEL_DEBUG, "Result [%f]",*result ); return SUCCEED; } if( (p = strstr(exp,"*")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "* is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } *result=value1*value2; return SUCCEED; } if( (p = strstr(exp,"/")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "/ is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if(cmp_double(value2,0) == 0) { zbx_snprintf(error,maxerrlen,"Division by zero. Cannot evaluate expression [%s/%s]", first,second); zabbix_log(LOG_LEVEL_WARNING, error); zabbix_syslog(error); return FAIL; } else { *result=value1/value2; } return SUCCEED; } if( (p = strstr(exp,"+")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "+ is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } *result=value1+value2; return SUCCEED; } if( (p = strstr(exp,"-")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "- is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } *result=value1-value2; return SUCCEED; } if( (p = strstr(exp,"=")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "= is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( cmp_double(value1,value2) ==0 ) { *result=1; } else { *result=0; } return SUCCEED; } if( (p = strstr(exp,"#")) != NULL ) { zabbix_log(LOG_LEVEL_DEBUG, "# is found" ); *p=0; strscpy( first, exp); *p='|'; p++; strscpy( second, p); if( evaluate_simple(&value1,first,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( evaluate_simple(&value2,second,error,maxerrlen) == FAIL ) { zabbix_log(LOG_LEVEL_DEBUG, error); zabbix_syslog(error); return FAIL; } if( cmp_double(value1,value2) != 0 ) { *result=1; } else { *result=0; } return SUCCEED; } else { zbx_snprintf(error,maxerrlen,"Format error or unsupported operator. Exp: [%s]", exp); zabbix_log(LOG_LEVEL_WARNING, error); zabbix_syslog(error); return FAIL; } return SUCCEED; } /****************************************************************************** * * * Function: evaluate * * * * Purpose: evaluate simplified expression * * * * Parameters: exp - expression string * * * * Return value: SUCCEED - evaluated succesfully, result - value of the exp * * FAIL - otherwise * * * * Author: Alexei Vladishev * * * * Comments: example: ({15}>10)|({123}=1) * * * ******************************************************************************/ int evaluate(int *result,char *exp, char *error, int maxerrlen) { double value; char res[MAX_STRING_LEN]; char simple[MAX_STRING_LEN]; int i,l,r; zabbix_log(LOG_LEVEL_DEBUG, "In evaluate([%s])",exp); strscpy( res,exp ); while( find_char( exp, ')' ) != FAIL ) { l=-1; r=find_char(exp,')'); for(i=r;i>=0;i--) { if( exp[i] == '(' ) { l=i; break; } } if( r == -1 ) { zbx_snprintf(error, maxerrlen, "Cannot find left bracket [(]. Expression:[%s]", exp); zabbix_log(LOG_LEVEL_WARNING, error); zabbix_syslog(error); return FAIL; } for(i=l+1;i 0) { pr[0] = '\0'; strncat(str_out, pl, outlen); outlen -= MIN(strlen(pl), outlen); pr[0] = '{'; zbx_snprintf(replace_to, sizeof(replace_to), "{"); var_len = 1; if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_NAME, strlen(MVAR_TRIGGER_NAME)) == 0) { var_len = strlen(MVAR_TRIGGER_NAME); zbx_snprintf(replace_to, sizeof(replace_to), "%s", trigger->description); substitute_simple_macros(trigger, action, replace_to, sizeof(replace_to), MACRO_TYPE_TRIGGER_DESCRIPTION); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_COMMENT, strlen(MVAR_TRIGGER_COMMENT)) == 0) { var_len = strlen(MVAR_TRIGGER_COMMENT); zbx_snprintf(replace_to, sizeof(replace_to), "%s", trigger->comments); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY | MACRO_TYPE_TRIGGER_DESCRIPTION) && strncmp(pr, MVAR_HOST_NAME, strlen(MVAR_HOST_NAME)) == 0) { var_len = strlen(MVAR_HOST_NAME); result = DBselect("select distinct h.host from triggers t, functions f,items i, hosts h " "where t.triggerid=%d and f.triggerid=t.triggerid and f.itemid=i.itemid and h.hostid=i.hostid", trigger->triggerid); row = DBfetch(result); if(!row || DBis_null(row[0])==SUCCEED) { zabbix_log( LOG_LEVEL_ERR, "No hostname in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); zabbix_syslog("No hostname in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); zbx_snprintf(replace_to, sizeof(replace_to), "%s", STR_UNKNOWN_VARIAVLE); } else { zbx_snprintf(replace_to, sizeof(replace_to), "%s", row[0]); } DBfree_result(result); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_KEY, strlen(MVAR_TRIGGER_KEY)) == 0) { var_len = strlen(MVAR_TRIGGER_KEY); result = DBselect("select distinct i.key_ from triggers t, functions f,items i, hosts h" " where t.triggerid=%d and f.triggerid=t.triggerid and f.itemid=i.itemid and h.hostid=i.hostid" " order by i.key_", trigger->triggerid); row=DBfetch(result); if(!row || DBis_null(row[0])==SUCCEED) { zabbix_log( LOG_LEVEL_ERR, "No TRIGGER.KEY in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); zabbix_syslog("No TRIGGER.KEY in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); /* remove variable */ *replace_to = '\0'; } else { zbx_snprintf(replace_to, sizeof(replace_to), "%s", row[0]); } DBfree_result(result); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_IPADDRESS, strlen(MVAR_IPADDRESS)) == 0) { var_len = strlen(MVAR_IPADDRESS); result = DBselect("select distinct h.ip from triggers t, functions f,items i, hosts h" " where t.triggerid=%d and f.triggerid=t.triggerid and f.itemid=i.itemid and h.hostid=i.hostid and h.useip=1", trigger->triggerid); row = DBfetch(result); if(!row || DBis_null(row[0])==SUCCEED) { zabbix_log( LOG_LEVEL_ERR, "No hostname in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); zabbix_syslog("No hostname in substitute_simple_macros. Triggerid [%d]", trigger->triggerid); zbx_snprintf(replace_to, sizeof(replace_to), "%s", STR_UNKNOWN_VARIAVLE); } else { zbx_snprintf(replace_to, sizeof(replace_to), "%s", row[0]); } DBfree_result(result); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_DATE, strlen(MVAR_DATE)) == 0) { var_len = strlen(MVAR_TIME); now = time(NULL); tm = localtime(&now); zbx_snprintf(replace_to, sizeof(replace_to)-1, "%.4d.%.2d.%.2d", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY)&& strncmp(pr, MVAR_TIME, strlen(MVAR_TIME)) == 0) { var_len = strlen(MVAR_TIME); now = time(NULL); tm = localtime(&now); zbx_snprintf(replace_to, sizeof(replace_to), "%.2d:%.2d:%.2d",tm->tm_hour,tm->tm_min,tm->tm_sec); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_STATUS, strlen(MVAR_TRIGGER_STATUS)) == 0) { /* NOTE: if you make changes for this bloc, don't forgot MVAR_TRIGGER_STATUS_OLD block */ var_len = strlen(MVAR_TRIGGER_STATUS); if(trigger->value == TRIGGER_VALUE_TRUE) zbx_snprintf(replace_to, sizeof(replace_to), "OFF"); else zbx_snprintf(replace_to, sizeof(replace_to), "ON"); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_STATUS_OLD, strlen(MVAR_TRIGGER_STATUS_OLD)) == 0) { /* NOTE: if you make changes for this bloc, don't forgot MVAR_TRIGGER_STATUS block */ var_len = strlen(MVAR_TRIGGER_STATUS_OLD); if(trigger->value == TRIGGER_VALUE_TRUE) zbx_snprintf(replace_to, sizeof(replace_to), "OFF"); else zbx_snprintf(replace_to, sizeof(replace_to), "ON"); } else if(macro_type & (MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY) && strncmp(pr, MVAR_TRIGGER_SEVERITY, strlen(MVAR_TRIGGER_SEVERITY)) == 0) { var_len = strlen(MVAR_TRIGGER_SEVERITY); if(trigger->priority == 0) zbx_snprintf(replace_to, sizeof(replace_to), "Not classified"); else if(trigger->priority == 1) zbx_snprintf(replace_to, sizeof(replace_to), "Information"); else if(trigger->priority == 2) zbx_snprintf(replace_to, sizeof(replace_to), "Warning"); else if(trigger->priority == 3) zbx_snprintf(replace_to, sizeof(replace_to), "Average"); else if(trigger->priority == 4) zbx_snprintf(replace_to, sizeof(replace_to), "High"); else if(trigger->priority == 5) zbx_snprintf(replace_to, sizeof(replace_to), "Disaster"); else zbx_snprintf(replace_to, sizeof(replace_to), "Unknown"); } strncat(str_out, replace_to, outlen); outlen -= MIN(strlen(replace_to), outlen); pl = pr + var_len; } strncat(str_out, pl, outlen); outlen -= MIN(strlen(pl), outlen); zbx_snprintf(data, dala_max_len, "%s", str_out); zabbix_log( LOG_LEVEL_DEBUG, "Result expression [%s]", data ); } /****************************************************************************** * * * Function: substitute_macros * * * * Purpose: substitute macros in data string with real values * * * * Parameters: trigger - trigger structure * * action - action structure * * dala_max_len - max length of data string,include '\0' * * data - data string * * * * Return value: * * * * Author: Eugene Grigorjev * * * * Comments: example: "{127.0.0.1:system[procload].last(0)}" to "1.34" * * * ******************************************************************************/ void substitute_macros(DB_TRIGGER *trigger, DB_ACTION *action, char *data, int dala_max_len) { char str_out[MAX_STRING_LEN], replace_to[MAX_STRING_LEN], *pl = NULL, *pr = NULL, *pms = NULL, *pme = NULL, *p = NULL; char host[MAX_STRING_LEN], key[MAX_STRING_LEN], function[MAX_STRING_LEN], parameter[MAX_STRING_LEN]; int outlen, var_len; zabbix_log(LOG_LEVEL_DEBUG, "In substitute_macros([%s])",data); substitute_simple_macros(trigger, action, data, dala_max_len, MACRO_TYPE_MESSAGE_SUBJECT | MACRO_TYPE_MESSAGE_BODY); *str_out = '\0'; outlen = sizeof(str_out) - 1; pl = data; while((pr = strchr(pl, '{')) && outlen > 0) { if((pme = strchr(pr, '}')) == NULL) break; pme[0] = '\0'; pr = strrchr(pr, '{'); /* find '{' near '}' */ /* copy left side */ pr[0] = '\0'; strncat(str_out, pl, outlen); outlen -= MIN(strlen(pl), outlen); pr[0] = '{'; /* copy original name of variable */ zbx_snprintf(replace_to, sizeof(replace_to), "%s}", pr); /* in format used '}' */ /* cose in 'pr' string symbol '}' is changed to '\0' by 'pme'*/ var_len = strlen(replace_to); pms = pr + 1; if(NULL != (p = strchr(pms, ':'))) { *p = '\0'; zbx_snprintf(host, sizeof(host), "%s", pms); *p = ':'; pms = p + 1; if(NULL != (p = strrchr(pms, '.'))) { *p = '\0'; zbx_snprintf(key, sizeof(key), "%s", pms); *p = '.'; pms = p + 1; if(NULL != (p = strchr(pms, '('))) { *p = '\0'; zbx_snprintf(function, sizeof(function), "%s", pms); *p = '('; pms = p + 1; if(NULL != (p = strchr(pms, ')'))) { *p = '\0'; zbx_snprintf(parameter, sizeof(parameter), "%s", pms); *p = ')'; pms = p + 1; if(evaluate_FUNCTION2(replace_to,host,key,function,parameter) != SUCCEED) zbx_snprintf(replace_to, sizeof(replace_to), "%s", STR_UNKNOWN_VARIAVLE); } } } } pme[0] = '}'; strncat(str_out, replace_to, outlen); outlen -= MIN(strlen(replace_to), outlen); pl = pr + var_len; } strncat(str_out, pl, outlen); outlen -= MIN(strlen(pl), outlen); zbx_snprintf(data, dala_max_len, "%s", str_out); zabbix_log( LOG_LEVEL_DEBUG, "Result expression:%s", data ); } /****************************************************************************** * * * Function: substitute_functions * * * * Purpose: substitute expression functions with theirs values * * * * Parameters: exp - expression string * * error - place error message here if any * * maxerrlen - max length of error msg * * * * Return value: SUCCEED - evaluated succesfully, exp - updated expression * * FAIL - otherwise * * * * Author: Alexei Vladishev * * * * Comments: example: "({15}>10)|({123}=0)" => "(6.456>10)|(0=0) * * * ******************************************************************************/ int substitute_functions(char *exp, char *error, int maxerrlen) { double value; char functionid[MAX_STRING_LEN]; char res[MAX_STRING_LEN]; int i,l,r; zabbix_log(LOG_LEVEL_DEBUG, "BEGIN substitute_functions (%s)", exp); while( find_char(exp,'{') != FAIL ) { l=find_char(exp,'{'); r=find_char(exp,'}'); if( r == FAIL ) { zbx_snprintf(error,maxerrlen,"Cannot find right bracket. Expression:[%s]", exp); zabbix_log( LOG_LEVEL_WARNING, error); zabbix_syslog(error); return FAIL; } if( r < l ) { zbx_snprintf(error,maxerrlen, "Right bracket is before left one. Expression:[%s]", exp); zabbix_log( LOG_LEVEL_WARNING, error); zabbix_syslog(error); return FAIL; } for(i=l+1;i1)| * * ({a0:system[procload].max(300)}>3) * * * ******************************************************************************/ int evaluate_expression(int *result,char *expression, char *error, int maxerrlen) { zabbix_log(LOG_LEVEL_DEBUG, "In evaluate_expression(%s)", expression ); delete_spaces(expression); if( substitute_functions(expression, error, maxerrlen) == SUCCEED) { if( evaluate(result, expression, error, maxerrlen) == SUCCEED) { return SUCCEED; } } zabbix_log(LOG_LEVEL_WARNING, "Evaluation of expression [%s] failed [%s]", expression, error ); zabbix_syslog("Evaluation of expression [%s] failed [%s]", expression, error ); return FAIL; }