diff --git a/.gitattributes b/.gitattributes
index 87f36790..faa6f844 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,7 +1,11 @@
# 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
+/.github export-ignore
+/phpcs.xml export-ignore
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d6b4a47b..4ab8485e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,8 +3,29 @@ name: CI
on: [push, pull_request]
jobs:
+ phpcs:
+ name: Code style
+ runs-on: ubuntu-22.04
+
+ 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:
- runs-on: ubuntu-latest
+ name: Unit tests
+ runs-on: ubuntu-22.04
services:
mysql:
@@ -20,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
@@ -31,20 +52,35 @@ jobs:
--health-retries 5
ports:
- 5432:5432
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2022-CU13-ubuntu-20.04
+ 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]
+ php: [8.0, 8.1, 8.2, 8.3, 8.4]
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
+ uses: actions/checkout@v4
- name: Setup PHP
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 +92,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/Makefile b/Makefile
new file mode 100644
index 00000000..5397eed2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,51 @@
+.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
+
+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 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/composer.json b/composer.json
index 2a777a15..f21a5165 100644
--- a/composer.json
+++ b/composer.json
@@ -17,15 +17,31 @@
"require": {
"php": "^8.0",
"ext-json": "*",
+ "ext-mbstring": "*",
"ext-pdo": "*",
"codeception/codeception": "*@dev"
},
+ "require-dev": {
+ "behat/gherkin": "~4.10.0",
+ "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
new file mode 100644
index 00000000..3fb698ba
--- /dev/null
+++ b/docker-compose.amd64.yml
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 00000000..2de9e92c
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,53 @@
+services:
+ php:
+ container_name: codeception-module-db
+ build:
+ context: ./docker/php${php:-8.3}
+ environment:
+ COMPOSER_HOME: /tmp/.composer
+ 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:
+ - ${HOME}/.composer:/tmp/.composer
+ - .:/var/www/html
+
+ mysql:
+ image: mysql:5.7
+ environment:
+ MYSQL_ROOT_PASSWORD: codeception
+ MYSQL_DATABASE: codeception
+ ports:
+ - "3306:3306"
+
+ postgres:
+ image: postgres:16.4
+ environment:
+ POSTGRES_PASSWORD: codeception
+ POSTGRES_DB: codeception
+ ports:
+ - "5432:5432"
+
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2022-CU13-ubuntu-20.04
+ 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" ]
+
+ wait:
+ image: dokku/wait
diff --git a/docker/php8.0/Dockerfile b/docker/php8.0/Dockerfile
new file mode 100644
index 00000000..0f9c98b1
--- /dev/null
+++ b/docker/php8.0/Dockerfile
@@ -0,0 +1,29 @@
+FROM php:8.0-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.1.5
+
+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.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/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..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 = [];
@@ -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/Db.php b/src/Codeception/Lib/Driver/Db.php
index fdcdb4e4..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);
@@ -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
@@ -290,10 +290,14 @@ 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;
+ } elseif (is_string($param) && $this->isBinary($param)) {
+ $type = PDO::PARAM_LOB;
} else {
$type = PDO::PARAM_STR;
}
@@ -342,4 +346,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/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..f5a5b761 100644
--- a/src/Codeception/Lib/Driver/PostgreSql.php
+++ b/src/Codeception/Lib/Driver/PostgreSql.php
@@ -164,12 +164,12 @@ 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);
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..e539649d 100644
--- a/src/Codeception/Lib/Driver/Sqlite.php
+++ b/src/Codeception/Lib/Driver/Sqlite.php
@@ -11,10 +11,10 @@
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)
+ public function __construct(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null)
{
$filename = substr($dsn, 7);
if ($filename === ':memory:') {
@@ -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/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 8f3a09c0..17aa3f28 100644
--- a/src/Codeception/Module/Db.php
+++ b/src/Codeception/Module/Db.php
@@ -23,10 +23,10 @@
* 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.
+ * 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.
- *
- * ## 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 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)
- * * 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;'
+ * 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.
+ *
+ * ## 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
@@ -529,8 +516,7 @@ private function readSql($databaseKey = null, $databaseConfig = null): void
}
/**
- * @return bool|null|string|string[]
- * @throws ModuleConfigException
+ * @throws ModuleConfigException|ModuleException
*/
private function readSqlFile(string $filePath): ?string
{
@@ -546,7 +532,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
@@ -557,38 +552,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 {
@@ -664,14 +664,14 @@ 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']}");
}
}
$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;
@@ -724,12 +724,12 @@ 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;
- if ($databaseConfig['populator']) {
+ if (!empty($databaseConfig['populator'])) {
$this->loadDumpUsingPopulator($databaseKey, $databaseConfig);
return;
}
@@ -759,12 +759,12 @@ 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
* 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
@@ -801,8 +801,15 @@ 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));
+ $missingPrimaryKeyColumns = array_diff_key($primaryKey, $filledKeys);
+
+ if (count($missingPrimaryKeyColumns) === 0) {
+ $primary = array_intersect_key($row, array_flip($primaryKey));
+ } elseif (count($missingPrimaryKeyColumns) === 1) {
+ $primary = array_intersect_key($row, array_flip($primaryKey));
+ $missingColumn = reset($missingPrimaryKeyColumns);
+ $primary[$missingColumn] = $id;
} else {
foreach ($primaryKey as $column) {
if (isset($row[$column])) {
@@ -830,7 +837,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
);
}
@@ -856,7 +863,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
)
);
@@ -868,7 +875,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
);
}
@@ -910,7 +917,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
@@ -930,7 +937,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:
*
@@ -949,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', ['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', ['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
*
@@ -966,7 +1044,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
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..213549cb
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,72 @@
+# 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.
+
+## 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:
+
+```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/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/dumps/mysql.sql b/tests/data/dumps/mysql.sql
index 4102f7ef..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');
@@ -94,8 +95,25 @@ 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 `auto_increment_on_composite_pk` (
+ `id` int(11) NOT NULL,
+ `counter` int(11) AUTO_INCREMENT NOT NULL,
+PRIMARY KEY (`id`, `counter`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
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/data/dumps/postgres.sql b/tests/data/dumps/postgres.sql
index a95dfe0f..30af3596 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
\.
@@ -446,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/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/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/data/sqlite.db b/tests/data/sqlite.db
old mode 100644
new mode 100755
index 6c7c70e4..b8b0d252
Binary files a/tests/data/sqlite.db and b/tests/data/sqlite.db differ
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 eda908ae..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 = [
@@ -25,9 +21,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);
@@ -63,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'");
@@ -102,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());
}
@@ -117,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 7e66826f..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 = [
@@ -27,10 +23,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()
@@ -125,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/AbstractDbTest.php b/tests/unit/Codeception/Module/Db/AbstractDbTest.php
index 9f4ccfe9..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);
@@ -64,6 +64,23 @@ public function testConnectionIsKeptForTheWholeSuite()
$this->module->_afterSuite();
}
+ 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')]);
+ }
+
+ public function testSeeInDatabaseWithNull()
+ {
+ $this->module->seeInDatabase('users', ['uuid' => null]);
+ }
+
public function testSeeInDatabase()
{
$this->module->seeInDatabase('users', ['name' => 'davert']);
@@ -76,11 +93,35 @@ public function testCountInDatabase()
$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', ['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');
@@ -116,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);
@@ -175,7 +216,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/MssqlDblibDbTest.php b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php
new file mode 100644
index 00000000..5b4a4ccd
--- /dev/null
+++ b/tests/unit/Codeception/Module/Db/MssqlDblibDbTest.php
@@ -0,0 +1,32 @@
+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..b54641e8
--- /dev/null
+++ b/tests/unit/Codeception/Module/Db/MssqlSqlSrvDbTest.php
@@ -0,0 +1,32 @@
+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 . ';Encrypt=no;TrustServerCertificate=yes';
+
+ 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 7fb56397..cb7d9b2e 100644
--- a/tests/unit/Codeception/Module/Db/MySqlDbTest.php
+++ b/tests/unit/Codeception/Module/Db/MySqlDbTest.php
@@ -2,32 +2,29 @@
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']);
}
public function getConfig(): array
{
- $host = getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : 'localhost';
- $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;
return [
- 'dsn' => 'mysql:host='.$host.';dbname=codeception_test',
- 'user' => 'root',
+ 'dsn' => $dsn,
+ 'user' => $user,
'password' => $password,
'dump' => 'tests/data/dumps/mysql.sql',
'reconnect' => true,
@@ -51,14 +48,14 @@ 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();
@@ -66,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);
@@ -83,14 +80,15 @@ 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);
}
public function testGrabColumnFromDatabase()
{
+ $this->module->_beforeSuite();
$emails = $this->module->grabColumnFromDatabase('users', 'email');
$this->assertSame(
[
@@ -99,6 +97,80 @@ public function testGrabColumnFromDatabase()
'miles@davis.com',
'charlie@parker.com',
],
- $emails);
+ $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 = [
+ '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);
+ }
+
+ 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);
}
}
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 fbdae270..d93e3294 100644
--- a/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php
+++ b/tests/unit/Codeception/Module/Db/PostgreSqlDbTest.php
@@ -2,18 +2,13 @@
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
{
- 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,16 +17,34 @@ 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',
- '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,
];
}
+
+ 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']);
+ }
}
diff --git a/tests/unit/Codeception/Module/Db/SqliteDbTest.php b/tests/unit/Codeception/Module/Db/SqliteDbTest.php
index 82871f49..1d98cf79 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
@@ -54,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();
@@ -68,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);
@@ -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();