From e459561b464d589f9cf06b417610a53ef103aa49 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 4 Aug 2024 00:12:30 +0800 Subject: [PATCH] Add workflows for unit tests and static code analysis --- .github/workflows/static-code-analysis.yml | 59 ++++++++ .github/workflows/unit-tests.yml | 87 +++++++++++ .php-cs-fixer.dist.php | 1 + bin/parallel-phpunit | 15 ++ bin/parallel-phpunit.php | 161 +++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 .github/workflows/static-code-analysis.yml create mode 100644 .github/workflows/unit-tests.yml create mode 100755 bin/parallel-phpunit create mode 100644 bin/parallel-phpunit.php diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml new file mode 100644 index 0000000..a1156c7 --- /dev/null +++ b/.github/workflows/static-code-analysis.yml @@ -0,0 +1,59 @@ +name: Static Code Analysis + +on: + pull_request: + push: + branches: + - 1.x + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tests: + strategy: + fail-fast: false + matrix: + php-version: + - 8.2 + - 8.3 + + name: Static Code Analysis + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: none + + - name: Get composer cache directory + id: cache-dir + run: | + echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.cache-dir.outputs.COMPOSER_CACHE_DIR }} + key: ${{ github.workflow }}-PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }}-${{ github.run_id }} + restore-keys: | + ${{ github.workflow }}-PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.json') }}- + ${{ github.workflow }}-PHP_${{ matrix.php-version }}- + + - name: Install dependencies + run: composer update --ansi + + - name: Check - PHP-CS-Fixer + run: composer cs:check + + - name: Check - PHPStan + run: composer phpstan:check diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..01cba50 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,87 @@ +name: Unit Tests + +on: + pull_request: + push: + branches: + - 1.x + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tests: + strategy: + fail-fast: false + matrix: + php-version: + - 8.2 + - 8.3 + os: # always use the latest runners + - ubuntu-24.04 + - windows-2022 + + name: Unit Tests + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - name: Setup Git for Windows + if: matrix.os == 'windows-2022' + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Get composer cache directory + id: cache-dir + shell: bash + run: | + echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.cache-dir.outputs.COMPOSER_CACHE_DIR }} + key: ${{ github.workflow }}-PHP_${{ matrix.php-version }}-${{ matrix.os }}-${{ hashFiles('**/composer.json') }}-${{ github.run_id }} + restore-keys: | + ${{ github.workflow }}-PHP_${{ matrix.php-version }}-${{ matrix.os }}-${{ hashFiles('**/composer.json') }}- + ${{ github.workflow }}-PHP_${{ matrix.php-version }}-${{ matrix.os }}- + ${{ github.workflow }}-PHP_${{ matrix.php-version }}- + + - name: Install dependencies + run: composer update --ansi --no-scripts + + - name: Run Unit Tests + run: | + bin/parallel-phpunit ${{ env.COVERAGE_OPTION }} + env: + COVERAGE_OPTION: ${{ matrix.os == 'windows-2022' && '' || '--coverage' }} + + - name: Display structure of coverage files + run: ls -la + working-directory: build/cov + + - name: Merge coverage files into Clover + run: | + composer global require phpunit/phpcov --ansi + phpcov merge --clover build/phpunit/clover.xml build/cov + + - name: Upload coverage to Coveralls + run: | + composer global require php-coveralls/php-coveralls --ansi + php-coveralls --verbose --exclude-no-stmt --ansi --coverage_clover build/phpunit/clover.xml --json_path build/phpunit/coveralls-upload.json + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + COVERALLS_FLAG_NAME: ${{ format('PHP {0}-{1}', matrix.php-version, matrix.os) }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f864243..e85f15e 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -37,6 +37,7 @@ ]) ->append([ __FILE__, + 'bin/parallel-phpunit', 'bin/prune-cache', ]) ; diff --git a/bin/parallel-phpunit b/bin/parallel-phpunit new file mode 100755 index 0000000..2152066 --- /dev/null +++ b/bin/parallel-phpunit @@ -0,0 +1,15 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +require __DIR__.'/parallel-phpunit.php'; diff --git a/bin/parallel-phpunit.php b/bin/parallel-phpunit.php new file mode 100644 index 0000000..ba94eeb --- /dev/null +++ b/bin/parallel-phpunit.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +if (PHP_SAPI !== 'cli') { + echo "\033[31mFAIL\033[0m This script must be run from the command line.\n"; + + exit(1); +} + +$exit = 0; +$args = $argv; +$coverage = false; +$runsOnGithubActions = (bool) getenv('GITHUB_ACTIONS'); + +if (in_array('--coverage', $args, true)) { + $coverage = true; + $args = array_flip($args); + unset($args['--coverage']); + $args = array_values(array_flip($args)); +} + +$directory = $args[1] ?? 'src/Nexus'; +$components = []; + +if (! is_dir($directory)) { + echo sprintf("\033[31mFAIL\033[0m The \"%s\" directory does not exist.\n", $directory); + + exit(1); +} + +$finder = new RecursiveDirectoryIterator($directory, FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS); +$finder = new RecursiveIteratorIterator($finder); +$finder->setMaxDepth(3); + +/** @var SplFileInfo $splFileInfo */ +foreach ($finder as $file => $splFileInfo) { + if ('composer.json' === $file) { + $components[] = dirname($splFileInfo->getPathname()); + } +} + +$githubActionsGroup = static function (string $component, string $message, int $exitCode) use ($runsOnGithubActions): string { + if (! $runsOnGithubActions) { + if ($exitCode > 0) { + return sprintf("%2\$s\n\033[31mFAIL\033[0m %1\$s\n", $component, $message); + } + + return sprintf("\n\033[32mOK\033[0m %1\$s\n", $component); + } + + if ($exitCode > 0) { + return sprintf("%1\$s\n%2\$s\n\033[31mFAIL\033[0m %1\$s\n", $component, $message); + } + + return sprintf( + << %1$s/phpunit.stdout 2> %1$s/phpunit.stderr', $component); + + if (DIRECTORY_SEPARATOR === '\\') { + $cmd = sprintf('cmd /v:on /d /c "(%s)%s"', $cmd, $redirects); + } else { + $cmd .= $redirects; + } + + return $cmd; +}; +$runningProcesses = []; + +foreach ($components as $component) { + $process = proc_open($phpunitCommand($component), [], $pipes); + + if (false !== $process) { + $runningProcesses[$component] = $process; + } else { + $exit = 1; + echo "\n\033[31mFAIL\033[0m {$component}\n"; + } +} + +$lastOutput = null; +$lastOutputTime = 0; + +while ([] !== $runningProcesses) { + usleep(300_000); + $terminatedProcesses = []; + + foreach ($runningProcesses as $component => $process) { + $processStatus = proc_get_status($process); + + if (! $processStatus['running']) { + $terminatedProcesses[$component] = [$processStatus['command'], $processStatus['exitcode']]; + unset($runningProcesses[$component]); + proc_close($process); + } + } + + if ([] === $terminatedProcesses && count($runningProcesses) === 1) { + $component = key($runningProcesses); + $process = $runningProcesses[$component]; + + $output = file_get_contents(sprintf('%s/phpunit.stdout', $component)); + $output .= file_get_contents(sprintf('%s/phpunit.stderr', $component)); + + if ($lastOutput !== $output) { + $lastOutput = $output; + $lastOutputTime = microtime(true); + } elseif (microtime(true) - $lastOutputTime > 60) { + echo "\n\033[31mFAIL\033[0m Timeout {$component}\n"; + + if (DIRECTORY_SEPARATOR === '\\') { + exec(sprintf('taskkill /F /T /PID %d 2>&1', proc_get_status($process)['pid'])); + } else { + proc_terminate($process); + } + } + } + + foreach ($terminatedProcesses as $component => [$command, $status]) { + $output = $command."\n\n"; + + foreach (['out', 'err'] as $file) { + $file = sprintf('%s/phpunit.std%s', $component, $file); + $output .= file_get_contents($file); + unlink($file); + } + + echo $githubActionsGroup($component, $output, $status); + } +} + +exit($exit);