From 5af44f4ee439ea8ca7973ec97238d5c3c77d1544 Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Sat, 5 Jan 2019 16:10:34 +0100 Subject: [PATCH 1/7] Added Vagrant configuration for quickly setting-up development environment: - Sets-up a Debian-based VM. - Installs required system packages for using the B2DB library. - Installs and sets-up a local database server for testing. - Installs vendor libraries through Composer. --- .gitignore | 9 +++++++ Vagrantfile | 17 +++++++++++++ ansible/provision.yml | 57 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 Vagrantfile create mode 100644 ansible/provision.yml diff --git a/.gitignore b/.gitignore index 9f11b75..26a57f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ .idea/ + +# Ignore development environment artefacts. +*~ +.vagrant +ansible/*.retry + +# Ignore Composer artefacts. +composer.lock +vendor/ \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..6859a01 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,17 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + # Configure a single virtual machine. + config.vm.box = "debian/contrib-stretch64" + config.vm.hostname = "b2db" + + # Use Ansible for provisining the virtual machine. Use local provisioning in + # order not to polute the user's host system. + config.vm.provision "ansible_local" do |ansible| + ansible.playbook = "ansible/provision.yml" + ansible.install_mode = "pip" + ansible.version = "2.7.5" + ansible.compatibility_mode = "2.0" + end +end diff --git a/ansible/provision.yml b/ansible/provision.yml new file mode 100644 index 0000000..746a9c2 --- /dev/null +++ b/ansible/provision.yml @@ -0,0 +1,57 @@ +--- + +- hosts: all + become: yes + tasks: + + # Package installation. + - name: Install packages for Ansible use + apt: + name: + - python-mysqldb + + - name: Install database servers + apt: + name: + - mariadb-server + state: present + + - name: Install PHP and B2DB requirements + apt: + name: + - php + - php-cli + - php-apcu + - php7.0-mbstring + + # Create necessary databases and database users. + - name: Create database + mysql_db: + name: b2db + encoding: utf8 + collation: utf8_general_ci + state: present + + - name: Create database user + mysql_user: + name: b2db + host: localhost + password: b2db + priv: 'b2db.*:ALL' + state: present + + - name: Download composer + get_url: + url: https://getcomposer.org/download/1.8.0/composer.phar + checksum: sha256:0901a84d56f6d6ae8f8b96b0c131d4f51ccaf169d491813d2bcedf2a6e4cefa6 + dest: /usr/local/bin/composer + owner: vagrant + group: vagrant + mode: 0755 + + - name: Install development requirements via composer + become_user: vagrant + composer: + command: install + no_dev: no + working_dir: /vagrant From 066d0aedf683522da3553752a89e5472c4137df5 Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Sat, 5 Jan 2019 16:22:42 +0100 Subject: [PATCH 2/7] Add initial configuration for phpunit: - Update Vagrant environment to make it possible to easily run PHPUnit (together with extra deps). - Include PHPUnit in Composer file as development requirement. --- ansible/provision.yml | 18 ++++++++++++++++-- composer.json | 3 +++ phpunit.xml | 11 +++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 phpunit.xml diff --git a/ansible/provision.yml b/ansible/provision.yml index 746a9c2..afc3eb6 100644 --- a/ansible/provision.yml +++ b/ansible/provision.yml @@ -19,10 +19,15 @@ - name: Install PHP and B2DB requirements apt: name: + # Core for library itself. - php - php-cli - php-apcu - php7.0-mbstring + # For Composer. + - unzip + # For running tests. + - php7.0-xml # Create necessary databases and database users. - name: Create database @@ -40,6 +45,7 @@ priv: 'b2db.*:ALL' state: present + # Composer dependency installation. - name: Download composer get_url: url: https://getcomposer.org/download/1.8.0/composer.phar @@ -49,9 +55,17 @@ group: vagrant mode: 0755 - - name: Install development requirements via composer + - name: Install up-to-date development requirements via composer become_user: vagrant composer: - command: install + command: update no_dev: no working_dir: /vagrant + + - name: Create convenience symlink for running the phpunit + file: + src: "/vagrant/vendor/bin/phpunit" + dest: "/usr/local/bin/phpunit" + owner: root + group: root + state: link diff --git a/composer.json b/composer.json index 7bee38d..c90f325 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,9 @@ "ext-mbstring" : "*", "ext-pdo" : "*" }, + "require-dev" : { + "phpunit/phpunit": "^6" + }, "autoload" : { "psr-4" : { "b2db\\" : "src/" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..90b6d3e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + From d7c1e780b77d17e5d943614ea446256e0c7c7a2c Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Sat, 5 Jan 2019 16:41:14 +0100 Subject: [PATCH 3/7] Enable coverage reporting for PHPUnit: - Deploy the xdebug extension in Vagrant dev environment. - Set-up logging to stdout. --- ansible/provision.yml | 1 + phpunit.xml | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/ansible/provision.yml b/ansible/provision.yml index afc3eb6..04a5df1 100644 --- a/ansible/provision.yml +++ b/ansible/provision.yml @@ -28,6 +28,7 @@ - unzip # For running tests. - php7.0-xml + - php-xdebug # Create necessary databases and database users. - name: Create database diff --git a/phpunit.xml b/phpunit.xml index 90b6d3e..1f1e58c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,9 +3,21 @@ colors="true" verbose="true" stopOnFailure="false"> + tests + + + + + + + + src/ + + + From 1d4a56d08443b957ad32fb4e6a9ed9759dfb8212 Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Sat, 5 Jan 2019 16:53:51 +0100 Subject: [PATCH 4/7] Added tests for the Exception class. --- tests/unit/ExceptionTest.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/unit/ExceptionTest.php diff --git a/tests/unit/ExceptionTest.php b/tests/unit/ExceptionTest.php new file mode 100644 index 0000000..1eb68a6 --- /dev/null +++ b/tests/unit/ExceptionTest.php @@ -0,0 +1,34 @@ +assertEquals($message, $exception->getMessage()); + } + + public function test_sql_is_set_when_passed_in() + { + $message = "Test message"; + $sqlQuery = "INSERT INTO mytable VALUES(1)"; + + $exception = new b2db\Exception($message, $sqlQuery); + + $this->assertEquals($sqlQuery, $exception->getSQL()); + } + + public function test_sql_is_set_to_null_when_not_passed_in() + { + $message = "Test message"; + + $exception = new b2db\Exception($message); + + $this->assertEquals(null, $exception->getSQL()); + } +} From ca3455c671b61ece49d276718769f3c0ca6a541b Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Tue, 8 Jan 2019 21:18:30 +0100 Subject: [PATCH 5/7] Implemented tests for the Cache constructor, and minor cleanups: - Added missing documentation. - Updated existing documentation, removing use of deprecated tags (@subpackage) along the way. - Implemented tests for the Cache constructor. - Added new method for checking if the Cache instance is enabled or not. - Throw an exception if unsupported option is passed-in. - Minor clean-ups. --- src/Cache.php | 387 ++++++++++-------- src/InvalidConfigurationException.php | 19 + tests/unit/CacheTest.php | 54 +++ .../InvalidConfigurationExceptionTest.php | 15 + 4 files changed, 310 insertions(+), 165 deletions(-) create mode 100644 src/InvalidConfigurationException.php create mode 100644 tests/unit/CacheTest.php create mode 100644 tests/unit/InvalidConfigurationExceptionTest.php diff --git a/src/Cache.php b/src/Cache.php index 747e3d7..dd0ba3a 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -1,197 +1,254 @@ value pairs. The underlying cache + * implementation is opaque, and the caller does not need to worry + * too much about the details beyond picking the cache type to + * use. * - * @package b2db - * @subpackage core - */ - class Cache implements interfaces\Cache - { - - const TYPE_DUMMY = 0; - const TYPE_APC = 1; - const TYPE_FILE = 2; + * At the moment it is suggested to use the file-based cache. + * + * @package b2db\core + */ + class Cache implements interfaces\Cache + { /** - * @var bool + * Dummy cache implementation (never caches anything). + * @var int */ - protected $enabled = true; - - protected $type; - - protected $path; - - public function __construct($type, $options = []) - { - $this->type = $type; - - if (isset($options['enabled'])) { - $this->enabled = $options['enabled']; - } - - if (isset($options['path'])) { - if (!file_exists($options['path'])) { - throw new \Exception("Configured cache path ({$options['path']}) is not writable. Please check your configuration."); - } - - $this->path = $options['path']; - } - } - + const TYPE_DUMMY = 0; /** - * @return string + * In-memory cache (APC). + * @var int */ - public function getCacheTypeDescription() - { - switch ($this->type) { - case self::TYPE_DUMMY: - return 'Dummy cache'; - case self::TYPE_APC: - return 'In-memory cache (apc)'; - case self::TYPE_FILE: - return 'File cache (' . $this->path . ')'; - } - - return 'Invalid cache type'; - } - + const TYPE_APC = 1; /** - * @return int - */ - public function getType() - { - return $this->type; - } - - /** - * @param string $key The cache key to look up - * - * @param null $default_value - * @return mixed + * File-based cache implementation. + * @var int */ - public function get($key, $default_value = null) - { - if (!$this->enabled) return $default_value; + const TYPE_FILE = 2; - switch ($this->type) { - case self::TYPE_APC: - $success = false; - $var = apc_fetch($key, $success); - - return ($success) ? $var : $default_value; - case self::TYPE_FILE: - $filename = $this->path . $key . '.cache'; - if (!file_exists($filename)) return $default_value; - - $value = unserialize(file_get_contents($filename)); - return $value; - case self::TYPE_DUMMY: - default: - return $default_value; - } - } + /** + * Specifies if the cache is enabled or not. + * @var bool + */ + protected $enabled = true; /** - * @param string $key The cache key to look up - * - * @return bool + * Type of cache implementation provided by the object. See the TYPE constants. + * @var int */ - public function has($key) - { - if (!$this->enabled) return false; - - switch ($this->type) { - case self::TYPE_APC: - $success = false; - apc_fetch($key, $success); - break; - case self::TYPE_FILE: - $filename = $this->path . $key . '.cache'; - $success = file_exists($filename); - break; - case self::TYPE_DUMMY: - default: - $success = false; - } - - return $success; - } + protected $type; /** - * Store an item in the cache - * - * @param string $key The cache key to store the item under - * @param mixed $value The value to store - * - * @return bool + * Directory under which the file-based cache stores files. */ - public function set($key, $value) - { - if (!$this->enabled) { - return false; - } - - switch ($this->type) { - case self::TYPE_APC: - apc_store($key, $value); - break; - case self::TYPE_FILE: - $filename = $this->path . $key . '.cache'; - file_put_contents($filename, serialize($value)); - break; - } - - - return true; - } + protected $path; /** - * Delete an entry from the cache + * Initialise an instance. * - * @param string $key The cache key to delete + * @param int $type Type of cache implementation to use. See document type constants for valid values. + * @param array $options Options for initialising the cache. + * $options = [ + * 'enabled' => bool Specifies if cache is enabled. Default: true. + * 'path' => string Path to directory where the cache files are stored. Directory must exist and be writable. + * ] + * + * @throws InvalidConfigurationException if the passed-in arguments hold invalid/unsupported values. */ - public function delete($key) - { - if (!$this->enabled) return; + public function __construct($type, $options = []) + { + $this->type = $type; + + foreach ($options as $option => $value) + { + if ($option == 'enabled') + { + $this->enabled = $value; + } + else if ($option == 'path') + { + if (!file_exists($value)) + { + throw new \Exception("Configured cache path ($value) is not writable. Please check your configuration."); + } - switch ($this->type) { - case self::TYPE_APC: - apc_delete($key); - break; - case self::TYPE_FILE: - $filename = $this->path . $key . '.cache'; - unlink($filename); + $this->path = $value; + } + else + { + throw new InvalidConfigurationException("Unsupported cache configuration option $option => $value"); + } } - } + } + + + /** + * @return string + */ + public function getCacheTypeDescription() + { + switch ($this->type) { + case self::TYPE_DUMMY: + return 'Dummy cache'; + case self::TYPE_APC: + return 'In-memory cache (apc)'; + case self::TYPE_FILE: + return 'File cache (' . $this->path . ')'; + } + + return 'Invalid cache type'; + } + + /** + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $key The cache key to look up + * + * @param null $default_value + * @return mixed + */ + public function get($key, $default_value = null) + { + if (!$this->enabled) return $default_value; + + switch ($this->type) { + case self::TYPE_APC: + $success = false; + $var = apc_fetch($key, $success); + + return ($success) ? $var : $default_value; + case self::TYPE_FILE: + $filename = $this->path . $key . '.cache'; + if (!file_exists($filename)) return $default_value; + + $value = unserialize(file_get_contents($filename)); + return $value; + case self::TYPE_DUMMY: + default: + return $default_value; + } + } + + /** + * @param string $key The cache key to look up + * + * @return bool + */ + public function has($key) + { + if (!$this->enabled) return false; + + switch ($this->type) { + case self::TYPE_APC: + $success = false; + apc_fetch($key, $success); + break; + case self::TYPE_FILE: + $filename = $this->path . $key . '.cache'; + $success = file_exists($filename); + break; + case self::TYPE_DUMMY: + default: + $success = false; + } + + return $success; + } + + /** + * Store an item in the cache + * + * @param string $key The cache key to store the item under + * @param mixed $value The value to store + * + * @return bool + */ + public function set($key, $value) + { + if (!$this->enabled) { + return false; + } + + switch ($this->type) { + case self::TYPE_APC: + apc_store($key, $value); + break; + case self::TYPE_FILE: + $filename = $this->path . $key . '.cache'; + file_put_contents($filename, serialize($value)); + break; + } + + + return true; + } + + /** + * Delete an entry from the cache + * + * @param string $key The cache key to delete + */ + public function delete($key) + { + if (!$this->enabled) return; + + switch ($this->type) { + case self::TYPE_APC: + apc_delete($key); + break; + case self::TYPE_FILE: + $filename = $this->path . $key . '.cache'; + unlink($filename); + } + } + + /** + * Set the enabled property + * + * @param bool $value + */ + public function setEnabled($value) + { + $this->enabled = $value; + } /** - * Set the enabled property + * Checks if the cache is currently enabled or not. * - * @param bool $value + * @returns bool */ - public function setEnabled($value) + public function isEnabled() { - $this->enabled = $value; + return $this->enabled; } - /** - * Temporarily disable the cache - */ - public function disable() - { - $this->setEnabled(false); - } - - /** - * (Re-)enable the cache - */ - public function enable() - { - $this->setEnabled(true); - } + /** + * Temporarily disable the cache + */ + public function disable() + { + $this->setEnabled(false); + } + + /** + * (Re-)enable the cache + */ + public function enable() + { + $this->setEnabled(true); + } /** * Flush all entries in the cache @@ -213,4 +270,4 @@ public function flush() } } - } + } diff --git a/src/InvalidConfigurationException.php b/src/InvalidConfigurationException.php new file mode 100644 index 0000000..77ec6af --- /dev/null +++ b/src/InvalidConfigurationException.php @@ -0,0 +1,19 @@ +assertEquals(100, $cache->getType()); + } + + public function test_can_set_option_enabled_through_constructor() + { + $cache = new Cache(0, ['enabled' => false]); + + $this->assertEquals(false, $cache->isEnabled()); + } + + public function test_is_enabled_by_default() + { + $cache = new Cache(0); + + $this->assertEquals(true, $cache->isEnabled()); + } + + public function test_can_set_option_path_through_constructor() + { + $cache = new Cache(0, ['path' => '/tmp/b2db-test-cache']); + + $this->assertInstanceOf(Cache::class, $cache); + } + + public function test_throws_exception_in_constructor_if_option_path_set_to_non_writeable_directory() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessageRegExp('/.*\/tmp\/b2db-test-non-existing-directory.*/'); + + $cache = new Cache(0, ['path' => '/tmp/b2db-test-non-existing-directory']); + } + + public function test_throws_exception_in_constructor_if_unsupported_option_is_set() + { + $this->expectException(InvalidConfigurationException::class); + + $cache = new Cache(0, ['bogusoption' => true]); + + } +} diff --git a/tests/unit/InvalidConfigurationExceptionTest.php b/tests/unit/InvalidConfigurationExceptionTest.php new file mode 100644 index 0000000..60730ba --- /dev/null +++ b/tests/unit/InvalidConfigurationExceptionTest.php @@ -0,0 +1,15 @@ +assertEquals($message, $exception->getMessage()); + } +} From 1e38cde932a9b369e0cc55efcfaa1bc5c2df2bdd Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Tue, 8 Jan 2019 21:30:33 +0100 Subject: [PATCH 6/7] Implemented tests and updated documentation for the Cache::getCacheTypeDescription method. --- src/Cache.php | 2 ++ tests/unit/CacheTest.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Cache.php b/src/Cache.php index dd0ba3a..79bc012 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -89,6 +89,8 @@ public function __construct($type, $options = []) /** + * Produces human-friendly description of cache type. + * * @return string */ public function getCacheTypeDescription() diff --git a/tests/unit/CacheTest.php b/tests/unit/CacheTest.php index f61bdd9..38d6918 100644 --- a/tests/unit/CacheTest.php +++ b/tests/unit/CacheTest.php @@ -51,4 +51,36 @@ public function test_throws_exception_in_constructor_if_unsupported_option_is_se $cache = new Cache(0, ['bogusoption' => true]); } + + public function cacheTypes() + { + return [ + [Cache::TYPE_DUMMY], + [Cache::TYPE_APC], + [Cache::TYPE_FILE] + ]; + } + + /** + * @dataProvider cacheTypes + */ + public function test_reports_correct_description_for_valid_cache_type($cacheType) + { + $cache = new Cache($cacheType); + + $description = $cache->getCacheTypeDescription(); + + $this->assertInternalType('string', $description); + $this->assertNotContains('Invalid', $description); + } + + public function test_reports_correct_description_for_invalid_cache_type() + { + $cache = new Cache(100000); + + $description = $cache->getCacheTypeDescription(); + + $this->assertInternalType('string', $description); + $this->assertContains('Invalid', $description); + } } From 1468e9d6ac87c55633c86fd12f5d6d24b67a1126 Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Tue, 8 Jan 2019 21:45:18 +0100 Subject: [PATCH 7/7] Updated documentation for the Cache::getType method. --- src/Cache.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Cache.php b/src/Cache.php index 79bc012..60dc8c8 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -108,6 +108,10 @@ public function getCacheTypeDescription() } /** + * Returns cache type. + * + * Use the `TYPE_` constants when performing comparisons. + * * @return int */ public function getType()