math_expression.test

File

tests/math_expression.test

View source
<?php


/**
 * Tests the MathExpression library of ctools.
 */
class CtoolsMathExpressionTestCase extends DrupalWebTestCase {
  
  /**
   * {@inheritdoc}
   */
  public static function getInfo() {
    return array(
      'name' => 'Math expressions',
      'description' => 'Test the math expression library of ctools.',
      'group' => 'ctools',
      'dependencies' => array(
        'ctools',
      ),
    );
  }
  
  /**
   * {@inheritdoc}
   */
  public function setUp(array $modules = array()) {
    $modules[] = 'ctools';
    $modules[] = 'ctools_plugin_test';
    parent::setUp($modules);
  }
  
  /**
   * 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 int
   *   -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 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, $message = '', $delta = 1.0E-8, $group = 'Other') {
    // Check for NaN and Inf because the abs() and sign() code won't like those.
    $equal = FALSE || is_infinite($first) && is_infinite($second) || is_nan($first) && is_nan($second) || 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_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}");
      // @todo: Doesn't work with zero or negative powers when number is zero or negative, e.g. 0^0, 0^-2, -2^0, -2^-2.
      $random_number = mt_rand(1, 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 various built-in transcendental and extended functions.
   */
  public function testBuildInFunctions() {
    $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()'), "time()");
    $random_double_a = $this->rand01();
    $random_double_b = $this->rand01();
    $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_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;
    // 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}"));
    // 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_expr = new ctools_math_expr();
    $number_a = mt_rand(5, 10);
    $number_b = mt_rand(5, 10);
    // Create a one-argument function.
    $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_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(($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_expr = new ctools_math_expr();
    $number_a = mt_rand(1, 10);
    $number_b = mt_rand(11, 20);
    foreach (range(1, 4) as $n) {
      // @todo: Doesn't work with negative numbers.
      if ($n == 2 || $n == 4) {
        //$number_a = -$number_a;
      }
      if ($n == 3 || $n == 4) {
        //$number_b = -$number_b;
      }
      $this->assertEqual($number_a, $math_expr->evaluate("if(1, {$number_a}, {$number_b})"), "if(1, {$number_a}, {$number_b})");
      $this->assertEqual($number_a, $math_expr->evaluate("if(1, {$number_a})", "if(1, {$number_a})"));
      $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($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})");
    }
  }

}

Classes

Title Deprecated Summary
CtoolsMathExpressionTestCase Tests the MathExpression library of ctools.