diff --git a/src/php/Phix_Project/ContractLib/Contract.php b/src/php/Phix_Project/ContractLib/Contract.php new file mode 100644 index 0000000..4a0a43f --- /dev/null +++ b/src/php/Phix_Project/ContractLib/Contract.php @@ -0,0 +1,274 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +class Contract +{ + /** + * Are we currently enforcing any contracts passed to + * self::Enforce()? + * + * By default, we do not! + * + * @var boolean + */ + static protected $enforcing = false; + + /** + * Stateless library; cannot instantiate + */ + protected function __construct() + { + // do nothing + } + + /** + * Precondition: is the expression $expr true? + * + * Use this method at the start of your method to make sure you're + * happy with the data that you have been passed, and with the + * current state of your object + * + * Throws an E5xx_ContractPreconditionException if the parameter + * passed in is false + * + * @throw E5xx_ContractPreconditionException + * @param boolean $expr + */ + static public function Requires($expr) + { + if (!$expr) + { + throw new E5xx_ContractPreconditionException(); + } + } + + /** + * Precondition: is the expression $expr true? + * + * Use this method at the start of your method to make sure you're + * happy with the data that you have been passed, and with the + * current state of your object + * + * Throws an E5xx_ContractPreconditionException if $expr is false, + * and adds $value to the exception's error message so that you + * can see which value failed the test + * + * @param boolean $expr + * @param mixed $value + */ + static public function RequiresValue($expr, $value) + { + if (!$expr) + { + throw new E5xx_ContractPreconditionException(true, $value); + } + } + + /** + * Postcondition: is the expression $expr true? + * + * Use this method at the end of your method to make sure you're + * happy with the results before your method returns to the caller + * + * Throws an E5xx_ContractPostconditionException if $expr is false. + * + * @param boolean $expr + */ + static public function Ensures($expr) + { + if (!$expr) + { + throw new E5xx_ContractPostconditionException(); + } + } + + /** + * Postcondition: is the expression $expr true? + * + * Use this method at the end of your method to make sure you're + * happy with the results before your method returns to the caller + * + * Throws an E5xx_ContractPostConditionException if $expr is false, + * and adds $value to the exception's error message so that you + * can see which value failed the test + * + * @param boolean $expr + * @param mixed $value + */ + static public function EnsuresValue($expr, $value) + { + if (!$expr) + { + throw new E5xx_ContractPostconditionException(true, $value); + } + } + + /** + * Condition: is the expr $expr true? + * + * Use this method in the middle of your method, to check the + * workings of your method before continuing. + * + * Throws an E5xx_ContractConditionException if $expr is false. + * + * @param type $expr + */ + static public function Asserts($expr) + { + if (!$expr) + { + throw new E5xx_ContractConditionException(); + } + } + + /** + * Condition: is the expr $expr true? + * + * Use this method in the middle of your method, to check the + * workings of your method before continuing. + * + * Throws an E5xx_ContractConditionException if $expr is false, + * and adds $value to the exception's error message so that you + * can see which value failed the test + * + * @param boolean $expr + * @param mixed $value + */ + static public function AssertsValue($expr, $value) + { + if (!$expr) + { + throw new E5xx_ContractConditionException(true, $value); + } + } + + /** + * + * @param type $values + * @param type $callback + */ + static public function ForAll($values, $callback) + { + array_walk($values, $callback); + } + + // ================================================================ + // + // Wrapped contract support + // + // ---------------------------------------------------------------- + + /** + * Tell us to enforce contracts passed to self::Enforce() + */ + static public function EnforceWrappedContracts() + { + self::$enforcing = true; + } + + /** + * Tell us to enforce only calls made directly to the individual + * contract conditions: Requires, Assert, Ensures et al + */ + static public function EnforceOnlyDirectContracts() + { + self::$enforcing = false; + } + + /** + * Check a set of preconditions *if* we are enforcing wrapped + * contracts. + * + * This exists as a performance boost, allowing us to leave + * contracts in the code even in production environments + * + * @param callback $callback + * @param array $params + */ + static public function Preconditions($callback, $params = array()) + { + if (self::$enforcing) + { + call_user_func_array($callback, $params); + } + } + + /** + * Check a set of postconditions *if* we are enforcing wrapped + * contracts. + * + * This exists as a performance boost, allowing us to leave + * contracts in the code even in production environments + * + * @param callback $callback + * @param array $params + */ + static public function Postconditions($callback, $params = array()) + { + if (self::$enforcing) + { + call_user_func_array($callback, $params); + } + } + + /** + * Check a set of conditions mid-method *if* we are enforcing + * wrapped contracts. + * + * This exists as a performance boost, allowing us to leave + * contracts in the code even in production environments + * + * @param callback $callback + * @param array $params + */ + static public function Conditionals($callback, $params = array()) + { + if (self::$enforcing) + { + call_user_func_array($callback, $params); + } + } +} \ No newline at end of file diff --git a/src/php/Phix_Project/ContractLib/ContractInvariant.php b/src/php/Phix_Project/ContractLib/ContractInvariant.php new file mode 100644 index 0000000..a99e700 --- /dev/null +++ b/src/php/Phix_Project/ContractLib/ContractInvariant.php @@ -0,0 +1,55 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +interface ContractInvariant +{ + /** + * A test method that can be called at any time during the life + * of an object, which will run a series of assert()s to prove + * that the object is in a sane state + */ + public function objectInvariant(); +} \ No newline at end of file diff --git a/src/php/Phix_Project/ContractLib/E5xx/ContractConditionException.php b/src/php/Phix_Project/ContractLib/E5xx/ContractConditionException.php new file mode 100644 index 0000000..af07da9 --- /dev/null +++ b/src/php/Phix_Project/ContractLib/E5xx/ContractConditionException.php @@ -0,0 +1,63 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +/** + * This exception is thrown when an assert() statement fails + */ +class E5xx_ContractConditionException extends E5xx_ContractFailedException +{ + public function __construct($hasValue = false, $value = false) + { + if (!$hasValue) + { + parent::__construct("Contract::Assert() failed"); + } + else + { + parent::__construct("Contract::AssertValue() failed with value '" . print_r($value, true) . "'"); + } + } +} \ No newline at end of file diff --git a/src/php/Phix_Project/ContractLib/E5xx/ContractFailedException.php b/src/php/Phix_Project/ContractLib/E5xx/ContractFailedException.php new file mode 100644 index 0000000..6637927 --- /dev/null +++ b/src/php/Phix_Project/ContractLib/E5xx/ContractFailedException.php @@ -0,0 +1,58 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +use Phix_Project\ExceptionsLib\E5xx_InternalServerErrorException; + +/** + * This exception is thrown when an assert() statement fails + */ +class E5xx_ContractFailedException extends E5xx_InternalServerErrorException +{ + public function __construct($message) + { + parent::__construct($message); + } +} \ No newline at end of file diff --git a/src/php/Phix_Project/ContractLib/E5xx/ContractPostconditionException.php b/src/php/Phix_Project/ContractLib/E5xx/ContractPostconditionException.php new file mode 100644 index 0000000..b84b6b0 --- /dev/null +++ b/src/php/Phix_Project/ContractLib/E5xx/ContractPostconditionException.php @@ -0,0 +1,63 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +/** + * This exception is thrown when a contract's postcondition is not met + */ +class E5xx_ContractPostconditionException extends E5xx_ContractFailedException +{ + public function __construct($hasValue = false, $value = null) + { + if (!$hasValue) + { + parent::__construct('Contract::Ensures() failed'); + } + else + { + parent::__construct("Contract::EnsuresValue() failed with value '" . print_r($value, true) . "'"); + } + } +} \ No newline at end of file diff --git a/src/php/Phix_Project/ContractLib/E5xx/ContractPreconditionException.php b/src/php/Phix_Project/ContractLib/E5xx/ContractPreconditionException.php new file mode 100644 index 0000000..7d4e132 --- /dev/null +++ b/src/php/Phix_Project/ContractLib/E5xx/ContractPreconditionException.php @@ -0,0 +1,63 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + +namespace Phix_Project\ContractLib; + +/** + * This exception is thrown when a contract's precondition is not met + */ +class E5xx_ContractPreconditionException extends E5xx_ContractFailedException +{ + public function __construct($hasValue = false, $value = null) + { + if (!$hasValue) + { + parent::__construct("Contract::Requires() failed"); + } + else + { + parent::__construct("Contract::RequiresValue() failed with value '" . print_r($value, true) . "'"); + } + } +} \ No newline at end of file diff --git a/src/tests/unit-tests/php/Phix_Project/Contract/ContractTest.php b/src/tests/unit-tests/php/Phix_Project/Contract/ContractTest.php new file mode 100644 index 0000000..8cc34d0 --- /dev/null +++ b/src/tests/unit-tests/php/Phix_Project/Contract/ContractTest.php @@ -0,0 +1,206 @@ + + * @copyright 2011 Stuart Herbert + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://phix-project.org + * @version @@PACKAGE_VERSION@@ + */ + + +namespace Phix_Project\ContractLib; + +use Exception; +use ReflectionClass; +use PHPUnit_Framework_TestCase; + +use Phix_Project\ContractLib\Contract; + +class ContractTest extends PHPUnit_Framework_TestCase +{ + public function testCannotInstantiate() + { + $refClass = new ReflectionClass('Phix_Project\ContractLib\Contract'); + $refMethod = $refClass->getMethod('__construct'); + $this->assertFalse($refMethod->isPublic()); + } + + /* + public function testCanInstantiate() + { + $obj = new Contract(); + $this->assertTrue($obj instanceof Contract); + } + */ + + public function testPreconditionsMustBeTrue() + { + // preconditional testing + Contract::Requires(true); + $this->assertTrue(true); + + $caughtException = false; + try + { + Contract::Requires(false); + } + catch (E5xx_ContractPreconditionException $e) + { + $caughtException = true; + } + $this->assertTrue($caughtException); + } + + public function testPostconditionsMustBeTrue() + { + // postconditional testing + Contract::Ensures(true); + $this->assertTrue(true); + + $caughtException = false; + try + { + Contract::Ensures(false); + } + catch (E5xx_ContractPostconditionException $e) + { + $caughtException = true; + } + $this->assertTrue($caughtException); + } + + public function testCanApplyConditionsToArrays() + { + $testData1 = array (1,2,3,4,5); + $testData2 = array (6,7,8,9,10); + + // these contracts are satisfied + Contract::ForAll($testData1, function($value) { Contract::Requires($value < 6); }); + $this->assertTrue(true); + Contract::ForAll($testData2, function($value) { Contract::Requires($value > 5); }); + $this->assertTrue(true); + + // these contracts are not satisfied + $caughtException = false; + try + { + Contract::ForAll($testData1, function($value) { Contract::Requires($value > 5); }); + } + catch (E5xx_ContractPreconditionException $e) + { + $caughtException = true; + } + $this->assertTrue($caughtException); + + // these contracts are not satisfied + $caughtException = false; + try + { + Contract::ForAll($testData2, function($value) { Contract::Requires($value < 6); }); + } + catch (E5xx_ContractPreconditionException $e) + { + $caughtException = true; + } + $this->assertTrue($caughtException); + } + + public function testCanSeeTheValueThatFailedThePrecondition() + { + $caughtException = false; + try + { + Contract::RequiresValue(false, 5); + } + catch (E5xx_ContractPreconditionException $e) + { + $caughtException = $e->getMessage(); + } + + // did we catch the exception? + $this->assertTrue($caughtException !== false); + + // did we get the message we expect? + $expected = "Internal server error: Contract::RequiresValue() failed with value '5'"; + $this->assertEquals($expected, $caughtException); + } + + public function testCanSeeTheValueThatFailedThePostcondition() + { + $caughtException = false; + try + { + Contract::EnsuresValue(false, 5); + } + catch (E5xx_ContractPostconditionException $e) + { + $caughtException = $e->getMessage(); + } + + // did we catch the exception? + $this->assertTrue($caughtException !== false); + + // did we get the message we expect? + $expected = "Internal server error: Contract::EnsuresValue() failed with value '5'"; + $this->assertEquals($expected, $caughtException); + } + + public function testCanWrapContractUpForPeformance() + { + Contract::EnforceWrappedContracts(); + + $x = 1; + $y = 2; + $z = 3; + + Contract::Preconditions(function($x, $y, $z) { + Contract::Requires($x < $y); + Contract::Requires($y < $z); + }, array($x, $y, $z)); + + Contract::Conditionals(function() { + Contract::Asserts(2 > 1); + Contract::Asserts(5 > 4); + }); + + Contract::Postconditions(function($x, $y, $z) { + Contract::Ensures($x < $z); + Contract::Ensures($z > $x); + }, array($x, $y, $z)); + } +} \ No newline at end of file