From 1738584bc9c0859a92be132f88299805b208cab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Tue, 9 Apr 2024 11:08:13 +0200 Subject: [PATCH] Add README and basic CI files --- .gitattributes | 2 + .github/release-drafter.yml | 9 + .github/workflows/build-changelog.yml | 16 + .github/workflows/build-release.yml | 47 +++ .github/workflows/test-unit.yml | 460 ++++++++++++++++++++++++++ .gitignore | 17 + .php-cs-fixer.dist.php | 61 ++++ LICENSE | 21 ++ README.md | 42 +++ behat.yml.dist | 29 ++ codecov.yml | 9 + composer.json | 57 ++++ phpstan.neon.dist | 11 + phpunit.xml.dist | 21 ++ 14 files changed, 802 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/build-changelog.yml create mode 100644 .github/workflows/build-release.yml create mode 100644 .github/workflows/test-unit.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.dist.php create mode 100644 LICENSE create mode 100644 behat.yml.dist create mode 100644 codecov.yml create mode 100644 composer.json create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ddfbd7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.min.css -text -filter +*.min.js -text -filter diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..6b1ea80 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,9 @@ +categories: + - title: "Major features" + labels: + - "MAJOR" + - title: "Breaking changes" + labels: + - "BC-break" + - title: "Other changes" +template: $CHANGES diff --git a/.github/workflows/build-changelog.yml b/.github/workflows/build-changelog.yml new file mode 100644 index 0000000..65b9151 --- /dev/null +++ b/.github/workflows/build-changelog.yml @@ -0,0 +1,16 @@ +name: Build changelog + +on: + push: + branches: + - develop + +jobs: + update: + name: Update + runs-on: ubuntu-latest + steps: + - name: Run + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..b169aef --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,47 @@ +name: Build release + +on: + push: + branches: + - 'release/*' + +jobs: + autocommit: + name: Build Release + runs-on: ubuntu-latest + container: + image: ghcr.io/mvorisek/image-php:latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Install PHP dependencies + run: composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: Update composer.json + run: >- + composer config --unset version && php -r ' + $f = __DIR__ . "/composer.json"; + $data = json_decode(file_get_contents($f), true); + foreach ($data as $k => $v) { + if (preg_match("~^(.+)-release$~", $k, $matches)) { + $data[$matches[1]] = $data[$k]; unset($data[$k]); + } + } + $str = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + echo $str; + file_put_contents($f, $str); + ' + + - name: Commit + run: | + git config --global user.name "$(git show -s --format='%an')" + git config --global user.email "$(git show -s --format='%ae')" + git add . -N && (git diff --exit-code || git commit -a -m "Branch for stable release") + + - name: Push + uses: ad-m/github-push-action@master + with: + branch: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml new file mode 100644 index 0000000..85a9a93 --- /dev/null +++ b/.github/workflows/test-unit.yml @@ -0,0 +1,460 @@ +name: Unit + +on: + pull_request: + push: + schedule: + - cron: '0 0/2 * * *' + +jobs: + smoke-test: + name: Smoke + runs-on: ubuntu-latest + container: + image: ghcr.io/mvorisek/image-php:${{ matrix.php }} + strategy: + fail-fast: false + matrix: + php: ['latest'] + type: ['Phpunit'] + include: + - php: 'latest' + type: 'CodingStyle' + - php: 'latest' + type: 'StaticAnalysis' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure PHP + run: | + rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + php --version + + - name: Setup cache 1/2 + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Setup cache 2/2 + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-smoke-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: | + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev; fi + if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer ergebnis/composer-normalize --dev; fi + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* behat/\* --dev; fi + composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + + - name: "Run tests: SQLite (only for Phpunit)" + if: startsWith(matrix.type, 'Phpunit') + run: | + php demos/_demo-data/create-db.php + vendor/bin/phpunit --exclude-group none --no-coverage --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + + - name: Check Coding Style (only for CodingStyle) + if: matrix.type == 'CodingStyle' + run: | + if [ "$(find demos/ -name '*.php' -print0 | xargs -0 grep -L "namespace Atk4\\\\Ui\\\\Demos[;\\\\]" | tee /dev/fd/2)" ]; then echo 'All demos/ files must have namespace declared' && (exit 1); fi + vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --diff --verbose + composer config --unset version && composer config --unset require-release + composer validate --strict --no-check-lock && composer normalize --dry-run --no-check-lock + + - name: Run Static Analysis (only for StaticAnalysis) + if: matrix.type == 'StaticAnalysis' + run: | + echo "memory_limit = 2G" > /usr/local/etc/php/conf.d/custom-memory-limit.ini + vendor/bin/phpstan analyse + + unit-test: + name: Unit + runs-on: ubuntu-latest + container: + image: ghcr.io/mvorisek/image-php:${{ matrix.php }} + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + type: ['Phpunit', 'Phpunit Lowest'] + include: + - php: 'latest' + type: 'Phpunit Burn' + env: + LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.3' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" + services: + mysql: + image: mysql + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + mariadb: + image: mariadb + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + postgres: + image: postgres:alpine + env: + POSTGRES_USER: atk4_test_user + POSTGRES_PASSWORD: atk4_pass + POSTGRES_DB: atk4_test + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + mssql: + image: mcr.microsoft.com/mssql/server + env: + ACCEPT_EULA: Y + SA_PASSWORD: atk4_pass + oracle: + image: gvenzl/oracle-xe:18-slim-faststart + env: + ORACLE_PASSWORD: atk4_pass + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure PHP + run: | + if [ -n "$LOG_COVERAGE" ]; then echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; else rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; fi + php --version + + - name: Setup cache 1/2 + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Setup cache 2/2 + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: | + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev; fi + if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer ergebnis/composer-normalize --dev; fi + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* behat/\* --dev; fi + if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi + composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + if [ "${{ matrix.type }}" = "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi + if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~public function runBare(): void~public function runBare(): void { gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 64; else echo 16; fi)"', 0); $emitter = Event\\Facade::emitter(); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); if ($this->inIsolation) { $dispatcher = \\Closure::bind(static fn () => $emitter->dispatcher, null, Event\\DispatchingEmitter::class)(); if ($i === -1) { $dispatcherEvents = $dispatcher->flush()->asArray(); } else { $dispatcher->flush(); } foreach ($dispatcherEvents as $event) { $dispatcher->dispatch($event); } } gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $e = new AssertionFailedError("Memory leak detected! (" . implode(" + ", array_map(static fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)"); $this->status = TestStatus::failure($e->getMessage()); $emitter->testFailed($this->valueObjectForEvents(), Event\\Code\\ThrowableBuilder::from($e), Event\\Code\\ComparisonFailureBuilder::from($e)); $this->onNotSuccessfulTest($e); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi + + - name: Init + run: | + php -r '(new PDO("mysql:host=mysql", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' + php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' + php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");' + /usr/lib/oracle/setup.sh + if [ -n "$LOG_COVERAGE" ]; then mkdir coverage; fi + + - name: "Run tests: SQLite" + run: | + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-sqlite.cov; fi + + - name: "Run tests: MySQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "mysql:host=mysql;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mysql.cov; fi + + - name: "Run tests: MariaDB" + if: success() || failure() + env: + DB_DSN: "mysql:host=mariadb;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mariadb.cov; fi + + - name: "Run tests: PostgreSQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "pgsql:host=postgres;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-postgres.cov; fi + + - name: "Run tests: MSSQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "sqlsrv:host=mssql;dbname=master;driverOptions[TrustServerCertificate]=1" + DB_USER: sa + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mssql.cov; fi + + - name: "Run tests: Oracle (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "oci:dbname=oracle/free" + DB_USER: system + DB_PASSWORD: atk4_pass + NLS_LANG: AMERICAN_AMERICA.AL32UTF8 + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) + if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-oracle.cov; fi + + - name: Upload coverage logs 1/2 (only for latest Phpunit) + if: env.LOG_COVERAGE + run: | + ls -l coverage | wc -l + php -d memory_limit=2G vendor/bin/phpcov merge coverage/ --clover coverage/merged.xml + + - name: Upload coverage logs 2/2 (only for latest Phpunit) + if: env.LOG_COVERAGE + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: coverage/merged.xml + + behat-test: + name: Behat + runs-on: ubuntu-latest + container: + image: ghcr.io/mvorisek/image-php:${{ matrix.php }}-selenium + strategy: + fail-fast: false + matrix: + # Selenium with PHP 7.4 and 8.0 was failing too often on initial/create session request, probably because of older (Alpine 3.16) base image + php: ['8.1', '8.2', '8.3'] + type: ['Chrome', 'Chrome Lowest'] + include: + - php: 'latest' + type: 'Firefox' + - php: 'latest' + type: 'Chrome Slow' + - php: 'latest' + type: 'Firefox Slow' + env: + LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.3' && matrix.type == 'Chrome' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" + services: + mysql: + image: mysql + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + mariadb: + image: mariadb + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + postgres: + image: postgres:alpine + env: + POSTGRES_USER: atk4_test_user + POSTGRES_PASSWORD: atk4_pass + POSTGRES_DB: atk4_test + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + mssql: + image: mcr.microsoft.com/mssql/server + env: + ACCEPT_EULA: Y + SA_PASSWORD: atk4_pass + oracle: + image: gvenzl/oracle-xe:18-slim-faststart + env: + ORACLE_PASSWORD: atk4_pass + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure PHP + run: | + if [ -n "$LOG_COVERAGE" ]; then echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; else rm /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; fi + php --version + + - name: Setup cache 1/2 + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Setup cache 2/2 + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-behat-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install JS dependencies (only for coverage or Chrome Slow) + if: env.LOG_COVERAGE || matrix.type == 'Chrome Slow' + run: | + if [ -n "$LOG_COVERAGE" ]; then + (cd js && npm install --package-lock-only --save-dev babel-plugin-istanbul nyc && npm ci --loglevel=error) + else + mv public public.orig + mkdir public public/css public/external + cp public.orig/.gitattributes public + cp public.orig/.gitignore public + cp public.orig/logo.png public + cp public.orig/css/agileui.less public/css + cp public.orig/external/.gitignore public/external + cp public.orig/external/package.json public/external + cp public.orig/external/package-lock.json public/external + cp public.orig/external/postinstall.js public/external + npm install --loglevel=error -g pug-cli less less-plugin-clean-css uglify-js + (cd js && npm ci --loglevel=error) + (cd public/external && npm ci --loglevel=error && git clean -dxfq .) + fi + + - name: Lint JS files (only for Chrome Slow) + if: matrix.type == 'Chrome Slow' + run: | + cp public/external/postinstall.js js + (cd js && npm run lint) + + - name: Compile HTML files (only for Chrome Slow) + if: matrix.type == 'Chrome Slow' + run: | + cp -r template template.orig + find template -not -type d -not -name '*.pug' -delete + (cd template && pug --doctype html --pretty --silent .) + + - name: Compile CSS files (only for Chrome Slow) + if: matrix.type == 'Chrome Slow' + run: | + lessc public/css/agileui.less public/css/agileui.min.css --clean-css="--s1 --advanced" --source-map + + - name: Compile JS files (only for coverage or Chrome Slow) + if: env.LOG_COVERAGE || matrix.type == 'Chrome Slow' + run: | + if [ -n "$LOG_COVERAGE" ]; then + rm -r public/js + (cd js && ISTANBUL_COVERAGE=1 npm run build) + else + (cd js && npm run build) + fi + + - name: Diff compiled files (only for Chrome Slow) + if: matrix.type == 'Chrome Slow' + run: | + diff -ru public.orig public + diff -ru template.orig template + rm -r public.orig template.orig + + - name: Install PHP dependencies + run: | + composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev + composer remove --no-interaction --no-update friendsofphp/php-cs-fixer ergebnis/composer-normalize --dev + composer remove --no-interaction --no-update phpstan/\* --dev + if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi + composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader + if [ "${{ matrix.type }}" = "Chrome Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi + + - name: Init + run: | + php -r '(new PDO("mysql:host=mysql", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' + php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' + php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");' + /usr/lib/oracle/setup.sh + if [ -n "$LOG_COVERAGE" ]; then mkdir coverage coverage/js; fi + ci_wait_until () { timeout 30 sh -c "until { $1 2> /dev/null; }; do sleep 0.02; done" || timeout 15 sh -c "$1" || { echo "health timeout: $1"; exit 1; }; } + php -d opcache.enable_cli=1 -S 127.0.0.1:8888 > /dev/null 2>&1 & + ci_wait_until 'nc -w 1 127.0.0.1 8888' + if [ -f /etc/alpine-release ]; then addgroup browser && adduser browser -G browser -D -s /bin/sh; else adduser browser --gecos "" --disabled-login -shell /bin/sh > /dev/null; fi + { Xvfb -ac :99 -screen 0 1920x1200x24 2> /dev/null & } && export DISPLAY=:99 + ci_wait_until '[ -e /tmp/.X11-unix/X99 ]' + su browser -c 'java -Dwebdriver.chrome.whitelistedIps=127.0.0.1 -jar /opt/selenium-server-standalone.jar -role standalone -host 127.0.0.1 -port 4444 -sessionTimeout 15 -browserTimeout 12 > /dev/null 2>&1 &' + ci_wait_until 'nc -w 1 127.0.0.1 4444' + if [ "${{ matrix.type }}" = "Firefox" ] || [ "${{ matrix.type }}" = "Firefox Slow" ]; then sed -i "s~chrome~firefox~" behat.yml.dist; fi + if [ "${{ matrix.type }}" = "Chrome Slow" ] || [ "${{ matrix.type }}" = "Firefox Slow" ]; then echo 'usleep(500_000);' >> demos/init-app.php; fi + + - name: "Run tests: SQLite" + run: | + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: "Run tests: MySQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "mysql:host=mysql;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: "Run tests: MariaDB (only for coverage or cron)" + if: (success() || failure()) && (env.LOG_COVERAGE || github.event_name == 'schedule') + env: + DB_DSN: "mysql:host=mariadb;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: "Run tests: PostgreSQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "pgsql:host=postgres;dbname=atk4_test" + DB_USER: atk4_test_user + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: "Run tests: MSSQL (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "sqlsrv:host=mssql;dbname=master;driverOptions[TrustServerCertificate]=1" + DB_USER: sa + DB_PASSWORD: atk4_pass + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: "Run tests: Oracle (only for cron)" + if: (success() || failure()) && github.event_name == 'schedule' + env: + DB_DSN: "oci:dbname=oracle/free" + DB_USER: system + DB_PASSWORD: atk4_pass + NLS_LANG: AMERICAN_AMERICA.AL32UTF8 + run: | + sed -E "s~(\\\$db = new.+Persistence\\\\Sql)\(.+\);~\\1('$DB_DSN', '$DB_USER', '$DB_PASSWORD');~g" -i demos/db.default.php + php demos/_demo-data/create-db.php + vendor/bin/behat -vv --config behat.yml.dist + + - name: Upload coverage logs 1/2 (only for coverage) + if: env.LOG_COVERAGE + run: | + ls -l coverage | wc -l + php -d memory_limit=2G vendor/bin/phpcov merge coverage/ --clover coverage/merged.xml + ls -l coverage/js | wc -l + (cd js && npx nyc report --temp-dir ../coverage/js --report-dir ../coverage/js -e vue --reporter=clover) + # fix never reached condition is rendered to clover with falsecount > 0 + # https://github.com/istanbuljs/istanbuljs/issues/695 + sed -i -E 's~count="0" type="cond" truecount="0" falsecount="[1-9]+[0-9]*"~count="0" type="cond" truecount="0" falsecount="0"~' coverage/js/clover.xml + sed -i -E 's~count="[0-9]+" type="cond" truecount="[1-9]+[0-9]*" falsecount="[0-9]+"~count="1" type="cond" truecount="1" falsecount="1"~' coverage/js/clover.xml + + - name: Upload coverage logs 2/2 (only for coverage) + if: env.LOG_COVERAGE + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: coverage/merged.xml,coverage/js/clover.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7dc8e28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/coverage +/vendor +/composer.lock +.idea +nbproject +.vscode +.DS_Store + +local +*.local +*.local.* +cache +*.cache +*.cache.* + +/phpunit.xml +/behat.yml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..496e452 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,61 @@ +in([__DIR__]) + ->exclude(['vendor']); + +return (new Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, + '@PHP74Migration' => true, + '@PHP74Migration:risky' => true, + + // required by PSR-12 + 'concat_space' => [ + 'spacing' => 'one', + ], + + // disable some too strict rules + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'none', + ], + 'single_line_throw' => false, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + ], + 'native_constant_invocation' => true, + 'native_function_invocation' => false, + 'void_return' => false, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'exit'], + ], + 'final_internal_class' => false, + 'combine_consecutive_issets' => false, + 'combine_consecutive_unsets' => false, + 'multiline_whitespace_before_semicolons' => false, + 'no_superfluous_elseif' => false, + 'ordered_class_elements' => false, + 'php_unit_internal_class' => false, + 'php_unit_test_class_requires_covers' => false, + 'phpdoc_add_missing_param_annotation' => false, + 'return_assignment' => false, + 'comment_to_phpdoc' => false, + 'general_phpdoc_annotation_remove' => [ + 'annotations' => ['author', 'copyright', 'throws'], + ], + + // fn => without curly brackets is less readable, + // also prevent bounding of unwanted variables for GC + 'use_arrow_functions' => false, + ]) + ->setFinder($finder) + ->setCacheFile(sys_get_temp_dir() . '/php-cs-fixer.' . md5(__DIR__) . '.cache'); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..445d887 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Agile Toolkit Limited (UK) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index af277c2..3de070a 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ # atk4/community + +Community maintained repository of [atk4/data](https://github.com/atk4/data) and [atk4/ui](https://github.com/atk4/ui) extensions. + +[![Build](https://github.com/atk4/community/actions/workflows/test-unit.yml/badge.svg?branch=develop)](https://github.com/atk4/community/actions?query=branch:develop) +[![CodeCov](https://codecov.io/gh/atk4/community/branch/develop/graph/badge.svg)](https://codecov.io/gh/atk4/community) +[![GitHub release](https://img.shields.io/github/release/atk4/community.svg)](https://github.com/atk4/community/releases) + +## Installation + +Run + +``` +composer require atk4/community +``` + +to require this and official `atk4/data` and `atk4/ui` packages. + +## Documentation + +Source code itself and demos are the documentation. If you miss some usecase and once you figured out the solution, feel free to contribute a demo. + +## Support + +This repository is created from atk4 developers for atk4 developers. Please respect everyone interests and always use constructive tone of language. Before you ask for something, make sure tried to figure out the answer by yourself. + +For discussion use [Discord](https://discord.gg/QVKSk2B). + +For reporting issues use [GitHub issues](https://github.com/atk4/community/issues). If you seek a commercial support, mention this in your issue explicitly. + +Feature requests are welcomed but noone is going to serve you. The best way to express an interest in some feature is to contribute a code. + +## Contributions + +For contributions use [GitHub pull requests](https://github.com/atk4/community/pulls). + +A pull request must either fix some bug or implement a new feature. Either way a test is required. + +New data feature must be placed under `Atk4\Community\Data\Xxx` namespace and fully covered with an unit test(s). + +New UI feature must be placed under `Atk4\Community\Ui\Xxx` namespace and fully covered with a Behat test(s). + +Please keep LoC and comments to a minimum to make review and maintaining easier. \ No newline at end of file diff --git a/behat.yml.dist b/behat.yml.dist new file mode 100644 index 0000000..997c0e6 --- /dev/null +++ b/behat.yml.dist @@ -0,0 +1,29 @@ +default: + suites: + main: + paths: + features: '%paths.base%/tests-behat' + contexts: + - Behat\MinkExtension\Context\MinkContext + - Atk4\Ui\Behat\Context + extensions: + Behat\MinkExtension: + base_url: 'http://127.0.0.1:8888/demos' + sessions: + default: + selenium2: + browser: chrome + wd_host: 'http://127.0.0.1:4444/wd/hub' + capabilities: + extra_capabilities: + chrome: + args: + - '--no-sandbox' + - '--headless' + - '--disable-dev-shm-usage' + - '--disable-gpu' + - '--window-size=1280,720' + proxy: + proxyType: manual + httpProxy: 203.0.113.0:2 + sslProxy: 203.0.113.0:2 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..54e1be7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +comment: false +coverage: + status: + project: + default: + target: auto + threshold: 0.015 + patch: false + changes: false diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d59edca --- /dev/null +++ b/composer.json @@ -0,0 +1,57 @@ +{ + "name": "atk4/community", + "description": "Community maintained repository of atk4/data and atk4/ui extensions.", + "license": "MIT", + "type": "library", + "keywords": [ + "atk4", + "data", + "ui", + "community" + ], + "version": "dev-develop", + "authors": [ + { + "name": "GitHub community", + "homepage": "https://github.com/atk4/ui/graphs/contributors" + } + ], + "homepage": "https://github.com/atk4/community", + "require": { + "php": ">=7.4 <8.4", + "atk4/data": "dev-ui" + }, + "require-release": { + "php": ">=7.4 <8.4", + "atk4/ui": "~5.1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.13", + "ergebnis/phpunit-slow-test-detector": "^2.9", + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^9.5.5 || ^10.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Atk4\\Community\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Atk4\\Community\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + }, + "sort-packages": true + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..2bd4794 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + +parameters: + level: 6 + checkMissingOverrideMethodAttribute: true + paths: + - . + excludePaths: + - vendor + - js \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..6f14faf --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + tests + + + + + + + + src + tests + + + + + + + +