You are here

math_expression.test in Chaos Tool Suite (ctools) 7

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

Namesort descending Description
CtoolsMathExpressionTestCase Tests the MathExpression library of ctools.