From f36cb72af26a06410fd14d0d9f375404632b1733 Mon Sep 17 00:00:00 2001 From: Ihor Sychevskyi Date: Sat, 5 Nov 2022 02:07:17 +0200 Subject: [PATCH 01/25] update links (#41) --- src/Codeception/Module/Db.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 8f3a09c0..0563bb4a 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -23,7 +23,7 @@ * Access a database. * * The most important function of this module is to clean a database before each test. - * This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](http://codeception.com/docs/modules/Db#seeInDatabase) + * This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](https://codeception.com/docs/modules/Db#seeInDatabase) * * In order to have your database populated with data you need a raw SQL dump. * Simply put the dump in the `tests/_data` directory (by default) and specify the path in the config. @@ -55,11 +55,11 @@ * * cleanup: false - whether the dump should be reloaded before each test * * reconnect: false - whether the module should reconnect to the database before each test * * waitlock: 0 - wait lock (in seconds) that the database session should use for DDL statements - * * ssl_key - path to the SSL key (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key) - * * ssl_cert - path to the SSL certificate (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert) - * * ssl_ca - path to the SSL certificate authority (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca) - * * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php) - * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) + * * ssl_key - path to the SSL key (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key) + * * ssl_cert - path to the SSL certificate (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert) + * * ssl_ca - path to the SSL certificate authority (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca) + * * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php) + * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) * * databases - include more database configs and switch between them in tests. * * initial_queries - list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation * * skip_cleanup_if_failed - Do not perform the cleanup if the tests failed. If this is used, manual cleanup might be required when re-running From 9b4b881cbf899236b337c4efd705b76a84813ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Thu, 1 Dec 2022 19:27:46 +0100 Subject: [PATCH 02/25] feat: use rows value to delete inserted row if primary key is filled --- src/Codeception/Module/Db.php | 9 +++++++-- tests/data/dumps/mysql.sql | 12 +++++++++++- tests/unit/Codeception/Module/Db/MySqlDbTest.php | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 0563bb4a..be5f4752 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -801,8 +801,13 @@ private function addInsertedRow(string $table, array $row, $id): void $primaryKey = $this->_getDriver()->getPrimaryKey($table); $primary = []; if ($primaryKey !== []) { - if ($id && count($primaryKey) === 1) { - $primary [$primaryKey[0]] = $id; + $filledKeys = array_intersect($primaryKey, array_keys($row)); + $primaryKeyIsFilled = count($filledKeys) === count($primaryKey); + + if ($primaryKeyIsFilled) { + $primary = array_intersect_key($row, array_flip($primaryKey)); + } elseif ($id && count($primaryKey) === 1) { + $primary[$primaryKey[0]] = $id; } else { foreach ($primaryKey as $column) { if (isset($row[$column])) { diff --git a/tests/data/dumps/mysql.sql b/tests/data/dumps/mysql.sql index 4102f7ef..b1ca296c 100644 --- a/tests/data/dumps/mysql.sql +++ b/tests/data/dumps/mysql.sql @@ -94,8 +94,18 @@ CREATE TABLE `no_pk` ( `status` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `auto_increment_not_on_pk` ( + `id` int(11) NOT NULL, + `counter` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE INDEX counter ON `auto_increment_not_on_pk` (counter); +ALTER TABLE `auto_increment_not_on_pk` + MODIFY counter int AUTO_INCREMENT; + CREATE TABLE `empty_table` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(255), PRIMARY KEY(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 7fb56397..c0cf32e9 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -101,4 +101,16 @@ public function testGrabColumnFromDatabase() ], $emails); } + + public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() + { + $testData = [ + 'id' => 777, + ]; + $this->module->haveInDatabase('auto_increment_not_on_pk', $testData); + $this->module->seeInDatabase('auto_increment_not_on_pk', $testData); + $this->module->_after(Stub::makeEmpty(TestInterface::class)); + + $this->module->dontSeeInDatabase('auto_increment_not_on_pk', $testData); + } } From 24a5e95a7f01db9f36787f42ef27fb04d4d61606 Mon Sep 17 00:00:00 2001 From: Jonathan Massuchetti Date: Sat, 3 Dec 2022 00:38:52 +0100 Subject: [PATCH 03/25] feat: support auto increment on a composite pk --- src/Codeception/Module/Db.php | 14 ++++++++------ tests/data/dumps/mysql.sql | 7 +++++++ tests/unit/Codeception/Module/Db/MySqlDbTest.php | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index be5f4752..53094094 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -759,8 +759,8 @@ protected function loadDumpUsingDriver(string $databaseKey): void } /** - * Inserts an SQL record into a database. This record will be erased after the test, - * unless you've configured "skip_cleanup_if_failed", and the test fails. + * Inserts an SQL record into a database. This record will be erased after the test, + * unless you've configured "skip_cleanup_if_failed", and the test fails. * * ```php * module->_before($testCase1); - + $connection1 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase1); @@ -83,7 +83,7 @@ public function testInitialQueriesAreExecuted() ]; $this->module->_reconfigure($config); $this->module->_before(Stub::makeEmpty(TestInterface::class)); - + $usedDatabaseName = $this->module->dbh->query('SELECT DATABASE();')->fetch(PDO::FETCH_COLUMN); $this->assertSame($dbName, $usedDatabaseName); @@ -113,4 +113,16 @@ public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() $this->module->dontSeeInDatabase('auto_increment_not_on_pk', $testData); } + + public function testHaveInDatabaseAutoIncrementOnCompositePrimaryKey() + { + $testData = [ + 'id' => 777, + ]; + $this->module->haveInDatabase('auto_increment_on_composite_pk', $testData); + $this->module->seeInDatabase('auto_increment_on_composite_pk', $testData); + $this->module->_after(Stub::makeEmpty(TestInterface::class)); + + $this->module->dontSeeInDatabase('auto_increment_on_composite_pk', $testData); + } } From 72b6313f9a8cf601d4cda2af5564bc6cc74646e9 Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 11:49:19 +0200 Subject: [PATCH 04/25] Add grabEntryFromDatabase and grabEntriesFromDatabase methods * feat: mysql helper to grab entire rows * fix: use semantic assertions * feat: grabEntryFromDatabase makes the test fail if no row is found * misc: code style * Fix syntax error made while resolving conflict Co-authored-by: Gintautas Miselis --- src/Codeception/Module/Db.php | 73 ++++++++++++++++++- .../Codeception/Module/Db/AbstractDbTest.php | 2 +- .../Codeception/Module/Db/MySqlDbTest.php | 50 +++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 53094094..0dff9175 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -729,7 +729,7 @@ public function _loadDump(string $databaseKey = null, array $databaseConfig = nu $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; - if ($databaseConfig['populator']) { + if (!empty($databaseConfig['populator'])) { $this->loadDumpUsingPopulator($databaseKey, $databaseConfig); return; } @@ -956,6 +956,77 @@ public function grabFromDatabase(string $table, string $column, array $criteria return $this->proceedSeeInDatabase($table, $column, $criteria); } + /** + * Fetches a whole entry from a database. + * Make the test fail if the entry is not found. + * Provide table name, desired column and criteria. + * + * ``` php + * grabEntryFromDatabase('users', array('name' => 'Davert')); + * ``` + * Comparison expressions can be used as well: + * + * ```php + * grabEntryFromDatabase('posts', ['num_comments >=' => 100]); + * $user = $I->grabEntryFromDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @return array Returns a single entry value + * @throws PDOException|Exception + */ + public function grabEntryFromDatabase(string $table, array $criteria = []): array + { + $query = $this->_getDriver()->select('*', $table, $criteria); + $parameters = array_values($criteria); + $this->debugSection('Query', $query); + $this->debugSection('Parameters', $parameters); + $sth = $this->_getDriver()->executeQuery($query, $parameters); + + $result = $sth->fetch(PDO::FETCH_ASSOC, 0); + + if ($result === false) { + throw new \AssertionError("No matching row found"); + } + + return $result; + } + + /** + * Fetches a set of entries from a database. + * Provide table name and criteria. + * + * ``` php + * grabEntriesFromDatabase('users', array('name' => 'Davert')); + * ``` + * Comparison expressions can be used as well: + * + * ```php + * grabEntriesFromDatabase('posts', ['num_comments >=' => 100]); + * $user = $I->grabEntriesFromDatabase('users', ['email like' => 'miles%']); + * ``` + * + * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. + * + * @return array> Returns an array of all matched rows + * @throws PDOException|Exception + */ + public function grabEntriesFromDatabase(string $table, array $criteria = []): array + { + $query = $this->_getDriver()->select('*', $table, $criteria); + $parameters = array_values($criteria); + $this->debugSection('Query', $query); + $this->debugSection('Parameters', $parameters); + $sth = $this->_getDriver()->executeQuery($query, $parameters); + + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + /** * Returns the number of rows in a database * diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index 9f4ccfe9..325482c8 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -175,7 +175,7 @@ public function testLoadWithPopulator() 'cleanup' => true, ] ); - $this->module->_loadDump(); + $this->module->_loadDump(null, $this->getConfig()); $this->assertTrue($this->module->_isPopulated()); $this->module->seeInDatabase('users', ['name' => 'davert']); } diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 27c10599..8e083b8b 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -91,6 +91,7 @@ public function testInitialQueriesAreExecuted() public function testGrabColumnFromDatabase() { + $this->module->_beforeSuite(); $emails = $this->module->grabColumnFromDatabase('users', 'email'); $this->assertSame( [ @@ -102,6 +103,55 @@ public function testGrabColumnFromDatabase() $emails); } + public function testGrabEntryFromDatabaseShouldFailIfNotFound() + { + try { + $this->module->grabEntryFromDatabase('users', ['email' => 'doesnot@exist.info']); + $this->fail("should have thrown an exception"); + } catch (\Throwable $t) { + $this->assertInstanceOf(AssertionError::class, $t); + } + } + + public function testGrabEntryFromDatabaseShouldReturnASingleEntry() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntryFromDatabase('users', ['is_active' => true]); + + $this->assertArrayNotHasKey(0, $result); + } + + public function testGrabEntryFromDatabaseShouldReturnAnAssocArray() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntryFromDatabase('users', ['is_active' => true]); + + $this->assertArrayHasKey('is_active', $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnAnEmptyArrayIfNoRowMatches() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['email' => 'doesnot@exist.info']); + $this->assertEquals([], $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnAllMatchedRows() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['is_active' => true]); + + $this->assertCount(3, $result); + } + + public function testGrabEntriesFromDatabaseShouldReturnASetOfAssocArray() + { + $this->module->_beforeSuite(); + $result = $this->module->grabEntriesFromDatabase('users', ['is_active' => true]); + + $this->assertEquals(true, array_key_exists('is_active', $result[0])); + } + public function testHaveInDatabaseAutoIncrementOnANonPrimaryKey() { $testData = [ From 298150cb18d4191f41ee2d5e956a2171a8e40015 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Sat, 3 Dec 2022 11:51:05 +0200 Subject: [PATCH 05/25] Remove unnecessary and incorrect @return annotation --- src/Codeception/Module/Db.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 0dff9175..42c58bca 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -529,7 +529,6 @@ private function readSql($databaseKey = null, $databaseConfig = null): void } /** - * @return bool|null|string|string[] * @throws ModuleConfigException */ private function readSqlFile(string $filePath): ?string From 1f659bdfdb0654a94a051e904867016f531b2a23 Mon Sep 17 00:00:00 2001 From: Jesus The Hun Date: Sat, 3 Dec 2022 11:52:18 +0200 Subject: [PATCH 06/25] add Dockerfiles and docker-compose for local testing * add Dockerfiles and docker-compose for local testing * fix: env defaults * fix: populator requires config * fix: use cli base image and import composer from official image * fix: key check triggering warning * fix: exclude Dockerfiles from git archives Co-authored-by: Gintautas Miselis --- .gitattributes | 1 + docker-compose.yml | 26 +++++++++++++++ php81.Dockerfile | 33 +++++++++++++++++++ .../Codeception/Module/Db/MySqlDbTest.php | 6 ++-- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml create mode 100644 php81.Dockerfile diff --git a/.gitattributes b/.gitattributes index 87f36790..6e7735a0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ /Robofile.php export-ignore /*.md export-ignore /*.yml export-ignore +/*.Dockerfile export-ignore diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b164309f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.9" + +services: + php81: + image: codeception-module-db-php81:2.2.0 + build: + context: . + dockerfile: ./php81.Dockerfile + environment: + MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" + MYSQL_USER: root + MYSQL_PASSWORD: codeception + XDEBUG_MODE: "debug" + XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" + PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field + volumes: + - ".:/var/www/html" + + mariadb105: + image: mariadb:10.5 + environment: + MARIADB_ROOT_PASSWORD: codeception + MARIADB_DATABASE: codeception + ports: + - "3102:3306" + diff --git a/php81.Dockerfile b/php81.Dockerfile new file mode 100644 index 00000000..9e6b8bcd --- /dev/null +++ b/php81.Dockerfile @@ -0,0 +1,33 @@ +FROM php:8.1-cli + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + mariadb-client-10.5 + +RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql +RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli +RUN docker-php-ext-install zip + +RUN pecl install xdebug-3.1.5 && \ + echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY composer.json . +COPY composer.lock . + +RUN composer install --no-autoloader + +COPY . . + +RUN composer dump-autoload -o + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 8e083b8b..058aa4f5 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -23,11 +23,13 @@ public function getPopulator(): string public function getConfig(): array { $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; + $user = getenv('MYSQL_USER') ? getenv('MYSQL_USER') : 'root'; $password = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; + $dsn = getenv('MYSQL_DSN') ? getenv('MYSQL_DSN') : 'mysql:host='.$host.';dbname=codeception_test'; return [ - 'dsn' => 'mysql:host='.$host.';dbname=codeception_test', - 'user' => 'root', + 'dsn' => $dsn, + 'user' => $user, 'password' => $password, 'dump' => 'tests/data/dumps/mysql.sql', 'reconnect' => true, From 0ff358215efef8513869d407bb3bb850187975a3 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Sat, 3 Dec 2022 11:57:03 +0200 Subject: [PATCH 07/25] Run tests on PHP 8.2 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d6b4a47b..89e0a8f7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: - php: [8.0, 8.1] + php: [8.0, 8.1, 8.2] steps: - name: Checkout code From a5b0a198bcfb9ad0a29a1d807421b7ceab65073e Mon Sep 17 00:00:00 2001 From: rizort Date: Sat, 18 Mar 2023 09:29:16 +0200 Subject: [PATCH 08/25] Throw exception with advice to increase pcre.backtrack_limit if preg_replace returned null during dump loading. --- src/Codeception/Module/Db.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 42c58bca..4ef172cf 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -529,7 +529,7 @@ private function readSql($databaseKey = null, $databaseConfig = null): void } /** - * @throws ModuleConfigException + * @throws ModuleConfigException|ModuleException */ private function readSqlFile(string $filePath): ?string { @@ -545,7 +545,16 @@ private function readSqlFile(string $filePath): ?string $sql = file_get_contents(Configuration::projectDir() . $filePath); // remove C-style comments (except MySQL directives) - return preg_replace('#/\*(?!!\d+).*?\*/#s', '', $sql); + $replaced = preg_replace('#/\*(?!!\d+).*?\*/#s', '', $sql); + + if (!empty($sql) && is_null($replaced)) { + throw new ModuleException( + __CLASS__, + "Please, increase pcre.backtrack_limit value in PHP CLI config" + ); + } + + return $replaced; } private function connect($databaseKey, $databaseConfig): void From 185889230bccfc65d2c03624879ade5357ec43f9 Mon Sep 17 00:00:00 2001 From: Sergei Matros Date: Sat, 18 Mar 2023 09:35:25 +0200 Subject: [PATCH 09/25] Fixed encoding of binary values in assertion messages Co-authored-by: sm --- src/Codeception/Lib/Driver/Db.php | 7 +++++++ src/Codeception/Module/Db.php | 6 +++--- tests/data/dumps/mysql.sql | 9 +++++---- tests/data/dumps/postgres.sql | 12 +++++++----- tests/data/dumps/sqlite.sql | 10 +++++----- tests/data/sqlite.db | Bin 36864 -> 36864 bytes .../Codeception/Module/Db/AbstractDbTest.php | 7 +++++++ 7 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index fdcdb4e4..b5680459 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -294,6 +294,8 @@ public function executeQuery($query, array $params): PDOStatement $type = PDO::PARAM_BOOL; } elseif (is_int($param)) { $type = PDO::PARAM_INT; + } elseif ($this->isBinary($param)) { + $type = PDO::PARAM_LOB; } else { $type = PDO::PARAM_STR; } @@ -342,4 +344,9 @@ public function getOptions(): array { return $this->options; } + + protected function isBinary(string $string): bool + { + return false === mb_detect_encoding($string, null, true); + } } diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 4ef172cf..db418bd3 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -845,7 +845,7 @@ public function seeInDatabase(string $table, array $criteria = []): void $this->assertGreaterThan( 0, $res, - 'No matching records found for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR) . ' in table ' . $table + 'No matching records found for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE) . ' in table ' . $table ); } @@ -871,7 +871,7 @@ public function seeNumRecords(int $expectedNumber, string $table, array $criteri 'The number of found rows (%d) does not match expected number %d for criteria %s in table %s', $actualNumber, $expectedNumber, - json_encode($criteria, JSON_THROW_ON_ERROR), + json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE), $table ) ); @@ -883,7 +883,7 @@ public function dontSeeInDatabase(string $table, array $criteria = []): void $this->assertLessThan( 1, $count, - 'Unexpectedly found matching records for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR) . ' in table ' . $table + 'Unexpectedly found matching records for criteria ' . json_encode($criteria, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE) . ' in table ' . $table ); } diff --git a/tests/data/dumps/mysql.sql b/tests/data/dumps/mysql.sql index 3f9059fc..3617afd5 100644 --- a/tests/data/dumps/mysql.sql +++ b/tests/data/dumps/mysql.sql @@ -16,6 +16,7 @@ insert into `groups`(`id`,`name`,`enabled`,`created_at`) values (2,'jazzman',0, CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, + `uuid` binary(16) DEFAULT NULL, `name` varchar(30) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `is_active` bit(1) DEFAULT b'1', @@ -24,13 +25,13 @@ CREATE TABLE `users` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (1,'davert','davert@mail.ua', b'1','2012-02-01 21:17:04'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (1,0x11edc34b01d972fa9c1d0242ac120006,'davert','davert@mail.ua', b'1','2012-02-01 21:17:04'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (2,'nick','nick@mail.ua', b'1','2012-02-01 21:17:15'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (2,null,'nick','nick@mail.ua', b'1','2012-02-01 21:17:15'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (3,'miles','miles@davis.com', b'1','2012-02-01 21:17:25'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (3,null,'miles','miles@davis.com', b'1','2012-02-01 21:17:25'); -insert into `users`(`id`,`name`,`email`, `is_active`,`created_at`) values (4,'bird','charlie@parker.com', b'0','2012-02-01 21:17:39'); +insert into `users`(`id`,`uuid`, `name`,`email`, `is_active`,`created_at`) values (4,null,'bird','charlie@parker.com', b'0','2012-02-01 21:17:39'); diff --git a/tests/data/dumps/postgres.sql b/tests/data/dumps/postgres.sql index a95dfe0f..13b87d0e 100755 --- a/tests/data/dumps/postgres.sql +++ b/tests/data/dumps/postgres.sql @@ -28,6 +28,7 @@ SET default_with_oids = false; DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( name character varying(30), + uuid bytea, email character varying(50), created_at timestamp without time zone DEFAULT now(), id integer NOT NULL @@ -181,6 +182,7 @@ ALTER SEQUENCE permissions_id_seq OWNED BY permissions.id; DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( name character varying(30), + uuid bytea, email character varying(50), created_at timestamp without time zone DEFAULT now(), id integer NOT NULL @@ -332,11 +334,11 @@ SELECT pg_catalog.setval('permissions_id_seq', 10, true); -- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: - -- -COPY users (name, email, created_at, id) FROM stdin; -davert davert@mail.ua \N 1 -nick nick@mail.ua 2012-02-02 22:30:31.748 2 -miles miles@davis.com 2012-02-02 22:30:52.166 3 -bird charlie@parker.com 2012-02-02 22:32:13.107 4 +COPY users (name, uuid, email, created_at, id) FROM stdin; +davert \\x11edc34b01d972fa9c1d0242ac120006 davert@mail.ua \N 1 +nick NULL nick@mail.ua 2012-02-02 22:30:31.748 2 +miles NULL miles@davis.com 2012-02-02 22:30:52.166 3 +bird NULL charlie@parker.com 2012-02-02 22:32:13.107 4 \. diff --git a/tests/data/dumps/sqlite.sql b/tests/data/dumps/sqlite.sql index 4fbfeb95..87d65cd9 100755 --- a/tests/data/dumps/sqlite.sql +++ b/tests/data/dumps/sqlite.sql @@ -11,11 +11,11 @@ INSERT INTO "permissions" VALUES(5,3,2,'member'); INSERT INTO "permissions" VALUES(7,4,2,'admin'); DROP TABLE IF EXISTS "users"; -CREATE TABLE "users" ("name" VARCHAR, "email" VARCHAR, "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP); -INSERT INTO "users" VALUES('davert','davert@mail.ua','2012-02-01 21:17:04'); -INSERT INTO "users" VALUES('nick','nick@mail.ua','2012-02-01 21:17:15'); -INSERT INTO "users" VALUES('miles','miles@davis.com','2012-02-01 21:17:25'); -INSERT INTO "users" VALUES('bird','charlie@parker.com','2012-02-01 21:17:39'); +CREATE TABLE "users" ("name" VARCHAR, "uuid" BLOB DEFAULT NULL, "email" VARCHAR, "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP); +INSERT INTO "users" VALUES('davert',X'11edc34b01d972fa9c1d0242ac120006','davert@mail.ua','2012-02-01 21:17:04'); +INSERT INTO "users" VALUES('nick',null,'nick@mail.ua','2012-02-01 21:17:15'); +INSERT INTO "users" VALUES('miles',null,'miles@davis.com','2012-02-01 21:17:25'); +INSERT INTO "users" VALUES('bird',null,'charlie@parker.com','2012-02-01 21:17:39'); DROP TABLE IF EXISTS "empty_table"; CREATE TABLE "empty_table" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , "field" VARCHAR); diff --git a/tests/data/sqlite.db b/tests/data/sqlite.db index 6c7c70e451b5db94d66ed1dcfe7bf6074fa51875..b8b0d252a70ff2ed7592f9d4ccb45196f21b2df9 100644 GIT binary patch delta 169 zcmZozz|^pSX+n~e_e5R>1_lo95(a)ZzAt{YDygGsoRV0UT2vzV_OLhO V&7xm(WSN}S2r;m2zVEN-001BpE8qYC delta 125 zcmZozz|^pSX+n~e%m!Wt1_lo9eg=LwzAtb|~o!5Lb2j4$KEg@PgbXizLC)@jLvuHE3h)>S<*JM#= c0*cJ_SLM-WWRcV~PDw0FEh^c3-(S%I0Hc&2O8@`> diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index 325482c8..f880866b 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -64,6 +64,11 @@ public function testConnectionIsKeptForTheWholeSuite() $this->module->_afterSuite(); } + public function testSeeInDatabaseWithBinary() + { + $this->module->seeInDatabase('users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); + } + public function testSeeInDatabase() { $this->module->seeInDatabase('users', ['name' => 'davert']); @@ -71,6 +76,7 @@ public function testSeeInDatabase() public function testCountInDatabase() { + $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); $this->module->seeNumRecords(1, 'users', ['name' => 'davert']); $this->module->seeNumRecords(0, 'users', ['name' => 'davert', 'email' => 'xxx@yyy.zz']); $this->module->seeNumRecords(0, 'users', ['name' => 'user1']); @@ -78,6 +84,7 @@ public function testCountInDatabase() public function testDontSeeInDatabase() { + $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); $this->module->dontSeeInDatabase('users', ['name' => 'user1']); } From ab9d53a181a41b7b3ca14da12fe1a26db929abbf Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Mon, 29 May 2023 17:41:03 +0200 Subject: [PATCH 10/25] #49 Cast last insert id to string to avoid type error when pdo returns with false in case of dblib --- .github/workflows/main.yml | 25 ++- docker-compose.yml | 40 +++- php81.Dockerfile | 30 ++- src/Codeception/Lib/Driver/Db.php | 2 +- tests/data/dumps/mssql.sql | 76 ++++++++ tests/data/scripts/mssql.sh | 19 ++ tests/data/scripts/wait-for-it.sh | 184 ++++++++++++++++++ .../unit/Codeception/Lib/Driver/MysqlTest.php | 12 +- .../Codeception/Lib/Driver/PostgresTest.php | 20 +- .../Codeception/Module/Db/AbstractDbTest.php | 33 +++- .../Module/Db/MssqlDblibDbTest.php | 39 ++++ .../Module/Db/MssqlSqlSrvDbTest.php | 39 ++++ .../Codeception/Module/Db/MySqlDbTest.php | 9 +- .../Module/Db/PostgreSqlDbTest.php | 14 +- 14 files changed, 500 insertions(+), 42 deletions(-) create mode 100644 tests/data/dumps/mssql.sql create mode 100755 tests/data/scripts/mssql.sh create mode 100644 tests/data/scripts/wait-for-it.sh create mode 100644 tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php create mode 100644 tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89e0a8f7..ce2a2089 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,12 +31,27 @@ jobs: --health-retries 5 ports: - 5432:5432 + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: P@ssw0rd + ACCEPT_EULA: 'Y' + ports: + - 1433:1433 + options: >- + --health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -d master -Q 'SELECT COUNT(*) FROM master.dbo.spt_values;'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: matrix: php: [8.0, 8.1, 8.2] steps: + - name: Create default database for sqlsrv as image does not support it + run: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -Q 'CREATE DATABASE codeception_test' + - name: Checkout code uses: actions/checkout@v2 @@ -44,7 +59,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: pdo, pgsql, mysql, sqlite + extensions: pdo, pgsql, mysql, sqlite, sqlsrv, pdo_sqlsrv, pdo_dblib coverage: none - name: Validate composer.json and composer.lock @@ -56,5 +71,11 @@ jobs: - name: Run test suite run: php vendor/bin/codecept run env: - PGPASSWORD: postgres MYSQL_HOST: 127.0.0.1 + MYSQL_DB: codeception_test + PG_HOST: 127.0.0.1 + PG_DB: codeception_test + PG_PASSWORD: postgres + MSSQL_HOST: 127.0.0.1 + MSSQL_DB: codeception_test + MSSQL_PASSWORD: P@ssw0rd diff --git a/docker-compose.yml b/docker-compose.yml index b164309f..21e6d5dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,20 +7,46 @@ services: context: . dockerfile: ./php81.Dockerfile environment: - MYSQL_DSN: "mysql:host=host.docker.internal;port=3102;dbname=codeception" - MYSQL_USER: root + MYSQL_HOST: host.docker.internal + MYSQL_DB: codeception MYSQL_PASSWORD: codeception + PG_HOST: host.docker.internal + PG_DB: codeception + PG_PASSWORD: codeception + MSSQL_HOST: host.docker.internal + MSSQL_DB: codeception + MSSQL_PASSWORD: 'P@ssw0rd' XDEBUG_MODE: "debug" XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field volumes: - ".:/var/www/html" - mariadb105: - image: mariadb:10.5 + mysql: + image: mysql:5.7 environment: - MARIADB_ROOT_PASSWORD: codeception - MARIADB_DATABASE: codeception + MYSQL_ROOT_PASSWORD: codeception + MYSQL_DATABASE: codeception ports: - - "3102:3306" + - "3306:3306" + + postgres: + image: postgres + environment: + POSTGRES_PASSWORD: codeception + POSTGRES_DB: codeception + ports: + - "5432:5432" + + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + SA_PASSWORD: 'P@ssw0rd' + MSSQL_DATABASE: codeception + ACCEPT_EULA: 'Y' + ports: + - "1433:1433" + volumes: + - ./tests/data/scripts:/scripts:ro + entrypoint: [ "/bin/bash", "-c", "/scripts/mssql.sh" ] diff --git a/php81.Dockerfile b/php81.Dockerfile index 9e6b8bcd..ca24ccde 100644 --- a/php81.Dockerfile +++ b/php81.Dockerfile @@ -1,5 +1,7 @@ FROM php:8.1-cli +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + RUN apt-get update && \ apt-get install -y \ unzip \ @@ -7,27 +9,21 @@ RUN apt-get update && \ git \ zlib1g-dev \ libzip-dev \ - mariadb-client-10.5 - -RUN docker-php-ext-install pdo pdo_mysql && docker-php-ext-enable pdo pdo_mysql -RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli -RUN docker-php-ext-install zip - -RUN pecl install xdebug-3.1.5 && \ - echo zend_extension=xdebug.so > $PHP_INI_DIR/conf.d/xdebug.ini + libpq-dev \ + mariadb-client-10.5 + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.11.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.1.5 COPY --from=composer /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html -COPY composer.json . -COPY composer.lock . - -RUN composer install --no-autoloader - -COPY . . - -RUN composer dump-autoload -o - ENTRYPOINT ["tail"] CMD ["-f", "/dev/null"] diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index b5680459..2f7385ec 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -250,7 +250,7 @@ public function deleteQueryByCriteria(string $tableName, array $criteria): void public function lastInsertId(string $tableName): string { - return $this->getDbh()->lastInsertId(); + return (string)$this->getDbh()->lastInsertId(); } public function getQuotedName(string $name): string diff --git a/tests/data/dumps/mssql.sql b/tests/data/dumps/mssql.sql new file mode 100644 index 00000000..93fe7075 --- /dev/null +++ b/tests/data/dumps/mssql.sql @@ -0,0 +1,76 @@ +CREATE TABLE [dbo].[groups] ( + [id] INT NOT NULL IDENTITY(1,1), + [name] VARCHAR(100) NULL, + [enabled] BIT NULL, + [created_at] DATETIME NOT NULL CONSTRAINT DF_groups_created_at DEFAULT GETDATE(), + CONSTRAINT PK_groups PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[groups]([name],[enabled],[created_at]) +VALUES + ('coders', 1, '2012-02-01 21:17:50'), + ('jazzman', 0, '2012-02-01 21:18:40'); + + +CREATE TABLE [dbo].[users] ( + [id] INT NOT NULL IDENTITY(1,1), + [uuid] BINARY(16) NULL, + [name] VARCHAR(30) NULL, + [email] VARCHAR(255) NULL, + [is_active] BIT NOT NULL CONSTRAINT DF_users_is_active DEFAULT 1, + [created_at] DATETIME NOT NULL CONSTRAINT DF_users_created_at DEFAULT GETDATE(), + CONSTRAINT PK_users PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[users]([uuid],[name],[email],[is_active],[created_at]) +VALUES + (0x11edc34b01d972fa9c1d0242ac120006, 'davert', 'davert@mail.ua', 1, '2012-02-01 21:17:04'), + (null, 'nick', 'nick@mail.ua', 1, '2012-02-01 21:17:15'), + (null, 'miles', 'miles@davis.com', 1, '2012-02-01 21:17:25'), + (null, 'bird', 'charlie@parker.com', 0, '2012-02-01 21:17:39'); + + +CREATE TABLE [dbo].[permissions] ( + [id] INT NOT NULL IDENTITY(1,1), + [user_id] INT NULL, + [group_id] INT NULL, + [role] VARCHAR(30) NULL, + CONSTRAINT PK_permissions PRIMARY KEY CLUSTERED ([id] ASC), + CONSTRAINT FK_permissions FOREIGN KEY ([group_id]) REFERENCES [dbo].[groups] ([id]) ON DELETE CASCADE, + CONSTRAINT FK_users FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([id]) ON DELETE CASCADE +); + +INSERT INTO [dbo].[permissions]([user_id],[group_id],[role]) +VALUES + (1,1,'member'), + (2,1,'member'), + (3,2,'member'), + (4,2,'admin'); + + +CREATE TABLE [dbo].[order] ( + [id] INT NOT NULL IDENTITY(1,1), + [name] VARCHAR(255) NOT NULL, + [status] VARCHAR(255) NOT NULL, + CONSTRAINT PK_order PRIMARY KEY CLUSTERED ([id] ASC) +); + +INSERT INTO [dbo].[order]([name],[status]) VALUES ('main', 'open'); + + +CREATE TABLE [dbo].[composite_pk] ( + [group_id] INT NOT NULL, + [id] INT NOT NULL, + [status] VARCHAR(255) NOT NULL, + CONSTRAINT PK_composite_pk PRIMARY KEY CLUSTERED ([group_id] ASC, [id] ASC) +); + +CREATE TABLE [dbo].[no_pk] ( + [status] varchar(255) NOT NULL +); + +CREATE TABLE [dbo].[empty_table] ( + [id] int NOT NULL IDENTITY(1,1), + [field] varchar(255), + CONSTRAINT [PK_empty_table] PRIMARY KEY CLUSTERED ([id]) +); diff --git a/tests/data/scripts/mssql.sh b/tests/data/scripts/mssql.sh new file mode 100755 index 00000000..84434578 --- /dev/null +++ b/tests/data/scripts/mssql.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +/opt/mssql/bin/sqlservr & +/scripts/wait-for-it.sh 127.0.0.1:1433 + +for i in {1..50}; +do + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "CREATE DATABASE $MSSQL_DATABASE;" + if [ $? -eq 0 ] + then + echo "database created" + break + else + echo "not ready yet..." + sleep 1 + fi +done + +sleep infinity # Keep the container running forever diff --git a/tests/data/scripts/wait-for-it.sh b/tests/data/scripts/wait-for-it.sh new file mode 100644 index 00000000..44768829 --- /dev/null +++ b/tests/data/scripts/wait-for-it.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +#Source: https://github.com/vishnubob/wait-for-it/blob/81b1373f17855a4dc21156cfe1694c31d7d1792e/wait-for-it.sh + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/tests/unit/Codeception/Lib/Driver/MysqlTest.php b/tests/unit/Codeception/Lib/Driver/MysqlTest.php index eda908ae..94ea59f6 100644 --- a/tests/unit/Codeception/Lib/Driver/MysqlTest.php +++ b/tests/unit/Codeception/Lib/Driver/MysqlTest.php @@ -25,9 +25,15 @@ final class MysqlTest extends Unit public static function _setUpBeforeClass() { - $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; - self::$config['dsn'] = 'mysql:host='.$host.';dbname=codeception_test'; - self::$config['password'] = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; + $host = getenv('MYSQL_HOST') ?: 'localhost'; + $user = getenv('MYSQL_USER') ?: 'root'; + $password = getenv('MYSQL_PASSWORD') ?: ''; + $database = getenv('MYSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MYSQL_DSN') ?: 'mysql:host=' . $host . ';dbname=' . $database; + + self::$config['dsn'] = $dsn; + self::$config['user'] = $user; + self::$config['password'] = $password; $sql = file_get_contents(\Codeception\Configuration::dataDir() . '/dumps/mysql.sql'); $sql = preg_replace('#/\*(?:(?!\*/).)*\*/#s', "", $sql); diff --git a/tests/unit/Codeception/Lib/Driver/PostgresTest.php b/tests/unit/Codeception/Lib/Driver/PostgresTest.php index 7e66826f..b0982021 100644 --- a/tests/unit/Codeception/Lib/Driver/PostgresTest.php +++ b/tests/unit/Codeception/Lib/Driver/PostgresTest.php @@ -27,10 +27,26 @@ public static function _setUpBeforeClass() if (!function_exists('pg_connect')) { return; } - self::$config['password'] = getenv('PGPASSWORD') ? getenv('PGPASSWORD') : null; - $sql = file_get_contents(codecept_data_dir('dumps/postgres.sql')); + + $host = getenv('PG_HOST') ?: 'localhost'; + $user = getenv('PG_USER') ?: 'postgres'; + $password = getenv('PG_PASSWORD') ?: null; + $database = getenv('PG_DB') ?: 'codeception_test'; + $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; + + self::$config['dsn'] = $dsn; + self::$config['user'] = $user; + self::$config['password'] = $password; + + $sql = file_get_contents(\Codeception\Configuration::dataDir() . '/dumps/postgres.sql'); $sql = preg_replace('#/\*(?:(?!\*/).)*\*/#s', '', $sql); self::$sql = explode("\n", $sql); + + try { + $postgres = Db::create(self::$config['dsn'], self::$config['user'], self::$config['password']); + $postgres->cleanup(); + } catch (Exception $e) { + } } public function _setUp() diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index f880866b..81ad0942 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -66,6 +66,13 @@ public function testConnectionIsKeptForTheWholeSuite() public function testSeeInDatabaseWithBinary() { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + $this->module->seeInDatabase('users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); } @@ -76,18 +83,40 @@ public function testSeeInDatabase() public function testCountInDatabase() { - $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); $this->module->seeNumRecords(1, 'users', ['name' => 'davert']); $this->module->seeNumRecords(0, 'users', ['name' => 'davert', 'email' => 'xxx@yyy.zz']); $this->module->seeNumRecords(0, 'users', ['name' => 'user1']); } + public function testCountInDatabaseWithBinary() + { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + + $this->module->seeNumRecords(1, 'users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); + } + public function testDontSeeInDatabase() { - $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); $this->module->dontSeeInDatabase('users', ['name' => 'user1']); } + public function testDontSeeInDatabaseWithBinary() + { + if ( + $this instanceof MssqlSqlSrvDbTest + || $this instanceof MssqlDblibDbTest + ) { + $this->markTestSkipped('Filter to binary field does not supported by SqlSrv driver'); + } + + $this->module->dontSeeInDatabase('users', ['uuid' => hex2bin('ffffffffffffffffffffffffffffffff')]); + } + public function testDontSeeInDatabaseWithEmptyTable() { $this->module->dontSeeInDatabase('empty_table'); diff --git a/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php new file mode 100644 index 00000000..e9b46fff --- /dev/null +++ b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php @@ -0,0 +1,39 @@ +getConfig(); + + return sprintf('/opt/mssql-tools/bin/sqlcmd -S $host -U $user -P $password -d $dbname -i %s', $config['dump']); + } + + public function getConfig(): array + { + $host = getenv('MSSQL_HOST') ?: 'localhost'; + $user = getenv('MSSQL_USER') ?: 'sa'; + $password = getenv('MSSQL_PASSWORD') ?: ''; + $database = getenv('MSSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MSSQL_DSN') ?: 'dblib:host=' . $host . ';dbname=' . $database; + + return [ + 'dsn' => $dsn, + 'user' => $user, + 'password' => $password, + 'dump' => 'tests/data/dumps/mssql.sql', + 'reconnect' => true, + 'cleanup' => true, + 'populate' => true, + ]; + } +} diff --git a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php new file mode 100644 index 00000000..58776042 --- /dev/null +++ b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php @@ -0,0 +1,39 @@ +getConfig(); + + return sprintf('/opt/mssql-tools/bin/sqlcmd -S $Server -U $user -P $password -d $Database -i %s', $config['dump']); + } + + public function getConfig(): array + { + $host = getenv('MSSQL_HOST') ?: 'localhost'; + $user = getenv('MSSQL_USER') ?: 'sa'; + $password = getenv('MSSQL_PASSWORD') ?: ''; + $database = getenv('MSSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MSSQL_DSN') ?: 'sqlsrv:Server=' . $host . ';Database=' . $database; + + return [ + 'dsn' => $dsn, + 'user' => $user, + 'password' => $password, + 'dump' => 'tests/data/dumps/mssql.sql', + 'reconnect' => true, + 'cleanup' => true, + 'populate' => true, + ]; + } +} diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 058aa4f5..11dcfee0 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -22,10 +22,11 @@ public function getPopulator(): string public function getConfig(): array { - $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost'; - $user = getenv('MYSQL_USER') ? getenv('MYSQL_USER') : 'root'; - $password = getenv('MYSQL_PASSWORD') ? getenv('MYSQL_PASSWORD') : ''; - $dsn = getenv('MYSQL_DSN') ? getenv('MYSQL_DSN') : 'mysql:host='.$host.';dbname=codeception_test'; + $host = getenv('MYSQL_HOST') ?: 'localhost'; + $user = getenv('MYSQL_USER') ?: 'root'; + $password = getenv('MYSQL_PASSWORD') ?: ''; + $database = getenv('MYSQL_DB') ?: 'codeception_test'; + $dsn = getenv('MYSQL_DSN') ?: 'mysql:host=' . $host . ';dbname=' . $database; return [ 'dsn' => $dsn, diff --git a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php index fbdae270..9ebf1dd3 100644 --- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php @@ -13,7 +13,9 @@ final class PostgreSqlDbTest extends AbstractDbTest { public function getPopulator(): string { - return "psql -h localhost -d codeception_test -U postgres < tests/data/dumps/postgres.sql"; + $config = $this->getConfig(); + + return sprintf('psql -h $host -d $dbname -U $user < %s', $config['dump']); } public function getConfig(): array @@ -22,11 +24,15 @@ public function getConfig(): array $this->markTestSkipped(); } - $password = getenv('PGPASSWORD') ? getenv('PGPASSWORD') : null; + $host = getenv('PG_HOST') ?: 'localhost'; + $user = getenv('PG_USER') ?: 'postgres'; + $password = getenv('PG_PASSWORD') ?: null; + $database = getenv('PG_DB') ?: 'codeception_test'; + $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; return [ - 'dsn' => 'pgsql:host=localhost;dbname=codeception_test', - 'user' => 'postgres', + 'dsn' => $dsn, + 'user' => $user, 'password' => $password, 'dump' => 'tests/data/dumps/postgres.sql', 'reconnect' => true, From a00cb7b3cac7fdd179e7ef978c45be48cfb4cf1f Mon Sep 17 00:00:00 2001 From: Worma Date: Fri, 1 Dec 2023 15:30:23 +0100 Subject: [PATCH 11/25] Use proper parameter type for NULL values --- src/Codeception/Lib/Driver/Db.php | 4 +++- tests/unit/Codeception/Module/Db/AbstractDbTest.php | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index b5680459..ae467496 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -290,7 +290,9 @@ public function executeQuery($query, array $params): PDOStatement $i = 0; foreach ($params as $param) { ++$i; - if (is_bool($param)) { + if (is_null($param)) { + $type = PDO::PARAM_NULL; + } elseif (is_bool($param)) { $type = PDO::PARAM_BOOL; } elseif (is_int($param)) { $type = PDO::PARAM_INT; diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index f880866b..9bd1e4b2 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -69,6 +69,11 @@ public function testSeeInDatabaseWithBinary() $this->module->seeInDatabase('users', ['uuid' => hex2bin('11edc34b01d972fa9c1d0242ac120006')]); } + public function testSeeInDatabaseWithNull() + { + $this->module->seeInDatabase('users', ['uuid' => null]); + } + public function testSeeInDatabase() { $this->module->seeInDatabase('users', ['name' => 'davert']); From b7c45db45457e5519b68463fc79e6e96b9113e13 Mon Sep 17 00:00:00 2001 From: Worma Date: Fri, 1 Dec 2023 15:30:30 +0100 Subject: [PATCH 12/25] Add direct dependency for mbstring extension because this package uses mb_detect_encoding() --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 2a777a15..26c633a0 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "require": { "php": "^8.0", "ext-json": "*", + "ext-mbstring": "*", "ext-pdo": "*", "codeception/codeception": "*@dev" }, From 5666795f745ba0cd758ee0fe2744ee0209c451bf Mon Sep 17 00:00:00 2001 From: Worma Date: Fri, 1 Dec 2023 16:48:27 +0100 Subject: [PATCH 13/25] Call isBinary() only if parameter is a string --- src/Codeception/Lib/Driver/Db.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index ae467496..e83df570 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -296,7 +296,7 @@ public function executeQuery($query, array $params): PDOStatement $type = PDO::PARAM_BOOL; } elseif (is_int($param)) { $type = PDO::PARAM_INT; - } elseif ($this->isBinary($param)) { + } elseif (is_string($param) && $this->isBinary($param)) { $type = PDO::PARAM_LOB; } else { $type = PDO::PARAM_STR; From ad61407a502b1d1c77305ea38b191c5548179000 Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Fri, 10 May 2024 23:45:04 +0200 Subject: [PATCH 14/25] Support ODBC 18 in tests (#66) * feat(mssql): add odbc 18 support in mssql tests * feat(test): add documentation to run tests locally + Apple Silicon support --- Makefile | 40 ++++++++++++++ docker-compose.amd64.yml | 17 ++++++ docker-compose.yml | 10 ++-- php81.Dockerfile | 4 +- tests/README.md | 54 +++++++++++++++++++ .../Module/Db/MssqlSqlSrvDbTest.php | 2 +- 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 Makefile create mode 100644 docker-compose.amd64.yml create mode 100644 tests/README.md diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5d5cdee6 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: $(filter-out help, $(MAKECMDGOALS)) +.DEFAULT_GOAL := help + +DOCKER_COMPOSE := $(if $(shell command -v docker-compose 2> /dev/null),docker-compose,docker compose) -f docker-compose.yml +ARCH := $(shell uname -m) +CURRENT_USER := $(shell id -u):$(shell id -g) + +# Check if docker-compose.override.yml exists and if so, add it to DOCKER_COMPOSE +ifneq (,$(wildcard ./docker-compose.override.yml)) + DOCKER_COMPOSE += -f docker-compose.override.yml +endif + +# Support Apple Silicon +ifeq ($(ARCH),arm64) + DOCKER_COMPOSE += -f docker-compose.amd64.yml +endif + +DOCKER_EXEC_PHP_WITH_USER = $(DOCKER_COMPOSE) exec -u $(CURRENT_USER) php bash -c + +help: ## Show this help message + @echo "\033[33mUsage:\033[0m\n make [target] [arg=\"val\"...]\n\n\033[33mTargets:\033[0m" + @grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[32m%-25s\033[0m %s\n", $$1, $$2}' + +start: ## Start the containers for testing + $(MAKE) -i stop + CURRENT_USER=$(CURRENT_USER) $(DOCKER_COMPOSE) up -d --build --force-recreate --remove-orphans + $(DOCKER_COMPOSE) run --rm wait -c mysql:3306,postgres:5432,mssql:1433 -t 60 + $(MAKE) vendor + +stop: ## Stop and remove containers + $(DOCKER_COMPOSE) down --remove-orphans --volumes + +php-cli: ## Open bash in PHP container + $(DOCKER_COMPOSE) exec -u $(CURRENT_USER) php bash + +vendor: ## Install dependencies + $(DOCKER_EXEC_PHP_WITH_USER) "composer install --no-interaction --prefer-dist" + +test: ## Run the tests + $(DOCKER_EXEC_PHP_WITH_USER) "php vendor/bin/codecept run" diff --git a/docker-compose.amd64.yml b/docker-compose.amd64.yml new file mode 100644 index 00000000..3af4ab83 --- /dev/null +++ b/docker-compose.amd64.yml @@ -0,0 +1,17 @@ +version: '3.9' + +services: + php: + platform: linux/amd64 + + mysql: + platform: linux/amd64 + + postgres: + platform: linux/amd64 + + mssql: + platform: linux/amd64 + + wait: + platform: linux/amd64 diff --git a/docker-compose.yml b/docker-compose.yml index 21e6d5dc..5bb5a742 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,13 @@ version: "3.9" services: - php81: - image: codeception-module-db-php81:2.2.0 + php: + container_name: codeception-module-db build: context: . dockerfile: ./php81.Dockerfile environment: + COMPOSER_HOME: /tmp/.composer MYSQL_HOST: host.docker.internal MYSQL_DB: codeception MYSQL_PASSWORD: codeception @@ -20,7 +21,8 @@ services: XDEBUG_CONFIG: "client_host=host.docker.internal; client_port=9000; mode=debug; start_wih_request=1" PHP_IDE_CONFIG: "serverName=codeception-module-db" # the name must be the same as in your PHP -> Server -> "name" field volumes: - - ".:/var/www/html" + - ${HOME}/.composer:/tmp/.composer + - .:/var/www/html mysql: image: mysql:5.7 @@ -50,3 +52,5 @@ services: - ./tests/data/scripts:/scripts:ro entrypoint: [ "/bin/bash", "-c", "/scripts/mssql.sh" ] + wait: + image: dokku/wait diff --git a/php81.Dockerfile b/php81.Dockerfile index ca24ccde..440dfa19 100644 --- a/php81.Dockerfile +++ b/php81.Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.1-cli +FROM php:8.1-fpm COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ @@ -10,7 +10,7 @@ RUN apt-get update && \ zlib1g-dev \ libzip-dev \ libpq-dev \ - mariadb-client-10.5 + default-mysql-client RUN install-php-extensions \ pdo_mysql-stable \ diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..abab0b5d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,54 @@ +# Local Test Environment + +## Prerequisites + +- Docker +- Docker Compose +- Make + +## Setup + +1. Clone the repository to your local machine. +2. Navigate to the project directory. + +## Running the Docker Environment + +To start the Docker environment, use the following command: + +```bash +make start +``` + +This command will start all the necessary containers for the application. It will also build the Docker images if they are not already built. + +## Running Tests + +To run the tests, use the following command: + +```bash +make test +``` + +This command will execute the tests inside the PHP container. + +## Other Commands + +- To stop and remove the Docker containers, use the following command: + +```bash +make stop +``` + +- To open a bash shell inside the PHP container, use the following command: + +```bash +make php-cli +``` + +- To install the dependencies, use the following command: + +```bash +make vendor +``` + +Please note that all these commands should be run from the root directory of the project where the `Makefile` is located. diff --git a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php index 58776042..b758a26b 100644 --- a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php +++ b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php @@ -24,7 +24,7 @@ public function getConfig(): array $user = getenv('MSSQL_USER') ?: 'sa'; $password = getenv('MSSQL_PASSWORD') ?: ''; $database = getenv('MSSQL_DB') ?: 'codeception_test'; - $dsn = getenv('MSSQL_DSN') ?: 'sqlsrv:Server=' . $host . ';Database=' . $database; + $dsn = getenv('MSSQL_DSN') ?: 'sqlsrv:Server=' . $host . ';Database=' . $database . ';Encrypt=no;TrustServerCertificate=yes'; return [ 'dsn' => $dsn, From 06be16dcf4dda46eaef9454f1361d62bcb971c36 Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Thu, 16 May 2024 22:12:18 +0200 Subject: [PATCH 15/25] Validate PSR12 codestyle with PHPCS #69 (#70) --- .github/workflows/main.yml | 23 +++++++++++- composer.json | 14 ++++++++ docker-compose.amd64.yml | 2 -- docker-compose.yml | 2 -- phpcs.xml | 8 +++++ src/Codeception/Lib/DbPopulator.php | 2 +- src/Codeception/Lib/Driver/MySql.php | 2 +- src/Codeception/Lib/Driver/Oci.php | 2 +- src/Codeception/Lib/Driver/PostgreSql.php | 2 +- src/Codeception/Lib/Driver/SqlSrv.php | 2 +- src/Codeception/Lib/Driver/Sqlite.php | 6 ++-- src/Codeception/Module/Db.php | 19 ++++++---- tests/phpcs.xml | 9 +++++ tests/support/UnitTester.php | 9 +++-- tests/unit/Codeception/Lib/Driver/DbTest.php | 22 +++++------- .../unit/Codeception/Lib/Driver/MysqlTest.php | 11 ++---- .../Codeception/Lib/Driver/PostgresTest.php | 6 +--- .../Codeception/Lib/Driver/SqliteTest.php | 4 --- .../Module/Db/MssqlDblibDbTest.php | 7 ---- .../Module/Db/MssqlSqlSrvDbTest.php | 7 ---- .../Codeception/Module/Db/MySqlDbTest.php | 11 ++---- .../Module/Db/Populator/DbPopulatorTest.php | 4 --- .../Module/Db/PostgreSqlDbTest.php | 7 ---- .../Codeception/Module/Db/SqliteDbTest.php | 36 +++++++------------ 24 files changed, 104 insertions(+), 113 deletions(-) create mode 100644 phpcs.xml create mode 100644 tests/phpcs.xml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce2a2089..730b33ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,28 @@ name: CI on: [push, pull_request] jobs: + phpcs: + name: Code style + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: phpcs + + - name: Check production code style + run: composer cs-prod + + - name: Check test code style + run: composer cs-tests + tests: + name: Unit tests runs-on: ubuntu-latest services: @@ -53,7 +74,7 @@ jobs: run: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -Q 'CREATE DATABASE codeception_test' - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/composer.json b/composer.json index 26c633a0..77eefd0e 100644 --- a/composer.json +++ b/composer.json @@ -21,12 +21,26 @@ "ext-pdo": "*", "codeception/codeception": "*@dev" }, + "require-dev": { + "squizlabs/php_codesniffer": "*" + }, "conflict": { "codeception/codeception": "<5.0" }, "autoload":{ "classmap": ["src/"] }, + "autoload-dev": { + "classmap": ["tests/"] + }, + "scripts": { + "cs-prod": "phpcs src/", + "cs-tests": "phpcs tests/ --standard=tests/phpcs.xml" + }, + "scripts-descriptions": { + "cs-prod": "Check production code style", + "cs-tests": "Check test code style" + }, "config": { "classmap-authoritative": true, "sort-packages": true diff --git a/docker-compose.amd64.yml b/docker-compose.amd64.yml index 3af4ab83..3fb698ba 100644 --- a/docker-compose.amd64.yml +++ b/docker-compose.amd64.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: php: platform: linux/amd64 diff --git a/docker-compose.yml b/docker-compose.yml index 5bb5a742..d2052501 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: php: container_name: codeception-module-db diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..0fd59960 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,8 @@ + + + Codeception code standard + + + + + diff --git a/src/Codeception/Lib/DbPopulator.php b/src/Codeception/Lib/DbPopulator.php index f57a8cd0..59d9b23b 100644 --- a/src/Codeception/Lib/DbPopulator.php +++ b/src/Codeception/Lib/DbPopulator.php @@ -74,7 +74,7 @@ protected function buildCommand(string $command, string $dumpFile = null): strin foreach ($vars as $key => $value) { if (!is_array($value)) { - $vars['$'.$key] = $value; + $vars['$' . $key] = $value; } unset($vars[$key]); diff --git a/src/Codeception/Lib/Driver/MySql.php b/src/Codeception/Lib/Driver/MySql.php index c2baa1ff..6fd79e2e 100644 --- a/src/Codeception/Lib/Driver/MySql.php +++ b/src/Codeception/Lib/Driver/MySql.php @@ -43,7 +43,7 @@ public function getPrimaryKey(string $tableName): array $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($columns as $column) { - $primaryKey []= $column['Column_name']; + $primaryKey[] = $column['Column_name']; } $this->primaryKeys[$tableName] = $primaryKey; } diff --git a/src/Codeception/Lib/Driver/Oci.php b/src/Codeception/Lib/Driver/Oci.php index 7d3c58a5..54bf2c58 100644 --- a/src/Codeception/Lib/Driver/Oci.php +++ b/src/Codeception/Lib/Driver/Oci.php @@ -104,7 +104,7 @@ public function getPrimaryKey(string $tableName): array $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { - $primaryKey []= $column['COLUMN_NAME']; + $primaryKey[] = $column['COLUMN_NAME']; } $this->primaryKeys[$tableName] = $primaryKey; diff --git a/src/Codeception/Lib/Driver/PostgreSql.php b/src/Codeception/Lib/Driver/PostgreSql.php index 3f7336d5..45bed775 100644 --- a/src/Codeception/Lib/Driver/PostgreSql.php +++ b/src/Codeception/Lib/Driver/PostgreSql.php @@ -169,7 +169,7 @@ public function getPrimaryKey(string $tableName): array $stmt = $this->executeQuery($query, []); $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($columns as $column) { - $primaryKey []= $column['attname']; + $primaryKey[] = $column['attname']; } $this->primaryKeys[$tableName] = $primaryKey; diff --git a/src/Codeception/Lib/Driver/SqlSrv.php b/src/Codeception/Lib/Driver/SqlSrv.php index f742fe41..d1826b1e 100644 --- a/src/Codeception/Lib/Driver/SqlSrv.php +++ b/src/Codeception/Lib/Driver/SqlSrv.php @@ -77,7 +77,7 @@ public function getPrimaryKey(string $tableName): array $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($columns as $column) { - $primaryKey []= $column['Column_Name']; + $primaryKey[] = $column['Column_Name']; } $this->primaryKeys[$tableName] = $primaryKey; diff --git a/src/Codeception/Lib/Driver/Sqlite.php b/src/Codeception/Lib/Driver/Sqlite.php index 242d9306..f480c76d 100644 --- a/src/Codeception/Lib/Driver/Sqlite.php +++ b/src/Codeception/Lib/Driver/Sqlite.php @@ -11,7 +11,7 @@ class Sqlite extends Db { protected bool $hasSnapshot = false; - + protected string $filename = ''; public function __construct(string $dsn, string $user = null, string $password = null, array $options = null) @@ -47,7 +47,7 @@ public function load(array $sql): void if (file_exists($this->filename . '_snapshot')) { unlink($this->filename . '_snapshot'); } - + parent::load($sql); copy($this->filename, $this->filename . '_snapshot'); $this->hasSnapshot = true; @@ -71,7 +71,7 @@ public function getPrimaryKey(string $tableName): array foreach ($columns as $column) { if ($column['pk'] !== '0' && $column['pk'] !== 0) { - $primaryKey []= $column['name']; + $primaryKey[] = $column['name']; } } diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index db418bd3..a27299f2 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -565,38 +565,43 @@ private function connect($databaseKey, $databaseConfig): void $options = []; - if (array_key_exists('ssl_key', $databaseConfig) + if ( + array_key_exists('ssl_key', $databaseConfig) && !empty($databaseConfig['ssl_key']) && defined(PDO::class . '::MYSQL_ATTR_SSL_KEY') ) { $options[PDO::MYSQL_ATTR_SSL_KEY] = (string) $databaseConfig['ssl_key']; } - if (array_key_exists('ssl_cert', $databaseConfig) + if ( + array_key_exists('ssl_cert', $databaseConfig) && !empty($databaseConfig['ssl_cert']) && defined(PDO::class . '::MYSQL_ATTR_SSL_CERT') ) { $options[PDO::MYSQL_ATTR_SSL_CERT] = (string) $databaseConfig['ssl_cert']; } - if (array_key_exists('ssl_ca', $databaseConfig) + if ( + array_key_exists('ssl_ca', $databaseConfig) && !empty($databaseConfig['ssl_ca']) && defined(PDO::class . '::MYSQL_ATTR_SSL_CA') ) { $options[PDO::MYSQL_ATTR_SSL_CA] = (string) $databaseConfig['ssl_ca']; } - if (array_key_exists('ssl_cipher', $databaseConfig) + if ( + array_key_exists('ssl_cipher', $databaseConfig) && !empty($databaseConfig['ssl_cipher']) && defined(PDO::class . '::MYSQL_ATTR_SSL_CIPHER') ) { $options[PDO::MYSQL_ATTR_SSL_CIPHER] = (string) $databaseConfig['ssl_cipher']; } - if (array_key_exists('ssl_verify_server_cert', $databaseConfig) + if ( + array_key_exists('ssl_verify_server_cert', $databaseConfig) && defined(PDO::class . '::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') ) { - $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (boolean) $databaseConfig[ 'ssl_verify_server_cert' ]; + $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $databaseConfig[ 'ssl_verify_server_cert' ]; } try { @@ -672,7 +677,7 @@ protected function removeInserted($databaseKey = null): void try { $this->_getDriver()->deleteQueryByCriteria($row['table'], $row['primary']); } catch (Exception $e) { - $this->debug("Couldn't delete record " . json_encode($row['primary'], JSON_THROW_ON_ERROR) ." from {$row['table']}"); + $this->debug("Couldn't delete record " . json_encode($row['primary'], JSON_THROW_ON_ERROR) . " from {$row['table']}"); } } diff --git a/tests/phpcs.xml b/tests/phpcs.xml new file mode 100644 index 00000000..df0ae9c7 --- /dev/null +++ b/tests/phpcs.xml @@ -0,0 +1,9 @@ + + + Codeception code standard + + + + + + diff --git a/tests/support/UnitTester.php b/tests/support/UnitTester.php index e19544a5..b3472f63 100644 --- a/tests/support/UnitTester.php +++ b/tests/support/UnitTester.php @@ -1,6 +1,5 @@ assertSame($expectedResult, $result); } @@ -25,14 +21,14 @@ public function testGenerateWhereClause(array $criteria, string $expectedResult) public function getWhereCriteria(): array { return [ - 'like' => [['email like' => 'mail.ua'], 'WHERE "email" LIKE ? '], - '<=' => [['id <=' => '5'], 'WHERE "id" <= ? '], - '<' => [['id <' => '5'], 'WHERE "id" < ? '], - '>=' => [['id >=' => '5'], 'WHERE "id" >= ? '], - '>' => [['id >' => '5'], 'WHERE "id" > ? '], - '!=' => [['id !=' => '5'], 'WHERE "id" != ? '], - 'is null' => [['id' => null], 'WHERE "id" IS NULL '], - 'is not null' => [['id !=' => null], 'WHERE "id" IS NOT NULL '], + 'like' => [['email like' => 'mail.ua'], 'WHERE "email" LIKE ? '], + '<=' => [['id <=' => '5'], 'WHERE "id" <= ? '], + '<' => [['id <' => '5'], 'WHERE "id" < ? '], + '>=' => [['id >=' => '5'], 'WHERE "id" >= ? '], + '>' => [['id >' => '5'], 'WHERE "id" > ? '], + '!=' => [['id !=' => '5'], 'WHERE "id" != ? '], + 'is null' => [['id' => null], 'WHERE "id" IS NULL '], + 'is not null' => [['id !=' => null], 'WHERE "id" IS NOT NULL '], ]; } } diff --git a/tests/unit/Codeception/Lib/Driver/MysqlTest.php b/tests/unit/Codeception/Lib/Driver/MysqlTest.php index 94ea59f6..205ed230 100644 --- a/tests/unit/Codeception/Lib/Driver/MysqlTest.php +++ b/tests/unit/Codeception/Lib/Driver/MysqlTest.php @@ -7,10 +7,6 @@ use Codeception\Lib\Driver\MySql; use Codeception\Test\Unit; -/** - * @group appveyor - * @group db - */ final class MysqlTest extends Unit { protected static array $config = [ @@ -69,9 +65,6 @@ public function testCleanupDatabase() $this->assertEmpty($this->mysql->getDbh()->query("SHOW TABLES")->fetchAll()); } - /** - * @group appveyor - */ public function testLoadDump() { $res = $this->mysql->getDbh()->query("select * from users where name = 'davert'"); @@ -108,7 +101,7 @@ public function testInsertIntoBitField() { $res = $this->mysql->executeQuery( "insert into `users`(`id`,`name`,`email`,`is_active`,`created_at`) values (?,?,?,?,?)", - [5,'insert.test','insert.test@mail.ua',false,'2012-02-01 21:17:47'] + [5, 'insert.test', 'insert.test@mail.ua', false, '2012-02-01 21:17:47'] ); $this->assertSame(1, $res->rowCount()); } @@ -123,7 +116,7 @@ public function testLoadThrowsExceptionWhenDumpFileContainsSyntaxError() 'check the manual that corresponds to your MySQL server version for the right syntax to use near ' . "'VALS('')' at line 1\nSQL query being executed: \n" . $sql; $this->expectException(ModuleException::class); - $this->expectExceptionMessage( $expectedMessage); + $this->expectExceptionMessage($expectedMessage); $this->mysql->load([$sql]); } } diff --git a/tests/unit/Codeception/Lib/Driver/PostgresTest.php b/tests/unit/Codeception/Lib/Driver/PostgresTest.php index b0982021..8c15e863 100644 --- a/tests/unit/Codeception/Lib/Driver/PostgresTest.php +++ b/tests/unit/Codeception/Lib/Driver/PostgresTest.php @@ -6,10 +6,6 @@ use Codeception\Lib\Driver\PostgreSql; use Codeception\Test\Unit; -/** - * @group appveyor - * @group db - */ final class PostgresTest extends Unit { protected static array $config = [ @@ -141,7 +137,7 @@ public function testGetEmptyArrayIfTableHasNoPrimaryKey() public function testLastInsertIdReturnsSequenceValueWhenNonStandardSequenceNameIsUsed() { - $this->postgres->executeQuery('INSERT INTO seqnames(name) VALUES(?)',['test']); + $this->postgres->executeQuery('INSERT INTO seqnames(name) VALUES(?)', ['test']); $this->assertSame('1', $this->postgres->lastInsertId('seqnames')); } diff --git a/tests/unit/Codeception/Lib/Driver/SqliteTest.php b/tests/unit/Codeception/Lib/Driver/SqliteTest.php index e04dda80..680ea0bb 100644 --- a/tests/unit/Codeception/Lib/Driver/SqliteTest.php +++ b/tests/unit/Codeception/Lib/Driver/SqliteTest.php @@ -7,10 +7,6 @@ use Codeception\Lib\Driver\Sqlite; use Codeception\Test\Unit; -/** - * @group db - * Class SqliteTest - */ final class SqliteTest extends Unit { /** diff --git a/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php index e9b46fff..5b4a4ccd 100644 --- a/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php +++ b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php @@ -2,13 +2,6 @@ declare(strict_types=1); -use Codeception\Configuration; - -require_once Configuration::testsDir() . 'unit/Codeception/Module/Db/AbstractDbTest.php'; - -/** - * @group db - */ final class MssqlDblibDbTest extends AbstractDbTest { public function getPopulator(): string diff --git a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php index b758a26b..b54641e8 100644 --- a/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php +++ b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php @@ -2,13 +2,6 @@ declare(strict_types=1); -use Codeception\Configuration; - -require_once Configuration::testsDir() . 'unit/Codeception/Module/Db/AbstractDbTest.php'; - -/** - * @group db - */ final class MssqlSqlSrvDbTest extends AbstractDbTest { public function getPopulator(): string diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 11dcfee0..8bc10015 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -2,21 +2,15 @@ declare(strict_types=1); -use Codeception\Configuration; use Codeception\Stub; use Codeception\TestInterface; -require_once Configuration::testsDir().'unit/Codeception/Module/Db/AbstractDbTest.php'; - -/** - * @group db - */ final class MySqlDbTest extends AbstractDbTest { public function getPopulator(): string { $config = $this->getConfig(); - $password = $config['password'] ? '-p'.$config['password'] : ''; + $password = $config['password'] ? '-p' . $config['password'] : ''; return sprintf('mysql -u $user %s $dbname < %s', $password, $config['dump']); } @@ -103,7 +97,8 @@ public function testGrabColumnFromDatabase() 'miles@davis.com', 'charlie@parker.com', ], - $emails); + $emails + ); } public function testGrabEntryFromDatabaseShouldFailIfNotFound() diff --git a/tests/unit/Codeception/Module/Db/Populator/DbPopulatorTest.php b/tests/unit/Codeception/Module/Db/Populator/DbPopulatorTest.php index eb703f48..6490bf5c 100644 --- a/tests/unit/Codeception/Module/Db/Populator/DbPopulatorTest.php +++ b/tests/unit/Codeception/Module/Db/Populator/DbPopulatorTest.php @@ -5,10 +5,6 @@ use Codeception\Lib\DbPopulator; use Codeception\Test\Unit; -/** - * @group db - * Class DbPopulatorTest - */ final class DbPopulatorTest extends Unit { public function testCommandBuilderInterpolatesVariables() diff --git a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php index 9ebf1dd3..0f2c4900 100644 --- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php @@ -2,13 +2,6 @@ declare(strict_types=1); -use Codeception\Configuration; - -require_once Configuration::testsDir().'unit/Codeception/Module/Db/AbstractDbTest.php'; - -/** - * @group db - */ final class PostgreSqlDbTest extends AbstractDbTest { public function getPopulator(): string diff --git a/tests/unit/Codeception/Module/Db/SqliteDbTest.php b/tests/unit/Codeception/Module/Db/SqliteDbTest.php index 82871f49..cdbeb97d 100644 --- a/tests/unit/Codeception/Module/Db/SqliteDbTest.php +++ b/tests/unit/Codeception/Module/Db/SqliteDbTest.php @@ -2,31 +2,17 @@ declare(strict_types=1); -use Codeception\Configuration; use Codeception\Stub; use Codeception\TestInterface; use Codeception\Util\ActionSequence; -require_once Configuration::testsDir().'unit/Codeception/Module/Db/AbstractDbTest.php'; - -/** - * @group appveyor - * @group db - * Class SqliteDbTest - */ final class SqliteDbTest extends AbstractDbTest { public function getPopulator() { - if (getenv('APPVEYOR')) { - $this->markTestSkipped('Disabled on Appveyor'); - } - - $this->markTestSkipped('Currently Travis CI uses old SQLite :('); - $config = $this->getConfig(); @chmod('tests/data/sqlite.db', 0777); - return 'cat '. $config['dump'] .' | sqlite3 tests/data/sqlite.db'; + return 'cat ' . $config['dump'] . ' | sqlite3 tests/data/sqlite.db'; } public function getConfig(): array @@ -83,7 +69,7 @@ public function testMultiDatabase() ]); $this->module->_reconfigure( [ - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); @@ -107,7 +93,7 @@ public function testDatabaseIsAlwaysDefaultBeforeTest() $this->module->_reconfigure( [ 'cleanup' => false, - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); @@ -142,7 +128,7 @@ public function testMultiDatabaseWithArray() ]); $this->module->_reconfigure( [ - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); @@ -167,7 +153,7 @@ public function testMultiDatabaseWithActionSequence() ]); $this->module->_reconfigure( [ - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); @@ -176,9 +162,11 @@ public function testMultiDatabaseWithActionSequence() $testDataInDb2 = ['name' => 'userdb2', 'email' => 'userdb2@example.org']; $this->module->_insertInDatabase('users', $testDataInDb1); - $this->module->performInDatabase('db2', ActionSequence::build() - ->haveInDatabase('users', $testDataInDb2) - ->seeInDatabase('users', $testDataInDb2) + $this->module->performInDatabase( + 'db2', + ActionSequence::build() + ->haveInDatabase('users', $testDataInDb2) + ->seeInDatabase('users', $testDataInDb2) ); $this->module->seeInDatabase('users', $testDataInDb1); $this->module->dontSeeInDatabase('users', $testDataInDb2); @@ -192,7 +180,7 @@ public function testMultiDatabaseWithAnonymousFunction() ]); $this->module->_reconfigure( [ - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); @@ -225,7 +213,7 @@ public function testMultiDatabaseWithRemoveInserted() ]); $this->module->_reconfigure( [ - 'databases' => ['db2' => $config], + 'databases' => ['db2' => $config], ] ); $this->module->_beforeSuite(); From 69a88c5414f66a67c7f9f5405e5a24e5711cd1e1 Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Sun, 19 May 2024 17:21:19 +0200 Subject: [PATCH 16/25] Add explicit support for PHP 8.3 in test pipeline (#71) --- .gitattributes | 5 ++-- .github/workflows/main.yml | 2 +- Makefile | 15 ++++++++-- docker-compose.yml | 3 +- php81.Dockerfile => docker/php8.0/Dockerfile | 4 +-- docker/php8.1/Dockerfile | 29 +++++++++++++++++++ docker/php8.2/Dockerfile | 29 +++++++++++++++++++ docker/php8.3/Dockerfile | 29 +++++++++++++++++++ tests/README.md | 18 ++++++++++++ tests/data/sqlite.db | Bin 10 files changed, 125 insertions(+), 9 deletions(-) rename php81.Dockerfile => docker/php8.0/Dockerfile (87%) create mode 100644 docker/php8.1/Dockerfile create mode 100644 docker/php8.2/Dockerfile create mode 100644 docker/php8.3/Dockerfile mode change 100644 => 100755 tests/data/sqlite.db diff --git a/.gitattributes b/.gitattributes index 6e7735a0..4a905c87 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,9 @@ # Exclude files that don't need to be present in packages (so they're not downloaded by Composer) +/docker export-ignore /tests export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/Robofile.php export-ignore +/Makefile export-ignore /*.md export-ignore +/*.xml export-ignore /*.yml export-ignore -/*.Dockerfile export-ignore diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 730b33ef..c1e6b1c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: strategy: matrix: - php: [8.0, 8.1, 8.2] + php: [8.0, 8.1, 8.2, 8.3] steps: - name: Create default database for sqlsrv as image does not support it diff --git a/Makefile b/Makefile index 5d5cdee6..5397eed2 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,6 @@ start: ## Start the containers for testing $(MAKE) -i stop CURRENT_USER=$(CURRENT_USER) $(DOCKER_COMPOSE) up -d --build --force-recreate --remove-orphans $(DOCKER_COMPOSE) run --rm wait -c mysql:3306,postgres:5432,mssql:1433 -t 60 - $(MAKE) vendor stop: ## Stop and remove containers $(DOCKER_COMPOSE) down --remove-orphans --volumes @@ -34,7 +33,19 @@ php-cli: ## Open bash in PHP container $(DOCKER_COMPOSE) exec -u $(CURRENT_USER) php bash vendor: ## Install dependencies - $(DOCKER_EXEC_PHP_WITH_USER) "composer install --no-interaction --prefer-dist" + $(DOCKER_EXEC_PHP_WITH_USER) "composer update --no-interaction --prefer-dist" test: ## Run the tests $(DOCKER_EXEC_PHP_WITH_USER) "php vendor/bin/codecept run" + +pipeline: ## Run the tests pipeline + $(MAKE) start + $(MAKE) vendor + $(MAKE) test + $(MAKE) stop + +ci: ## Run the tests + $(MAKE) pipeline php=8.0 + $(MAKE) pipeline php=8.1 + $(MAKE) pipeline php=8.2 + $(MAKE) pipeline php=8.3 diff --git a/docker-compose.yml b/docker-compose.yml index d2052501..6fbad477 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ services: php: container_name: codeception-module-db build: - context: . - dockerfile: ./php81.Dockerfile + context: ./docker/php${php:-8.3} environment: COMPOSER_HOME: /tmp/.composer MYSQL_HOST: host.docker.internal diff --git a/php81.Dockerfile b/docker/php8.0/Dockerfile similarity index 87% rename from php81.Dockerfile rename to docker/php8.0/Dockerfile index 440dfa19..0f9c98b1 100644 --- a/php81.Dockerfile +++ b/docker/php8.0/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.1-fpm +FROM php:8.0-fpm COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ @@ -21,7 +21,7 @@ RUN install-php-extensions \ zip-stable \ xdebug-3.1.5 -COPY --from=composer /usr/bin/composer /usr/bin/composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html diff --git a/docker/php8.1/Dockerfile b/docker/php8.1/Dockerfile new file mode 100644 index 00000000..47b46a07 --- /dev/null +++ b/docker/php8.1/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.1-fpm + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + libpq-dev \ + default-mysql-client + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.11.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.3.2 + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/docker/php8.2/Dockerfile b/docker/php8.2/Dockerfile new file mode 100644 index 00000000..434fc9db --- /dev/null +++ b/docker/php8.2/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.2-fpm + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + libpq-dev \ + default-mysql-client + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.11.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.3.2 + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/docker/php8.3/Dockerfile b/docker/php8.3/Dockerfile new file mode 100644 index 00000000..babf5c06 --- /dev/null +++ b/docker/php8.3/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.3-fpm + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ + +RUN apt-get update && \ + apt-get install -y \ + unzip \ + wget \ + git \ + zlib1g-dev \ + libzip-dev \ + libpq-dev \ + default-mysql-client + +RUN install-php-extensions \ + pdo_mysql-stable \ + pdo_pgsql-stable \ + pdo_dblib-stable \ + pdo_sqlsrv-5.12.0 \ + pgsql-stable \ + zip-stable \ + xdebug-3.3.2 + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +ENTRYPOINT ["tail"] +CMD ["-f", "/dev/null"] diff --git a/tests/README.md b/tests/README.md index abab0b5d..213549cb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -31,6 +31,24 @@ make test This command will execute the tests inside the PHP container. +## Running the Tests Pipeline + +To run the tests pipeline, use the following command: + +```bash +make pipeline +``` + +This command will start the containers, install dependencies, run the tests, and then stop the containers. + +## Running the Tests for Continuous Integration + +```bash +make ci +``` + +This command will run the tests pipeline for all supported PHP versions. + ## Other Commands - To stop and remove the Docker containers, use the following command: diff --git a/tests/data/sqlite.db b/tests/data/sqlite.db old mode 100644 new mode 100755 From cbe0d1f66215259b13b3641e4fb8f0d5cc4f63a2 Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Sun, 28 Jul 2024 05:37:52 +0200 Subject: [PATCH 17/25] Use short array syntax for consistency (#72) --- src/Codeception/Lib/Interfaces/Db.php | 2 +- src/Codeception/Module/Db.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Codeception/Lib/Interfaces/Db.php b/src/Codeception/Lib/Interfaces/Db.php index 525de129..0407c4f8 100644 --- a/src/Codeception/Lib/Interfaces/Db.php +++ b/src/Codeception/Lib/Interfaces/Db.php @@ -56,7 +56,7 @@ public function dontSeeInDatabase(string $table, array $criteria = []): void; * * ``` php * grabFromDatabase('users', 'email', array('name' => 'Davert')); + * $mail = $I->grabFromDatabase('users', 'email', ['name' => 'Davert']); * ``` * Comparison expressions can be used as well: * diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index a27299f2..699e5d3b 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -777,7 +777,7 @@ protected function loadDumpUsingDriver(string $databaseKey): void * * ```php * haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com')); + * $I->haveInDatabase('users', ['name' => 'miles', 'email' => 'miles@davis.com']); * ``` */ public function haveInDatabase(string $table, array $data): int @@ -930,7 +930,7 @@ protected function proceedSeeInDatabase(string $table, string $column, array $cr * * ``` php * grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter')); + * $mails = $I->grabColumnFromDatabase('users', 'email', ['name' => 'RebOOter']); * ``` */ public function grabColumnFromDatabase(string $table, string $column, array $criteria = []): array @@ -950,7 +950,7 @@ public function grabColumnFromDatabase(string $table, string $column, array $cri * * ``` php * grabFromDatabase('users', 'email', array('name' => 'Davert')); + * $mail = $I->grabFromDatabase('users', 'email', ['name' => 'Davert']); * ``` * Comparison expressions can be used as well: * @@ -976,7 +976,7 @@ public function grabFromDatabase(string $table, string $column, array $criteria * * ``` php * grabEntryFromDatabase('users', array('name' => 'Davert')); + * $mail = $I->grabEntryFromDatabase('users', ['name' => 'Davert']); * ``` * Comparison expressions can be used as well: * @@ -1014,7 +1014,7 @@ public function grabEntryFromDatabase(string $table, array $criteria = []): arra * * ``` php * grabEntriesFromDatabase('users', array('name' => 'Davert')); + * $mail = $I->grabEntriesFromDatabase('users', ['name' => 'Davert']); * ``` * Comparison expressions can be used as well: * @@ -1057,7 +1057,7 @@ public function grabNumRecords(string $table, array $criteria = []): int * * ```php * updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com')); + * $I->updateInDatabase('users', ['isAdmin' => true], ['email' => 'miles@davis.com']); * ``` */ public function updateInDatabase(string $table, array $data, array $criteria = []): void From 8b9285f84b0640a9a3c6f80c5f2e8505d1e0b3ac Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Sun, 28 Jul 2024 05:40:01 +0200 Subject: [PATCH 18/25] Configure nullable types explicitly to avoid deprecation warnings in PHP 8.4 (#73) --- src/Codeception/Lib/DbPopulator.php | 2 +- src/Codeception/Lib/Driver/Db.php | 6 +++--- src/Codeception/Lib/Driver/Sqlite.php | 2 +- src/Codeception/Module/Db.php | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Codeception/Lib/DbPopulator.php b/src/Codeception/Lib/DbPopulator.php index 59d9b23b..22e3aa27 100644 --- a/src/Codeception/Lib/DbPopulator.php +++ b/src/Codeception/Lib/DbPopulator.php @@ -53,7 +53,7 @@ public function __construct(array $config) * @param string|null $dumpFile The dump file to build the command with. * @return string The resulting command string after evaluating any configuration's key */ - protected function buildCommand(string $command, string $dumpFile = null): string + protected function buildCommand(string $command, ?string $dumpFile = null): string { $dsn = $this->config['dsn'] ?? ''; $dsnVars = []; diff --git a/src/Codeception/Lib/Driver/Db.php b/src/Codeception/Lib/Driver/Db.php index 519b6790..e8041ecf 100755 --- a/src/Codeception/Lib/Driver/Db.php +++ b/src/Codeception/Lib/Driver/Db.php @@ -31,7 +31,7 @@ class Db */ protected array $primaryKeys = []; - public static function connect(string $dsn, string $user = null, string $password = null, array $options = null): PDO + public static function connect(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null): PDO { $dbh = new PDO($dsn, $user, $password, $options); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); @@ -47,7 +47,7 @@ public static function connect(string $dsn, string $user = null, string $passwor * * @return Db|SqlSrv|MySql|Oci|PostgreSql|Sqlite */ - public static function create(string $dsn, string $user = null, string $password = null, array $options = null): Db + public static function create(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null): Db { $provider = self::getProvider($dsn); @@ -78,7 +78,7 @@ public static function getProvider($dsn): string * @see https://www.php.net/manual/en/pdo.construct.php * @see https://www.php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants */ - public function __construct(string $dsn, string $user = null, string $password = null, array $options = null) + public function __construct(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null) { $this->dbh = new PDO($dsn, $user, $password, $options); $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); diff --git a/src/Codeception/Lib/Driver/Sqlite.php b/src/Codeception/Lib/Driver/Sqlite.php index f480c76d..e539649d 100644 --- a/src/Codeception/Lib/Driver/Sqlite.php +++ b/src/Codeception/Lib/Driver/Sqlite.php @@ -14,7 +14,7 @@ class Sqlite extends Db protected string $filename = ''; - public function __construct(string $dsn, string $user = null, string $password = null, array $options = null) + public function __construct(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null) { $filename = substr($dsn, 7); if ($filename === ':memory:') { diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index 699e5d3b..b78eb721 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -684,7 +684,7 @@ protected function removeInserted($databaseKey = null): void $this->insertedRows[$databaseKey] = []; } - public function _cleanup(string $databaseKey = null, array $databaseConfig = null): void + public function _cleanup(?string $databaseKey = null, ?array $databaseConfig = null): void { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; @@ -737,7 +737,7 @@ public function _isPopulated() return $this->databasesPopulated[$this->currentDatabase]; } - public function _loadDump(string $databaseKey = null, array $databaseConfig = null): void + public function _loadDump(?string $databaseKey = null, ?array $databaseConfig = null): void { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; From 656a30e27083315cf0728cacacfa33e4c12ee9a0 Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu Date: Fri, 3 Jan 2025 17:06:14 +0100 Subject: [PATCH 19/25] fix(ci): /opt/mssql-tools/bin/sqlcmd tool not found in given path (#80) --- .github/workflows/main.yml | 6 +++--- docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c1e6b1c9..0c1d40b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,7 @@ on: [push, pull_request] jobs: phpcs: name: Code style - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout @@ -25,7 +25,7 @@ jobs: tests: name: Unit tests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 services: mysql: @@ -53,7 +53,7 @@ jobs: ports: - 5432:5432 mssql: - image: mcr.microsoft.com/mssql/server:2019-latest + image: mcr.microsoft.com/mssql/server:2022-CU13-ubuntu-20.04 env: SA_PASSWORD: P@ssw0rd ACCEPT_EULA: 'Y' diff --git a/docker-compose.yml b/docker-compose.yml index 6fbad477..0a234913 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: - "5432:5432" mssql: - image: mcr.microsoft.com/mssql/server:2019-latest + image: mcr.microsoft.com/mssql/server:2022-CU13-ubuntu-20.04 environment: SA_PASSWORD: 'P@ssw0rd' MSSQL_DATABASE: codeception From 16021a83eca2c51d72a0211899b4c04ac6cdb300 Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Sun, 5 Jan 2025 14:20:52 +0100 Subject: [PATCH 20/25] Test against PHP 8.4 (#77) --- .github/workflows/main.yml | 2 +- tests/unit.suite.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0c1d40b7..72852229 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: strategy: matrix: - php: [8.0, 8.1, 8.2, 8.3] + php: [8.0, 8.1, 8.2, 8.3, 8.4] steps: - name: Create default database for sqlsrv as image does not support it diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml index 5399cf41..1c448f5d 100644 --- a/tests/unit.suite.yml +++ b/tests/unit.suite.yml @@ -1,5 +1,5 @@ # Codeception Test Suite Configuration # suite for unit (internal) tests. -error_level: "E_ALL | E_STRICT" +error_level: "E_ALL" class_name: UnitTester From 7c1932f241849df38db8d89604f90ef0abc5fc8d Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Wed, 15 Jan 2025 00:38:09 +0100 Subject: [PATCH 21/25] Avoid deprecated direct access to driver and dbh property (#81) --- tests/unit/Codeception/Module/Db/AbstractDbTest.php | 12 ++++++------ tests/unit/Codeception/Module/Db/MySqlDbTest.php | 8 ++++---- tests/unit/Codeception/Module/Db/SqliteDbTest.php | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/unit/Codeception/Module/Db/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php index a67a40a2..54035997 100644 --- a/tests/unit/Codeception/Module/Db/AbstractDbTest.php +++ b/tests/unit/Codeception/Module/Db/AbstractDbTest.php @@ -46,8 +46,8 @@ public function testConnectionIsKeptForTheWholeSuite() $this->module->_before($testCase1); // Save these object instances IDs $driverAndConn1 = [ - $this->module->driver, - $this->module->dbh + $this->module->_getDriver(), + $this->module->_getDbh() ]; $this->module->_after($testCase1); @@ -55,8 +55,8 @@ public function testConnectionIsKeptForTheWholeSuite() $this->module->_before($testCase2); $driverAndConn2 = [ - $this->module->driver, - $this->module->dbh + $this->module->_getDriver(), + $this->module->_getDbh() ]; $this->module->_after($testCase2); $this->assertSame($driverAndConn2, $driverAndConn1); @@ -157,8 +157,8 @@ public function testHaveInDatabaseWithCompositePrimaryKey() { $insertQuery = 'INSERT INTO composite_pk (group_id, id, status) VALUES (?, ?, ?)'; //this test checks that module does not delete columns by partial primary key - $this->module->driver->executeQuery($insertQuery, [1, 2, 'test']); - $this->module->driver->executeQuery($insertQuery, [2, 1, 'test2']); + $this->module->_getDriver()->executeQuery($insertQuery, [1, 2, 'test']); + $this->module->_getDriver()->executeQuery($insertQuery, [2, 1, 'test2']); $testData = ['id' => 2, 'group_id' => 2, 'status' => 'test3']; $this->module->haveInDatabase('composite_pk', $testData); diff --git a/tests/unit/Codeception/Module/Db/MySqlDbTest.php b/tests/unit/Codeception/Module/Db/MySqlDbTest.php index 8bc10015..cb7d9b2e 100644 --- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php @@ -49,13 +49,13 @@ public function testConnectionIsResetOnEveryTestWhenReconnectIsTrue() // Simulate a test that runs $this->module->_before($testCase1); - $connection1 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); + $connection1 = $this->module->_getDbh()->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase1); // Simulate a second test that runs $this->module->_before($testCase2); - $connection2 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); + $connection2 = $this->module->_getDbh()->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase2); $this->module->_afterSuite(); @@ -63,7 +63,7 @@ public function testConnectionIsResetOnEveryTestWhenReconnectIsTrue() $this->module->_before($testCase3); - $connection3 = $this->module->dbh->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); + $connection3 = $this->module->_getDbh()->query('SELECT CONNECTION_ID()')->fetch(PDO::FETCH_COLUMN); $this->module->_after($testCase3); $this->assertSame($connection1, $connection2); @@ -81,7 +81,7 @@ public function testInitialQueriesAreExecuted() $this->module->_reconfigure($config); $this->module->_before(Stub::makeEmpty(TestInterface::class)); - $usedDatabaseName = $this->module->dbh->query('SELECT DATABASE();')->fetch(PDO::FETCH_COLUMN); + $usedDatabaseName = $this->module->_getDbh()->query('SELECT DATABASE();')->fetch(PDO::FETCH_COLUMN); $this->assertSame($dbName, $usedDatabaseName); } diff --git a/tests/unit/Codeception/Module/Db/SqliteDbTest.php b/tests/unit/Codeception/Module/Db/SqliteDbTest.php index cdbeb97d..1d98cf79 100644 --- a/tests/unit/Codeception/Module/Db/SqliteDbTest.php +++ b/tests/unit/Codeception/Module/Db/SqliteDbTest.php @@ -40,13 +40,13 @@ public function testConnectionIsResetOnEveryTestWhenReconnectIsTrue() // Simulate a test that runs $this->module->_before($testCase1); - $connection1 = spl_object_hash($this->module->dbh); + $connection1 = spl_object_hash($this->module->_getDbh()); $this->module->_after($testCase1); // Simulate a second test that runs $this->module->_before($testCase2); - $connection2 = spl_object_hash($this->module->dbh); + $connection2 = spl_object_hash($this->module->_getDbh()); $this->module->_after($testCase2); $this->module->_afterSuite(); @@ -54,7 +54,7 @@ public function testConnectionIsResetOnEveryTestWhenReconnectIsTrue() $this->module->_before($testCase3); - $connection3 = spl_object_hash($this->module->dbh); + $connection3 = spl_object_hash($this->module->_getDbh()); $this->module->_after($testCase3); $this->assertSame($connection1, $connection2); From 6b490e0216191e759565c6dc7e02fbfa2d5d1764 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Wed, 15 Jan 2025 00:39:27 +0100 Subject: [PATCH 22/25] Update Db.php: Fixing code formating, removing duplication (#78) --------- Co-authored-by: Dieter Beck --- src/Codeception/Module/Db.php | 173 ++++++++++++++++------------------ 1 file changed, 80 insertions(+), 93 deletions(-) diff --git a/src/Codeception/Module/Db.php b/src/Codeception/Module/Db.php index b78eb721..17aa3f28 100644 --- a/src/Codeception/Module/Db.php +++ b/src/Codeception/Module/Db.php @@ -26,7 +26,7 @@ * This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](https://codeception.com/docs/modules/Db#seeInDatabase) * * In order to have your database populated with data you need a raw SQL dump. - * Simply put the dump in the `tests/_data` directory (by default) and specify the path in the config. + * Simply put the dump in the `tests/Support/Data` directory (by default) and specify the path in the config. * The next time after the database is cleared, all your data will be restored from the dump. * Don't forget to include `CREATE TABLE` statements in the dump. * @@ -41,85 +41,72 @@ * * MS SQL * * Oracle * - * Connection is done by database Drivers, which are stored in the `Codeception\Lib\Driver` namespace. - * [Check out the drivers](https://github.com/Codeception/Codeception/tree/2.4/src/Codeception/Lib/Driver) - * if you run into problems loading dumps and cleaning databases. + * Connection is done by database drivers, which are stored in the `Codeception\Lib\Driver` namespace. + * Check out the drivers if you run into problems loading dumps and cleaning databases. * - * ## Config - * - * * dsn *required* - PDO DSN - * * user *required* - username to access database - * * password *required* - password - * * dump - path to database dump - * * populate: false - whether the the dump should be loaded before the test suite is started - * * cleanup: false - whether the dump should be reloaded before each test - * * reconnect: false - whether the module should reconnect to the database before each test - * * waitlock: 0 - wait lock (in seconds) that the database session should use for DDL statements - * * ssl_key - path to the SSL key (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key) - * * ssl_cert - path to the SSL certificate (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert) - * * ssl_ca - path to the SSL certificate authority (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca) - * * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php) - * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) - * * databases - include more database configs and switch between them in tests. - * * initial_queries - list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation - * * skip_cleanup_if_failed - Do not perform the cleanup if the tests failed. If this is used, manual cleanup might be required when re-running - * ## Example - * - * modules: - * enabled: - * - Db: - * dsn: 'mysql:host=localhost;dbname=testdb' - * user: 'root' - * password: '' - * dump: 'tests/_data/dump.sql' - * populate: true - * cleanup: true - * reconnect: true - * waitlock: 10 - * skip_cleanup_if_failed: true - * ssl_key: '/path/to/client-key.pem' - * ssl_cert: '/path/to/client-cert.pem' - * ssl_ca: '/path/to/ca-cert.pem' - * ssl_verify_server_cert: false - * ssl_cipher: 'AES256-SHA' - * initial_queries: - * - 'CREATE DATABASE IF NOT EXISTS temp_db;' - * - 'USE temp_db;' - * - 'SET NAMES utf8;' + * ## Example `Functional.suite.yml` + * ```yaml + * modules: + * enabled: + * - Db: + * dsn: 'mysql:host=localhost;dbname=testdb' + * user: 'root' + * password: '' + * dump: 'tests/Support/Data/dump.sql' + * populate: true # whether the dump should be loaded before the test suite is started + * cleanup: true # whether the dump should be reloaded before each test + * reconnect: true # whether the module should reconnect to the database before each test + * waitlock: 10 # wait lock (in seconds) that the database session should use for DDL statements + * databases: # include more database configs and switch between them in tests. + * skip_cleanup_if_failed: true # Do not perform the cleanup if the tests failed. If this is used, manual cleanup might be required when re-running + * ssl_key: '/path/to/client-key.pem' # path to the SSL key (MySQL specific, see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key) + * ssl_cert: '/path/to/client-cert.pem' # path to the SSL certificate (MySQL specific, see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert) + * ssl_ca: '/path/to/ca-cert.pem' # path to the SSL certificate authority (MySQL specific, see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca) + * ssl_verify_server_cert: false # disables certificate CN verification (MySQL specific, see https://php.net/manual/de/ref.pdo-mysql.php) + * ssl_cipher: 'AES256-SHA' # list of one or more permissible ciphers to use for SSL encryption (MySQL specific, see https://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) + * initial_queries: # list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation + * - 'CREATE DATABASE IF NOT EXISTS temp_db;' + * - 'USE temp_db;' + * - 'SET NAMES utf8;' + * ``` * * ## Example with multi-dumps - * modules: - * enabled: - * - Db: - * dsn: 'mysql:host=localhost;dbname=testdb' - * user: 'root' - * password: '' - * dump: - * - 'tests/_data/dump.sql' - * - 'tests/_data/dump-2.sql' + * ```yaml + * modules: + * enabled: + * - Db: + * dsn: 'mysql:host=localhost;dbname=testdb' + * user: 'root' + * password: '' + * dump: + * - 'tests/Support/Data/dump.sql' + * - 'tests/Support/Data/dump-2.sql' + * ``` * * ## Example with multi-databases - * - * modules: - * enabled: - * - Db: - * dsn: 'mysql:host=localhost;dbname=testdb' - * user: 'root' - * password: '' - * databases: + * ```yaml + * modules: + * enabled: + * - Db: + * dsn: 'mysql:host=localhost;dbname=testdb' + * user: 'root' + * password: '' + * databases: * db2: - * dsn: 'mysql:host=localhost;dbname=testdb2' - * user: 'userdb2' - * password: '' - * - * ## Example with Sqlite + * dsn: 'mysql:host=localhost;dbname=testdb2' + * user: 'userdb2' + * password: '' + * ``` * - * modules: - * enabled: - * - Db: - * dsn: 'sqlite:relative/path/to/sqlite-database.db' - * user: '' - * password: '' + * ## Example with SQLite + * ```yaml + * modules: + * enabled: + * - Db: + * dsn: 'sqlite:relative/path/to/sqlite-database.db' + * user: '' + * password: '' + * ``` * * ## SQL data dump * @@ -134,30 +121,30 @@ * * ```yaml * modules: - * enabled: - * - Db: - * dsn: 'mysql:host=localhost;dbname=testdb' - * user: 'root' - * password: '' - * dump: 'tests/_data/dump.sql' - * populate: true # run populator before all tests - * cleanup: true # run populator before each test - * populator: 'mysql -u $user -h $host $dbname < $dump' + * enabled: + * - Db: + * dsn: 'mysql:host=localhost;dbname=testdb' + * user: 'root' + * password: '' + * dump: 'tests/Support/Data/dump.sql' + * populate: true # run populator before all tests + * cleanup: true # run populator before each test + * populator: 'mysql -u $user -h $host $dbname < $dump' * ``` * - * For PostgreSQL (using pg_restore) + * For PostgreSQL (using `pg_restore`) * - * ``` + * ```yaml * modules: - * enabled: - * - Db: - * dsn: 'pgsql:host=localhost;dbname=testdb' - * user: 'root' - * password: '' - * dump: 'tests/_data/db_backup.dump' - * populate: true # run populator before all tests - * cleanup: true # run populator before each test - * populator: 'pg_restore -u $user -h $host -D $dbname < $dump' + * enabled: + * - Db: + * dsn: 'pgsql:host=localhost;dbname=testdb' + * user: 'root' + * password: '' + * dump: 'tests/Support/Data/db_backup.dump' + * populate: true # run populator before all tests + * cleanup: true # run populator before each test + * populator: 'pg_restore -u $user -h $host -D $dbname < $dump' * ``` * * Variable names are being taken from config and DSN which has a `keyword=value` format, so you should expect to have a variable named as the From 23bc6a37472b9d5935bc8cf8defc9d59e852c0c2 Mon Sep 17 00:00:00 2001 From: Evgeniy Moiseenko Date: Sat, 1 Feb 2025 00:25:45 +0300 Subject: [PATCH 23/25] Remove unnecessary files from Composer package (#83) --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 4a905c87..faa6f844 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,5 @@ /*.md export-ignore /*.xml export-ignore /*.yml export-ignore +/.github export-ignore +/phpcs.xml export-ignore From 8a295c40d009f9821fae5e32c4d3ce10273e57a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 6 Feb 2025 20:34:04 +0100 Subject: [PATCH 24/25] fix: allow uppercase table names by quoting the table name when fetching the primary key of a table in PostgreSQL (#82) --- src/Codeception/Lib/Driver/PostgreSql.php | 2 +- tests/data/dumps/postgres.sql | 4 ++++ tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Driver/PostgreSql.php b/src/Codeception/Lib/Driver/PostgreSql.php index 45bed775..3d0818eb 100644 --- a/src/Codeception/Lib/Driver/PostgreSql.php +++ b/src/Codeception/Lib/Driver/PostgreSql.php @@ -164,7 +164,7 @@ public function getPrimaryKey(string $tableName): array FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = '{$tableName}'::regclass + WHERE i.indrelid = '\"{$tableName}\"'::regclass AND i.indisprimary"; $stmt = $this->executeQuery($query, []); $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); diff --git a/tests/data/dumps/postgres.sql b/tests/data/dumps/postgres.sql index 13b87d0e..30af3596 100755 --- a/tests/data/dumps/postgres.sql +++ b/tests/data/dumps/postgres.sql @@ -448,6 +448,10 @@ CREATE TABLE "no_pk" ( "status" VARCHAR NOT NULL ); +CREATE TABLE "NoPk" ( + "Status" VARCHAR NOT NULL +); + CREATE TABLE "order" ( "id" INTEGER NOT NULL PRIMARY KEY, "name" VARCHAR NOT NULL, diff --git a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php index 0f2c4900..ee2f8396 100644 --- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php @@ -33,4 +33,10 @@ public function getConfig(): array 'populate' => true ]; } + + public function testHaveInDatabaseWithUppercaseTableName() + { + $testData = ['Status' => 'test']; + $this->module->haveInDatabase('NoPk', $testData); + } } From 0ac08372c13f72c33745050e396317c8456a5f7b Mon Sep 17 00:00:00 2001 From: Szabolcs Hajdu <157013358+sabee-bb@users.noreply.github.com> Date: Mon, 3 Mar 2025 09:10:27 +0100 Subject: [PATCH 25/25] fix: properly quote table names with schema definition #84 (#86) --- .github/workflows/main.yml | 2 +- composer.json | 1 + docker-compose.yml | 2 +- src/Codeception/Lib/Driver/PostgreSql.php | 2 +- .../Module/Db/PostgreSqlDbTest.php | 26 ++++++++++++------- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72852229..4ab8485e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: --health-timeout 5s --health-retries 5 postgres: - image: postgres + image: postgres:16.4 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: codeception_test diff --git a/composer.json b/composer.json index 77eefd0e..f21a5165 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "codeception/codeception": "*@dev" }, "require-dev": { + "behat/gherkin": "~4.10.0", "squizlabs/php_codesniffer": "*" }, "conflict": { diff --git a/docker-compose.yml b/docker-compose.yml index 0a234913..2de9e92c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: - "3306:3306" postgres: - image: postgres + image: postgres:16.4 environment: POSTGRES_PASSWORD: codeception POSTGRES_DB: codeception diff --git a/src/Codeception/Lib/Driver/PostgreSql.php b/src/Codeception/Lib/Driver/PostgreSql.php index 3d0818eb..f5a5b761 100644 --- a/src/Codeception/Lib/Driver/PostgreSql.php +++ b/src/Codeception/Lib/Driver/PostgreSql.php @@ -164,7 +164,7 @@ public function getPrimaryKey(string $tableName): array FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = '\"{$tableName}\"'::regclass + WHERE i.indrelid = '" . $this->getQuotedName($tableName) . "'::regclass AND i.indisprimary"; $stmt = $this->executeQuery($query, []); $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); diff --git a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php index ee2f8396..d93e3294 100644 --- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php +++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php @@ -17,20 +17,20 @@ public function getConfig(): array $this->markTestSkipped(); } - $host = getenv('PG_HOST') ?: 'localhost'; - $user = getenv('PG_USER') ?: 'postgres'; + $host = getenv('PG_HOST') ?: 'localhost'; + $user = getenv('PG_USER') ?: 'postgres'; $password = getenv('PG_PASSWORD') ?: null; $database = getenv('PG_DB') ?: 'codeception_test'; - $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; + $dsn = getenv('PG_DSN') ?: 'pgsql:host=' . $host . ';dbname=' . $database; return [ - 'dsn' => $dsn, - 'user' => $user, - 'password' => $password, - 'dump' => 'tests/data/dumps/postgres.sql', + 'dsn' => $dsn, + 'user' => $user, + 'password' => $password, + 'dump' => 'tests/data/dumps/postgres.sql', 'reconnect' => true, - 'cleanup' => true, - 'populate' => true + 'cleanup' => true, + 'populate' => true, ]; } @@ -39,4 +39,12 @@ public function testHaveInDatabaseWithUppercaseTableName() $testData = ['Status' => 'test']; $this->module->haveInDatabase('NoPk', $testData); } + + public function testHaveInDatabaseWithAnotherSchema() + { + $userId = $this->module->haveInDatabase('anotherschema.users', ['name' => 'anotherschema', 'email' => 'anotherschema@test.com']); + $this->assertIsInt($userId); + + $this->module->seeInDatabase('anotherschema.users', ['name' => 'anotherschema', 'email' => 'anotherschema@test.com']); + } }