diff --git a/tests/math_expression.test b/tests/math_expression.test index 5bda4d6..0190afe 100644 --- a/tests/math_expression.test +++ b/tests/math_expression.test @@ -20,142 +20,302 @@ class CtoolsMathExpressionTestCase extends DrupalWebTestCase { /** * {@inheritDoc} */ - function setUp(array $modules = array()) { + public function setUp(array $modules = array()) { $modules[] = 'ctools'; $modules[] = 'ctools_plugin_test'; parent::setUp($modules); } /** - * Returns a random double between 0 and 1. + * Return the sign of the numeric arg $n as an integer -1, 0, 1. + * + * Note: Not defined when $n is Infinity or NaN (or NULL or ...)! + * + * @param int|float $n + * The number to test. + * @return integer + * -1 if the $n is negative, 0 if $n is zero or 1 if $n is positive. + * + * @see gmp_sign() + */ + protected static function sign($n) { + return ($n > 0) - ($n < 0); + } + + /** + * Returns a random number between 0 and 1. + * + * @return float + * A random number between 0 and 1 inclusive. */ protected function rand01() { - return rand(0, PHP_INT_MAX) / PHP_INT_MAX; + return mt_rand(0, PHP_INT_MAX) / PHP_INT_MAX; } /** * A custom assertion with checks the values in a certain range. + * + * @param float $first + * A value to check for equality. + * @param float $second + * A value to check for equality. + * @param string $message + * The message describing the correct behaviour, eg. "2/4 equals 1/2". The + * default message is used if this value is empty. + * @param float $delta + * The precision with which values must match. This accounts for rounding + * errors and imprecise representation errors in the floating point format. + * The value passed in should ideally be proportional to the values being + * compared. + * @param string $group + * Which group this assert belongs to. + * + * @return bool + * TRUE if the assertion was correct (that is, $first == $second within the + * given limits), FALSE otherwise. */ - protected function assertFloat($first, $second, $delta = 0.0000001, $message = '', $group = 'Other') { - return $this->assert(abs($first - $second) <= $delta, $message ? $message : t('Value @first is equal to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group); + protected function assertFloat($first, $second, $message = '', $delta = 0.00000001, $group = 'Other') { + // Check for NaN and Inf because the abs() and sign() code won't like those. + $equal = FALSE + // Equal if both an infinity. + || (is_infinite($first) && is_infinite($second)) + + // Equal if both NaN. + || (is_nan($first) && is_nan($second)) + + // Equal if same absolute value (within limits) and same sign. + || ((abs($first - $second) <= $delta) && (self::sign($first) === self::sign($second))); + + if (empty($message)) { + $default = t('Value !first is equal to value !second.', + array( + '!first' => var_export($first, TRUE), + '!second' => var_export($second, TRUE) + )); + $message = $default; + } + + return $this->assert($equal, $message, $group); } /** * Test some arithmetic handling. */ public function testArithmetic() { - $math_expression = new ctools_math_expr(); - - // Test constant expressions. - $this->assertEqual($math_expression->evaluate('2'), 2); - $random_number = rand(0, 10); - $this->assertEqual($random_number, $math_expression->evaluate((string) $random_number)); - - // Test simple arithmetic. - $random_number_a = rand(5, 10); - $random_number_b = rand(5, 10); - $this->assertEqual($random_number_a + $random_number_b, $math_expression->evaluate("$random_number_a + $random_number_b")); - $this->assertEqual($random_number_a - $random_number_b, $math_expression->evaluate("$random_number_a - $random_number_b")); - $this->assertEqual($random_number_a * $random_number_b, $math_expression->evaluate("$random_number_a * $random_number_b")); - $this->assertEqual(round($random_number_a / $random_number_b, 8), round($math_expression->evaluate("$random_number_a / $random_number_b"), 8)); - - // Test Associative property. - $random_number_c = rand(5, 10); - $this->assertEqual($math_expression->evaluate("$random_number_a + ($random_number_b + $random_number_c)"), $math_expression->evaluate("($random_number_a + $random_number_b) + $random_number_c")); - $this->assertEqual($math_expression->evaluate("$random_number_a * ($random_number_b * $random_number_c)"), $math_expression->evaluate("($random_number_a * $random_number_b) * $random_number_c")); - - // Test Commutative property. - $this->assertEqual($math_expression->evaluate("$random_number_a + $random_number_b"), $math_expression->evaluate("$random_number_b + $random_number_a")); - $this->assertEqual($math_expression->evaluate("$random_number_a * $random_number_b"), $math_expression->evaluate("$random_number_b * $random_number_a")); - - // Test Distributive property. - $this->assertEqual($math_expression->evaluate("($random_number_a + $random_number_b) * $random_number_c"), $math_expression->evaluate("($random_number_a * $random_number_c + $random_number_b * $random_number_c)")); - - $this->assertEqual(pow($random_number_a, $random_number_b), $math_expression->evaluate("$random_number_a ^ $random_number_b")); + $math_expr = new ctools_math_expr(); + + $this->assertEqual($math_expr->evaluate('2'), 2, 'Check Literal 2'); + + $this->assertEqual($math_expr->e('2+1'), $math_expr->evaluate('2+1'), 'Check that e() and evaluate() are equivalent.'); + + foreach (range(1, 4) as $n) { + // Test constant expressions. + $random_number = mt_rand(0, 20); + $this->assertEqual($random_number, $math_expr->evaluate((string) $random_number), "Literal $random_number"); + + // Test simple arithmetic. + $number_a = mt_rand(-55, 777); + $number_b = mt_rand(-555, 77); + $this->assertEqual( + $number_a + $number_b, + $math_expr->evaluate("$number_a + $number_b"), + "Addition: $number_a + $number_b"); + $this->assertEqual( + $number_a - $number_b, + $math_expr->evaluate("$number_a - $number_b"), + "Subtraction: $number_a + $number_b"); + $this->assertFloat( + ($number_a * $number_b), + $math_expr->evaluate("$number_a * $number_b"), + "Multiplication: $number_a * $number_b = " . ($number_a * $number_b)); + $this->assertFloat( + ($number_a / $number_b), + $math_expr->evaluate("$number_a / $number_b"), + "Division: $number_a / $number_b = " . ($number_a / $number_b)); + + // Test Associative property. + $number_c = mt_rand(-99, 77); + $this->assertEqual( + $math_expr->evaluate("$number_a + ($number_b + $number_c)"), + $math_expr->evaluate("($number_a + $number_b) + $number_c"), + "Associative: $number_a + ($number_b + $number_c)"); + $this->assertEqual( + $math_expr->evaluate("$number_a * ($number_b * $number_c)"), + $math_expr->evaluate("($number_a * $number_b) * $number_c"), + "Associative: $number_a * ($number_b * $number_c)"); + + // Test Commutative property. + $this->assertEqual( + $math_expr->evaluate("$number_a + $number_b"), + $math_expr->evaluate("$number_b + $number_a"), + "Commutative: $number_a + $number_b"); + $this->assertEqual( + $math_expr->evaluate("$number_a * $number_b"), + $math_expr->evaluate("$number_b * $number_a"), + "Commutative: $number_a * $number_b"); + + // Test Distributive property. + $this->assertEqual( + $math_expr->evaluate("($number_a + $number_b) * $number_c"), + $math_expr->evaluate("($number_a * $number_c + $number_b * $number_c)"), + "Distributive: ($number_a + $number_b) * $number_c"); + + $random_number = mt_rand(0, 15); + $random_power = mt_rand(-15, 15); + + $this->assertFloat( + pow($random_number, $random_power), + $math_expr->evaluate("$random_number ^ $random_power"), + "$random_number ^ $random_power"); + + $this->assertFloat( + pow($random_number, $random_power), + $math_expr->evaluate("pow($random_number, $random_power)"), + "pow($random_number, $random_power)"); + } } /** - * Test the basic built-in functions in the math expression library. + * Test various built-in transcendental and extended functions. */ public function testBuildInFunctions() { - $math_expression = new ctools_math_expr(); - - // @todo Maybe run this code multiple times to test different values. - $random_double = $this->rand01(); - $random_int = rand(5, 10); - $this->assertFloat(sin($random_double), $math_expression->evaluate("sin($random_double)")); - $this->assertFloat(cos($random_double), $math_expression->evaluate("cos($random_double)")); - $this->assertFloat(tan($random_double), $math_expression->evaluate("tan($random_double)")); - $this->assertFloat(exp($random_double), $math_expression->evaluate("exp($random_double)")); - $this->assertFloat(sqrt($random_double), $math_expression->evaluate("sqrt($random_double)")); - $this->assertFloat(log($random_double), $math_expression->evaluate("ln($random_double)")); - $this->assertFloat(round($random_double), $math_expression->evaluate("round($random_double)")); - $this->assertFloat(abs($random_double + $random_int), $math_expression->evaluate('abs(' . ($random_double + $random_int) . ')')); - $this->assertEqual(round($random_double + $random_int), $math_expression->evaluate('round(' . ($random_double + $random_int) . ')')); - $this->assertEqual(ceil($random_double + $random_int), $math_expression->evaluate('ceil(' . ($random_double + $random_int) . ')')); - $this->assertEqual(floor($random_double + $random_int), $math_expression->evaluate('floor(' . ($random_double + $random_int) . ')')); - - // @fixme: you can't run time without an argument. - $this->assertFloat(time(), $math_expression->evaluate('time(1)')); + $math_expr = new ctools_math_expr(); + + foreach (range(1, 4) as $n) { + $random_double = $this->rand01(); + $random_int = mt_rand(-65535, 65535); + $this->assertFloat(sin($random_double), $math_expr->evaluate("sin($random_double)"), "sin($random_double)"); + $this->assertFloat(cos($random_double), $math_expr->evaluate("cos($random_double)"), "cos($random_double)"); + $this->assertFloat(tan($random_double), $math_expr->evaluate("tan($random_double)"), "tan($random_double)"); + $this->assertFloat(exp($random_double), $math_expr->evaluate("exp($random_double)"), "exp($random_double)"); + $this->assertFloat(sqrt($random_double), $math_expr->evaluate("sqrt($random_double)"), "sqrt($random_double)"); + $this->assertFloat(log($random_double), $math_expr->evaluate("ln($random_double)"), "ln($random_double)"); + $this->assertFloat(round($random_double), $math_expr->evaluate("round($random_double)"), "round($random_double)"); + + $random_real = $random_double + $random_int; + $this->assertFloat(abs($random_real), $math_expr->evaluate('abs(' . $random_real . ')'), "abs($random_real)"); + $this->assertEqual(round($random_real), $math_expr->evaluate('round(' . $random_real . ')'), "round($random_real)"); + $this->assertEqual(ceil($random_real), $math_expr->evaluate('ceil(' . $random_real . ')'), "ceil($random_real)"); + $this->assertEqual(floor($random_real), $math_expr->evaluate('floor(' . $random_real . ')'), "floor($random_real)"); + } + + $this->assertFloat(time(), $math_expr->evaluate('time(1)'), "time(1)"); $random_double_a = $this->rand01(); $random_double_b = $this->rand01(); - $this->assertFloat(max($random_double_a, $random_double_b), $math_expression->evaluate("max($random_double_a, $random_double_b)")); - $this->assertFloat(min($random_double_a, $random_double_b), $math_expression->evaluate("min($random_double_a, $random_double_b)")); + $this->assertFloat( + max($random_double_a, $random_double_b), + $math_expr->evaluate("max($random_double_a, $random_double_b)"), + "max($random_double_a, $random_double_b)"); + $this->assertFloat( + min($random_double_a, $random_double_b), + $math_expr->evaluate("min($random_double_a, $random_double_b)"), + "min($random_double_a, $random_double_b)"); } /** * Test variable handling. */ public function testVariables() { - $math_expression = new ctools_math_expr(); + $math_expr = new ctools_math_expr(); + + // We should have a definition of pi: + $this->assertFloat(pi(), $math_expr->evaluate('pi')); + + // and a definition of e: + $this->assertFloat(exp(1), $math_expr->evaluate('e')); + + $number_a = 5; + $number_b = 10; - $random_number_a = rand(5, 10); - $random_number_b = rand(5, 10); + // Store the first number and use it on a calculation. + $math_expr->evaluate("var = $number_a"); + $this->assertEqual($number_a + $number_b, $math_expr->evaluate("var + $number_b")); - // Store the first random number and use it on calculations. - $math_expression->evaluate("var = $random_number_a"); - $this->assertEqual($random_number_a + $random_number_b, $math_expression->evaluate("var + $random_number_b")); - $this->assertEqual($random_number_a * $random_number_b, $math_expression->evaluate("var * $random_number_b")); - $this->assertEqual($random_number_a - $random_number_b, $math_expression->evaluate("var - $random_number_b")); - $this->assertEqual(round($random_number_a / $random_number_b, 8), round($math_expression->evaluate("var / $random_number_b"), 8)); + // Change the value and check the new value is used. + $math_expr->evaluate("var = $number_b"); + $this->assertEqual( + $number_b + $number_b, + $math_expr->evaluate("var + $number_b"), + "var + $number_b"); + + // Store another number and use it on a calculation. + $math_expr->evaluate("var = $number_a"); + $math_expr->evaluate("newvar = $number_a"); + + $this->assertEqual( + $number_a + $number_a, + $math_expr->evaluate('var + newvar'), + 'var + newvar'); + + $this->assertFloat( + $number_a / $number_b, + $math_expr->evaluate("var / $number_b"), + "var / $number_b"); } /** * Test custom function handling. */ public function testCustomFunctions() { - $math_expression = new ctools_math_expr(); + $math_expr = new ctools_math_expr(); - $random_number_a = rand(5, 10); - $random_number_b = rand(5, 10); + $number_a = mt_rand(5, 10); + $number_b = mt_rand(5, 10); // Create a one-argument function. - $math_expression->evaluate("userfuncs(x) = 2 * x"); - $this->assertEqual($random_number_a * 2, $math_expression->evaluate("userfuncs($random_number_a)")); - $this->assertEqual($random_number_b * 2, $math_expression->evaluate("userfuncs($random_number_b)")); + $math_expr->evaluate("f(x) = 2 * x"); + $this->assertEqual($number_a * 2, $math_expr->evaluate("f($number_a)")); + $this->assertEqual($number_b * 2, $math_expr->evaluate("f($number_b)")); // Create a two-argument function. - $math_expression->evaluate("g(x, y) = 2 * x + y"); - $this->assertEqual($random_number_a * 2 + $random_number_b, $math_expression->evaluate("g($random_number_a, $random_number_b)")); + $math_expr->evaluate("g(x, y) = 2 * x + y"); + $this->assertEqual( + $number_a * 2 + $number_b, + $math_expr->evaluate("g($number_a, $number_b)"), + "g($number_a, $number_b)"); // Use a custom function in another function. - $this->assertEqual(($random_number_a * 2 + $random_number_b) * 2, $math_expression->evaluate("userfuncs(g($random_number_a, $random_number_b))")); + $this->assertEqual( + ($number_a * 2 + $number_b) * 2, + $math_expr->evaluate("f(g($number_a, $number_b))"), + "f(g($number_a, $number_b))"); } + /** + * Test conditional handling. + */ public function testIf() { - $math_expression = new ctools_math_expr(); + $math_expr = new ctools_math_expr(); + + $number_a = mt_rand(-5, 5); + $number_b = mt_rand(10, 20); - $random_number_a = rand(1, 5); - $random_number_b = rand(10, 20); + $this->assertEqual( + $number_a, + $math_expr->evaluate("if(1, $number_a, $number_b)"), + "if(1, $number_a, $number_b)"); - $this->assertEqual($math_expression->evaluate("if(1, $random_number_a, $random_number_b)"), $random_number_a); - $this->assertEqual($math_expression->evaluate("if(1, $random_number_a)"), $random_number_a); + $this->assertEqual( + $number_a, + $math_expr->evaluate("if(1, $number_a)", + "if(1, $number_a)")); - $this->assertEqual($math_expression->evaluate("if(0, $random_number_a, $random_number_b)"), $random_number_b); + $this->assertEqual( + $number_b, + $math_expr->evaluate("if(0, $number_a, $number_b)"), + "if(0, $number_a, $number_b)"); // Also add an expression so ensure it's evaluated. - $this->assertEqual($math_expression->evaluate("if($random_number_a > $random_number_b, $random_number_a, $random_number_b)"), $random_number_b); + $this->assertEqual( + $number_b, + $math_expr->evaluate("if($number_a > $number_b, $number_a, $number_b)"), + "if($number_a > $number_b, $number_a, $number_b)"); + + $this->assertEqual( + $number_b, + $math_expr->evaluate("if($number_a < $number_b, $number_b, $number_a)"), + "if($number_a < $number_b, $number_b, $number_a)"); } }