diff --git a/build.xml b/build.xml index fa38d16..ebb8ffc 100644 --- a/build.xml +++ b/build.xml @@ -3,7 +3,7 @@ Running PHP PSR-2 code style checks. - + @@ -23,7 +23,7 @@ Running PHP parallel linter. - + @@ -31,7 +31,7 @@ Running PHP Unit tests. - + diff --git a/composer.json b/composer.json index e3036b7..0bdc7e2 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ } ], "require": { - "php": "^5.5 || ^7.0" + "php": "^5.5 || ^7.0", + "ircmaxell/random-lib": "^1.2" }, "require-dev": { "phpunit/phpunit": ">=4.8", diff --git a/src/Chances/Chance.php b/src/Chances/Chance.php new file mode 100644 index 0000000..0af2cc0 --- /dev/null +++ b/src/Chances/Chance.php @@ -0,0 +1,7 @@ +getLowStrengthGenerator(); + } + $this->generator = $generator; + } + + /** + * Determine whether or not the experiment should run + */ + public function shouldRun() + { + if ($this->percentage == 0) { + return false; + } + + $random = $this->generator + ->generateInt(0, 100); + return $random <= $this->percentage; + } + + /** + * @return int + */ + public function getPercentage() + { + return $this->percentage; + } + + /** + * @param int $percentage + * @return $this + */ + public function setPercentage($percentage) + { + $this->percentage = $percentage; + return $this; + } +} diff --git a/src/Experiment.php b/src/Experiment.php index d04fdf5..3ae831f 100644 --- a/src/Experiment.php +++ b/src/Experiment.php @@ -2,6 +2,8 @@ namespace Scientist; +use Scientist\Chances\Chance; +use Scientist\Chances\StandardChance; use Scientist\Matchers\Matcher; use Scientist\Matchers\StandardMatcher; @@ -60,9 +62,9 @@ class Experiment /** * Execution chance. * - * @var integer + * @var \Scientist\Chances\Chance */ - protected $chance = 100; + protected $chance; /** * Create a new experiment. @@ -75,6 +77,7 @@ public function __construct($name, Laboratory $laboratory) $this->name = $name; $this->laboratory = $laboratory; $this->matcher = new StandardMatcher; + $this->chance = new StandardChance; } /** @@ -185,13 +188,13 @@ public function getMatcher() /** * Set the execution chance. * - * @param integer $chance + * @param Chances\Chance $chance * * @return $this */ - public function chance($chance) + public function chance(Chance $chance) { - $this->chance = (int) $chance; + $this->chance = $chance; return $this; } @@ -199,7 +202,7 @@ public function chance($chance) /** * Get the execution chance. * - * @return integer + * @return Chances\Chance */ public function getChance() { @@ -213,7 +216,8 @@ public function getChance() */ public function shouldRun() { - return rand(0, 100) <= $this->chance; + return $this->chance + ->shouldRun(); } /** diff --git a/src/Machine.php b/src/Machine.php index 8ddb522..0da891a 100644 --- a/src/Machine.php +++ b/src/Machine.php @@ -52,7 +52,6 @@ public function __construct(callable $callback, array $params = [], $muted = fal $this->params = $params; $this->muted = $muted; $this->result = new Result; - } /** diff --git a/tests/Chances/StandardChanceTest.php b/tests/Chances/StandardChanceTest.php new file mode 100644 index 0000000..4001849 --- /dev/null +++ b/tests/Chances/StandardChanceTest.php @@ -0,0 +1,156 @@ +generator = $this->getMockGenerator(); + $this->chance = new StandardChance($this->generator); + } + + public function test_that_standard_chance_is_an_instance_of_chance() + { + $chance = new StandardChance(); + $this->assertInstanceOf('\Scientist\Chances\Chance', $chance); + } + + public function test_that_a_random_number_generator_is_created_upon_instantiation() + { + $chance = new StandardChance(); + $reflection = new \ReflectionClass($chance); + $property = $reflection->getProperty('generator'); + $property->setAccessible(true); + + $this->assertInstanceOf('\RandomLib\Generator', $property->getValue($chance)); + } + + public function test_that_it_takes_a_custom_random_number_generator_in_the_constructor() + { + $reflection = new \ReflectionClass($this->chance); + $property = $reflection->getProperty('generator'); + $property->setAccessible(true); + + $this->assertSame($this->generator, $property->getValue($this->chance)); + } + + /** + * @dataProvider percentageDataProvider + */ + public function test_that_should_run_returns_true_when_the_chance_is_100($random) + { + $this->generator + ->expects($this->once()) + ->method('generateInt') + ->with(0, 100) + ->willReturn($random); + + $this->assertTrue($this->chance->shouldRun()); + } + + public function test_that_the_default_percentage_is_100() + { + $this->assertEquals(100, $this->chance->getPercentage()); + } + + public function test_that_set_percentage_sets_the_percentage() + { + $percentage = rand(1, 100); + $this->chance + ->setPercentage($percentage); + $this->assertEquals($percentage, $this->chance->getPercentage()); + } + + public function test_that_set_percentage_returns_the_chance_object_for_chaining() + { + $percentage = rand(1, 100); + $this->assertSame($this->chance, $this->chance->setPercentage($percentage)); + } + + public function test_that_should_run_always_returns_false_when_percentage_is_zero() + { + $this->generator + ->expects($this->never()) + ->method('generateInt'); + $this->chance + ->setPercentage(0); + $this->assertFalse($this->chance->shouldRun()); + } + + /** + * @dataProvider nonZeroPercentageDataProvider + * @param integer $percentage Percentage of the time to run + */ + public function test_that_it_returns_true_when_percentage_is_greater_than_the_generated_number($percentage) + { + $this->generator + ->expects($this->once()) + ->method('generateInt') + ->with(0, 100) + ->willReturn($percentage - 1); + + $this->chance + ->setPercentage($percentage); + + $this->assertTrue($this->chance->shouldRun()); + } + + /** + * @dataProvider nonZeroPercentageDataProvider + * @param integer $percentage Percentage of the time to run + */ + public function test_that_it_returns_false_when_percentage_is_less_than_the_generated_number($percentage) + { + $this->generator + ->expects($this->once()) + ->method('generateInt') + ->with(0, 100) + ->willReturn($percentage + 1); + + $this->chance + ->setPercentage($percentage); + + $this->assertFalse($this->chance->shouldRun()); + } + + /** + * @return array + */ + public function nonZeroPercentageDataProvider() + { + $percentages = $this->percentageDataProvider(); + array_shift($percentages); + return $percentages; + } + + /** + * Data provider to cover all 100 percentage values + * @return array + */ + public function percentageDataProvider() + { + return array_map(function ($value) { + return [$value]; + }, range(0, 100)); + } + + public function getMockGenerator() + { + return $this->getMockBuilder('\RandomLib\Generator') + ->disableOriginalConstructor() + ->disableProxyingToOriginalMethods() + ->getMock(); + } +} diff --git a/tests/ExperimentTest.php b/tests/ExperimentTest.php index 43e0220..eb5ce88 100644 --- a/tests/ExperimentTest.php +++ b/tests/ExperimentTest.php @@ -63,9 +63,10 @@ public function test_that_multiple_trial_callbacks_can_be_defined() public function test_that_a_chance_variable_can_be_set() { + $chance = $this->getMock('\Scientist\Chances\Chance'); $e = new Experiment('test experiment', new Laboratory); - $e->chance(50); - $this->assertEquals(50, $e->getChance()); + $e->chance($chance); + $this->assertEquals($chance, $e->getChance()); } public function test_that_an_experiment_matcher_can_be_set() @@ -93,12 +94,25 @@ public function test_that_running_experiment_with_no_laboratory_executes_control public function test_that_running_experiment_with_zero_chance_executes_control() { + $chance = $this->getMockChance(); + $chance->expects($this->once()) + ->method('shouldRun') + ->willReturn(false); + $l = new Laboratory; $v = $l->experiment('test experiment') ->control(function () { return 'foo'; }) - ->chance(0) + ->chance($chance) ->run(); $this->assertEquals('foo', $v); } + + public function getMockChance() + { + return $this->getMockBuilder('\Scientist\Chances\Chance') + ->disableOriginalConstructor() + ->disableProxyingToOriginalMethods() + ->getMock(); + } }