diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e613e3e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +{ + "image": "opencodeco/phpctl:php83-devcontainer", + "containerEnv": { + "PHP_VERSION": "83" + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers-contrib/features/devcontainers-cli:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {}, + "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": { + "packages": "parallel" + } + } +} diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml new file mode 100644 index 0000000..61d9258 --- /dev/null +++ b/.github/workflows/devcontainer.yml @@ -0,0 +1,48 @@ +name: Dev Container + +on: + pull_request: + push: + branches: + - main + +jobs: + devcontainer: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - php: 81 + php-str: 8.1 + - php: 82 + php-str: 8.2 + - php: 83 + php-str: 8.3 + + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and run dev container task + uses: devcontainers/ci@v0.3 + env: + PHP_VERSION: ${{ matrix.php }} + PHP_VERSION_STR: ${{ matrix.php-str }} + with: + subFolder: src-devc + imageName: opencodeco/phpctl + imageTag: php${{ matrix.php }}-devcontainer + push: always diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cf04688..ebbf8c7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,5 +1,4 @@ name: Docker - on: pull_request: push: @@ -11,10 +10,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: - - 81 - - 82 - - 83 + include: + - alpine: '3.19' + php: 81 + without-watchr: 1 + - alpine: '3.20' + php: 82 + without-watchr: '' + - alpine: '3.20' + php: 83 + without-watchr: '' steps: - name: Checkout @@ -37,13 +42,17 @@ jobs: with: context: . load: true - build-args: PHP=${{ matrix.php }} + build-args: | + ALPINE=${{ matrix.alpine }} + PHP=${{ matrix.php }} + WITHOUT_WATCHR=${{ matrix.without-watchr }} tags: opencodeco/phpctl:php${{ matrix.php }} - name: Test env: PHP_VERSION: ${{ matrix.php }} PHPCTL_TTY: --label=no-tty + WITHOUT_WATCHR: ${{ matrix.without-watchr }} run: | ./lib/bashunit ./tests/ - @@ -52,5 +61,7 @@ jobs: with: context: . push: true - build-args: PHP=${{ matrix.php }} + build-args: | + ALPINE=${{ matrix.alpine }} + PHP=${{ matrix.php }} tags: opencodeco/phpctl:php${{ matrix.php }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ce75d7..2145d8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,9 @@ Feel free to contribute to this project by submitting a pull request to the `mai You can use `make` to simplify the development process. +> [!NOTE] +> Make sure you have [GNU/Parallel](https://www.gnu.org/software/parallel/) installed to speed up processes for each PHP version. + ### Building You can build the default image using `make build` or just `make`, the `build` target is the default: ```shell diff --git a/Dockerfile b/Dockerfile index 9c2d0f1..9c98734 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,26 @@ -ARG ALPINE=3.19 -FROM alpine:${ALPINE} +ARG ALPINE=3.20 +FROM alpine:$ALPINE + ARG PHP +ENV PHP_VERSION=$PHP + +ARG WITH_EXAKAT +ENV WITH_EXAKAT=$WITH_EXAKAT + +ARG WITHOUT_WATCHR +ENV WITHOUT_WATCHR=$WITHOUT_WATCHR + COPY rootfs / -RUN apk add --update --no-cache \ +RUN apk update && apk upgrade && apk add --no-cache \ git \ docker-cli \ php${PHP}-cli \ php${PHP}-ctype \ + php${PHP}-curl \ php${PHP}-dom \ + php${PHP}-ffi \ php${PHP}-fileinfo \ + php${PHP}-gd \ php${PHP}-iconv \ php${PHP}-mbstring \ php${PHP}-mysqlnd \ @@ -16,6 +28,7 @@ RUN apk add --update --no-cache \ php${PHP}-pcntl \ php${PHP}-pdo \ php${PHP}-pdo_mysql \ + php${PHP}-pdo_pgsql \ php${PHP}-phar \ php${PHP}-posix \ php${PHP}-simplexml \ @@ -26,6 +39,8 @@ RUN apk add --update --no-cache \ php${PHP}-xmlreader \ php${PHP}-xmlwriter \ php${PHP}-zip \ + php${PHP}-pecl-decimal \ + php${PHP}-pecl-ds \ php${PHP}-pecl-mongodb \ php${PHP}-pecl-pcov \ php${PHP}-pecl-rdkafka \ @@ -35,5 +50,10 @@ RUN apk add --update --no-cache \ && ln -sf /usr/bin/php${PHP} /usr/bin/php \ && mv /etc/php/php.ini /etc/php${PHP}/conf.d/zzphp.ini \ && /usr/local/bin/install-tools +ARG HOST_USER +RUN apk add doas; \ + adduser ${HOST_USER}; \ + echo "${HOST_USER}" | chpasswd; \ + echo "permit ${HOST_USER} as root"> /etc/doas.d/doas.conf ENTRYPOINT [ "/usr/bin/php" ] CMD [ "-v" ] diff --git a/Makefile b/Makefile index 26d428f..8be10a7 100644 --- a/Makefile +++ b/Makefile @@ -3,24 +3,17 @@ default: build .PHONY: build build: - phpctl build + @parallel --line-buffer PHP_VERSION={} ./bin/phpctl build ::: 81 82 83 .PHONY: test test: - @PHPCTL_TTY=--label=no-tty COMPOSER_AUTH= TERM= ./lib/bashunit ./tests/ + @parallel --line-buffer PHP_VERSION={} COMPOSER_AUTH= TERM= ./bin/notty ./lib/bashunit ./tests/ ::: 81 82 83 + @[ -f phpctl.ini ] && rm phpctl.ini .PHONY: install install: - @sudo ln -sf $(shell pwd)/bin/composer /usr/local/bin/composer - @sudo ln -sf $(shell pwd)/bin/php /usr/local/bin/php - @sudo ln -sf $(shell pwd)/bin/php-cs-fixer /usr/local/bin/php-cs-fixer - @sudo ln -sf $(shell pwd)/bin/phpctl /usr/local/bin/pctl - @sudo ln -sf $(shell pwd)/bin/phpctl /usr/local/bin/phpctl - @sudo ln -sf $(shell pwd)/bin/phpstan /usr/local/bin/phpstan - @sudo ln -sf $(shell pwd)/bin/phpunit /usr/local/bin/phpunit - @sudo ln -sf $(shell pwd)/bin/infection /usr/local/bin/infection - @sudo ln -sf $(shell pwd)/bin/pest /usr/local/bin/pest - @sudo ln -sf $(shell pwd)/bin/pint /usr/local/bin/pint - @sudo ln -sf $(shell pwd)/bin/exakat /usr/local/bin/exakat - @sudo ln -sf $(shell pwd)/bin/frankenphp /usr/local/bin/frankenphp - @sudo ln -sf $(shell pwd)/bin/rector /usr/local/bin/rector + @sudo ./scripts/symlink-bins.sh + +.PHONY: devcontainers +devcontainers: + @parallel --line-buffer PHP_VERSION={} ./src-devc/build.sh ::: 81 82 83 diff --git a/README.md b/README.md index 38d5303..8884908 100644 --- a/README.md +++ b/README.md @@ -7,216 +7,51 @@

phpctl phpctl docker badge + phpctl docker badge phpctl frankenphp badge

🐳 A Docker-based development environment for PHP 🐘

Heavily inspired by opencodeco/hfctl.
+

Open in GitHub Codespaces

-## Introduction +## Frictionless PHP Development -**Frictionless PHP Development!** Seamlessly run and switch between different versions of PHP, with different installed extensions, thanks to the power of containers. +Seamlessly run and switch between different versions of PHP, with different installed extensions, thanks to the power of containers. Take the advantage of goodie commands like `phpctl create` to start a new project, `phpctl repl` to start a REPL, `phpctl init` to initialize a new configuration file **and a lot more**. -## Getting started - -> [!TIP] -> Pro-tip: use it with [`stack`](https://github.com/opencodeco/stack) to spin up infrastructure components like MySQL, Redis, RabbitMQ etc. - -### Requirements -- Git -- Docker - -### Install -If you want to install it system-wide (at `/usr/local/bin`), run: +### Just install ```shell -sh <(wget -qO- https://raw.githubusercontent.com/opencodeco/phpctl/main/installer.sh) +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/phpctl/refs/heads/main/docs/install.sh)" ``` - -You can also pass an argument to install at a custom location (e.g. `~/bin`), but you have to make sure that folder is in your `$PATH` variable. +### And that is it! +Try it out: ```shell -sh <(wget -qO- https://raw.githubusercontent.com/opencodeco/phpctl/main/installer.sh) ~/bin +phpctl doctor +php --version +composer --version ``` -#### Update -You can re-run the installer or use the `self-update` command: -```shell -phpctl self-update +Using [Dev Containers](https://containers.dev/)? We have a pre-built image: +```json +{ + "image": "opencodeco/phpctl:php83-devcontainer" +} ``` +In fact, we use it ourselves to develop `phpctl` itself: [devcontainer.json](.devcontainer/devcontainer.json). -## Usage -Then you can use `phpctl` or `pctl` with a subcommand: -```shell -phpctl [arguments] -``` -For example -```shell -phpctl php -m # To show built-in modules -``` -Or -```shell -phpctl sh echo 'Hello, World!' # To run arbitrary sh commands inside the container -``` - -## Command list - -### Developing -| Command | Description | -|-----------------------------|---------------------------------------------------------------------------------------------| -| `php` | **Runs PHP commands** (`phpctl php -v` or `phpctl php -m`). | -| `composer` | Runs Composer commands (`phpctl composer install` or `pctl composer validate`). | -| `server [port] [directory]` | Runs PHP's built-in web-server (default port is `80` and default directory is current `.`). | -| `sh [commands]` | Starts an interactive Shell session or runs `sh` commands. | -| `repl` | Starts a PHP REPL session (powered by [PsySH](https://psysh.org/)). | -| `bundle` | Bundles a project into an image and ships it as a single binary file. | - -### Tools -| Command | Description | -|----------------|-------------------------------------------------------------------------------------------------------------------------------| -| `phpunit` | [PHPUnit](https://phpunit.de) is a programmer-oriented testing framework for PHP. | -| `php-cs-fixer` | [PHP Coding Standards Fixer (PHP CS Fixer)](https://cs.symfony.com/) fixes your code to follow standards. | -| `phpstan` | [PHPStan](https://phpstan.org/) finds bugs in your code without writing tests. It's open-source and free. | -| `infection` | [Infection](https://infection.github.io) is a Mutation Testing Framework. | -| `pest` | [Pest](https://pestphp.com) is a testing framework with a focus on simplicity. | -| `pint` | [Pint](https://github.com/laravel/pint) is an opinionated PHP code style fixer for minimalists. | -| `box` | [Box](https://github.com/box-project/box): fast, zero config application bundler with PHARs. | -| `exakat` | [Exakat](https://www.exakat.io) is a real time customizable static analyzer engine that analyse and fix code. | -| `frankenphp` | [FrankenPHP](https://frankenphp.dev): the Modern PHP App Server, written in Go. | -| `rector` | [Rector](https://getrector.com) is a tool that you can run on any project to get an instant upgrade or automated refactoring. | - -### Scaffolders -| Command | Description | -|----------------------------|---------------------------------------------------------------| -| `create [framework] [dir]` | Creates a new project using the given framework (or package). | -| `init [skeleton]` | Initializes a skeleton configuration. | - -#### Skeletons -- `phpunit` -- `php-cs-fixer` -- `phpstan` -- `infection` -- `box` - -### Helpers -| Command | Description | -|-----------------|-------------------------------------------------------------| -| `help` or `man` | Displays a help message. | -| `self-update` | Updates `phpctl` iself. | -| `doctor` | Inspects the current `PHP_VERSION` and `PHPCTL_IMAGE`. | -| `build` | Builds the current `Dockerfile` (useful for custom images). | -| `images` | Shows local `phpctl` images. | - -## The `.phpctlrc` file -You can also add a `.phpctlrc` file at project's root directory with some overrides like: - -### Environment variables -| Variable | Description | -|----------------|-----------------------------------| -| `PHP_VERSION` | Values can be `82` and `83` | -| `PHPCTL_IMAGE` | Use to name your own custom image | - -For example: -```shell -PHP_VERSION=83 -``` - -### Docker behaviour - -#### Run options - -You can also provide any additional [Docker `run` arguments](https://docs.docker.com/engine/reference/commandline/run/#options) using the `args` variable. - -For example, suppose you want to bind the `9501` port from the running `phpctl` container to your host, -you can add the following to your `.phpctlrc` file: -```shell -args=(-p 9501:9501) -``` - -#### Build options - -You can also provide [build options](https://docs.docker.com/engine/reference/commandline/build/) to the build command using the `build` variable: -```shell -build=(--build-arg APP_ENV=dev --label phprocks) -``` - -### Podman - -You can use Podman instead of Docker by setting the `PHPCTL_RUNTIME` variable to `podman` in your environment variables or at `.phpctlrc` file. -```shell -PHPCTL_RUNTIME=podman phpctl php -v -``` - -## The `phpctl.ini` file -You can also add a `phpctl.ini` file at project's root directory to set any [`php.ini` directive](https://www.php.net/manual/en/ini.list.php). -```ini -memory_limit = 1337M -``` - -```shell -$ phpctl php -i | grep memory_limit -memory_limit => 1337M => 1337M -``` - -## Modules -
-For the default Docker image we have the following modules installed (click to expand). -
-[PHP Modules]
-Core
-ctype
-curl
-date
-dom
-fileinfo
-filter
-hash
-iconv
-igbinary
-json
-libxml
-mbstring
-mongodb
-msgpack
-mysqlnd
-openssl
-pcntl
-pcov
-pcre
-PDO
-pdo_mysql
-Phar
-posix
-random
-rdkafka
-readline
-redis
-Reflection
-session
-SimpleXML
-sockets
-sodium
-SPL
-sqlite3
-standard
-swoole
-tokenizer
-xml
-xmlreader
-xmlwriter
-zip
-zlib
-
-
- -You can always use your custom image with the `PHPCTL_IMAGE` variable, but feel free to ask for more essential extensions in the issues. +## Getting started -## Why it exists? -> [!TIP] -> After some years struggling with different PHP distributions into different operating systems, dealing with different PHP versions and sets of extensions, -> I came out with `phpctl` to use the power of containers to seamlessly run PHP :elephant: for developement environments. +- [Installation guide](https://phpctl.dev/#installation) +- [How to use](https://phpctl.dev/#usage) +- [Available commands](https://phpctl.dev/commands) +- [The `.phpctlrc` file](https://phpctl.dev/phpctlrc) +- [The `phpctl.ini` file](https://phpctl.dev/phpctlini) +- [Available extensions](https://phpctl.dev/extensions) +- [Why it exists?](https://phpctl.dev/why) ## Contributing Click here to read the [contributing guidelines](CONTRIBUTING.md). diff --git a/bin/co-phpunit b/bin/co-phpunit new file mode 100755 index 0000000..c74f5c3 --- /dev/null +++ b/bin/co-phpunit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl co-phpunit $@ diff --git a/bin/composer-require-checker b/bin/composer-require-checker new file mode 100755 index 0000000..c825e4b --- /dev/null +++ b/bin/composer-require-checker @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl composer-require-checker $@ diff --git a/bin/couscous b/bin/couscous new file mode 100755 index 0000000..b1a31e7 --- /dev/null +++ b/bin/couscous @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl couscous $@ diff --git a/bin/deptrac b/bin/deptrac new file mode 100755 index 0000000..0efe0f4 --- /dev/null +++ b/bin/deptrac @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl deptrac $@ diff --git a/bin/infection b/bin/infection old mode 100644 new mode 100755 diff --git a/bin/notty b/bin/notty new file mode 100755 index 0000000..134f289 --- /dev/null +++ b/bin/notty @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +PHPCTL_TTY="--label=no-tty" $@ diff --git a/bin/phpcbf b/bin/phpcbf new file mode 100755 index 0000000..a41a85a --- /dev/null +++ b/bin/phpcbf @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl phpcbf $@ diff --git a/bin/phpcs b/bin/phpcs new file mode 100755 index 0000000..9882622 --- /dev/null +++ b/bin/phpcs @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl phpcs $@ diff --git a/bin/phpctl b/bin/phpctl index 3b5bcbc..4a42601 100755 --- a/bin/phpctl +++ b/bin/phpctl @@ -1,5 +1,12 @@ #!/usr/bin/env bash PHPCTL_DIR=$(dirname "$(realpath "0ドル")")/../ + +if [ -s "$HOME/.phpctlrc" ]; then + set -a + . "$HOME/.phpctlrc" + set +a +fi + if [ -s .phpctlrc ]; then set -a . .phpctlrc @@ -9,7 +16,20 @@ fi PHP_VERSION=${PHP_VERSION:-82} PHPCTL_IMAGE=${PHPCTL_IMAGE:-opencodeco/phpctl:php$PHP_VERSION} PHPCTL_TTY=${PHPCTL_TTY:--it} -PHPCTL_RUNTIME=${PHPCTL_RUNTIME:-docker} +PHPCTL_RUNTIME=${PHPCTL_RUNTIME:-detect} +PHPCTL_USER=${PHPCTL_USER:-root} + +if [ "${PHPCTL_RUNTIME}" == "detect" ]; then + PHPCTL_RUNTIME= + if command -v docker> /dev/null 2>&1; then + PHPCTL_RUNTIME=docker + elif command -v podman> /dev/null 2>&1; then + PHPCTL_RUNTIME=podman + else + echo "Could not find neither \"docker\" nor \"podman\", aborting" + exit 1 + fi +fi for file in "$PHPCTL_DIR"/src/*.sh; do # shellcheck source=src/php.sh diff --git a/bin/phpmd b/bin/phpmd new file mode 100755 index 0000000..9d11a09 --- /dev/null +++ b/bin/phpmd @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl phpmd $@ diff --git a/bin/watchr b/bin/watchr new file mode 100755 index 0000000..47dc592 --- /dev/null +++ b/bin/watchr @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +phpctl watchr $@ diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..4c18aa5 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +phpctl.dev \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..1245489 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,29 @@ +title: phpctl +description: Frictionless PHP Development +remote_theme: just-the-docs/just-the-docs +url: https://phpctl.dev +repository: https://github.com/opencodeco/phpctl +permalink: pretty + +aux_links: + phpctl on GitHub: + - https://github.com/opencodeco/phpctl + +nav_external_links: + - title: phpctl on GitHub + url: https://github.com/opencodeco/phpctl + +back_to_top: true +back_to_top_text: "Back to top" + +gh_edit_link: true +gh_edit_link_text: Edit this page on GitHub +gh_edit_repository: https://github.com/opencodeco/phpctl +gh_edit_branch: main +gh_edit_source: docs +gh_edit_view_mode: tree + +plugins: + - jekyll-remote-theme + - jekyll-seo-tag + - jekyll-github-metadata diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..f95efa1 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,63 @@ +--- +nav_order: 2 +--- + +# Available commands + +## Developing + +| Command | Description | +|:-----------------------------|:---------------------------------------------------------------------------------------------| +| `php` | **Runs PHP commands** (`phpctl php -v` or `phpctl php -m`). | +| `composer` | Runs Composer commands (`phpctl composer install` or `pctl composer validate`). | +| `server [port] [directory]` | Runs PHP's built-in web-server (default port is `80` and default directory is current `.`). | +| `sh [commands]` | Starts an interactive Shell session or runs `sh` commands. | +| `repl` | Starts a PHP REPL session (powered by [PsySH](https://psysh.org/)). | +| `bundle` | Bundles a project into an image and ships it as a single binary file. | + +## Tools + +| Command | Description | +|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `box` | [Box](https://github.com/box-project/box): fast, zero config application bundler with PHARs. | +| `co-phpunit` | [co-phpunit](https://github.com/hyperf/testing) is a Coroutine-aware PHPUnit for testing Hyperf projects. | +| `composer-require-checker` | [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker): A CLI tool to analyze composer dependencies and verify that no unknown symbols are used in the sources of a package. | +| `couscous` | [Couscous](https://github.com/CouscousPHP/Couscous): Couscous generates a GitHub pages website from your markdown documentation. | +| `deptrac` | [Deptrac](https://github.com/qossmic/deptrac): Deptrac is a static code analysis tool for PHP that helps you communicate, visualize and enforce architectural decisions in your projects. | +| `exakat` | [Exakat](https://www.exakat.io) is a real time customizable static analyzer engine that analyse and fix code. | +| `frankenphp` | [FrankenPHP](https://frankenphp.dev): the Modern PHP App Server, written in Go. | +| `infection` | [Infection](https://infection.github.io) is a Mutation Testing Framework. | +| `pest` | [Pest](https://pestphp.com) is a testing framework with a focus on simplicity. | +| `php-cs-fixer` | [PHP Coding Standards Fixer (PHP CS Fixer)](https://cs.symfony.com/) fixes your code to follow standards. | +| `phpcbf` | [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) is an essential development tool that ensures your code remains clean and consistent. | +| `phpcs` | [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) is an essential development tool that ensures your code remains clean and consistent. | +| `phpmd` | [PHP Mess Detector](https://phpmd.org/) looks for several potential problems within your source code. | +| `phpstan` | [PHPStan](https://phpstan.org/) finds bugs in your code without writing tests. It's open-source and free. | +| `phpunit` | [PHPUnit](https://phpunit.de) is a programmer-oriented testing framework for PHP. | +| `pint` | [Pint](https://github.com/laravel/pint) is an opinionated PHP code style fixer for minimalists. | +| `rector` | [Rector](https://getrector.com) is a tool that you can run on any project to get an instant upgrade or automated refactoring. | +| `watchr` | [watchr](https://github.com/flavioheleno/watchr): command-line utility to explore and validate domain names and certificates. | + +## Scaffolders + +| Command | Description | +|:----------------------------|:---------------------------------------------------------------| +| `create [framework] [dir]` | Creates a new project using the given framework (or package). | +| `init [skeleton]` | Initializes a skeleton configuration. | + +### Skeletons +- `phpunit` +- `php-cs-fixer` +- `phpstan` +- `infection` +- `box` + +## Helpers + +| Command | Description | +|:-----------------|:-------------------------------------------------------------| +| `help` or `man` | Displays a help message. | +| `self-update` | Updates `phpctl` iself. | +| `doctor` | Inspects the current `PHP_VERSION` and `PHPCTL_IMAGE`. | +| `build` | Builds the current `Dockerfile` (useful for custom images). | +| `images` | Shows local `phpctl` images. | diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 0000000..fb73ba5 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,57 @@ +--- +nav_order: 5 +--- + +# Available extensions +
+For the default Docker image we have the following modules installed (click to expand). +
+Core
+ctype
+curl
+date
+decimal
+dom
+ds
+fileinfo
+filter
+hash
+iconv
+igbinary
+json
+libxml
+mbstring
+mongodb
+msgpack
+mysqlnd
+openssl
+pcntl
+pcov
+pcre
+PDO
+pdo_mysql
+Phar
+posix
+random
+rdkafka
+readline
+redis
+Reflection
+session
+SimpleXML
+sockets
+sodium
+SPL
+sqlite3
+standard
+swoole
+tokenizer
+xml
+xmlreader
+xmlwriter
+zip
+zlib
+
+
+ +You can always use your custom image with the `PHPCTL_IMAGE` variable, but feel free to ask for more essential extensions in the issues. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..38b21f8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,74 @@ +--- +title: Frictionless PHP Development +nav_order: 1 +permalink: / +--- + +# Frictionless PHP Development + +[Get started now](#getting-started){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[View it on GitHub](https://github.com/opencodeco/phpctl){: .btn .fs-5 .mb-4 .mb-md-0 } + +Seamlessly run and switch between different versions of PHP, with different installed extensions, thanks to the power of containers. + +Take the advantage of goodies commands like `phpctl create` to start a new project, `phpctl repl` to start a REPL, `phpctl init` to initialize a new configuration file **and a lot more**. + +## Getting started + +### Installation + +```shell +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/phpctl/refs/heads/main/docs/install.sh)" +``` + +**That is it!** Now you have `phpctl` available in your system. + +#### Custom installation +You can also pass an argument to install at a custom location (e.g. `~/bin`), but you have to make sure that folder is in your `$PATH` variable. +```shell +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/phpctl/refs/heads/main/docs/install.sh)" ~/bin +``` + +#### Homebrew +```shell +brew install opencodeco/phpctl/phpctl +``` + +Or add the `oppencodeco` tap with `brew tap opencodeco/phpctl` and then `brew install phpctl`. + +### Update +You can re-run the installer or use the `self-update` command: +```shell +phpctl self-update +``` + +For those using Homebrew `brew upgrade opencodeco/phpctl/phpctl` or when run `brew update`. + +### Usage + +Simple as calling any other command in your terminal: + +```shell +phpctl [options] +``` + +You will also have aliases for the most common commands available without the `phpctl` prefix, like: +- `php` +- `composer` +- `phpunit` +- `php-cs-fixer` +- etc + +So you can use them as you would normally do: + +```shell +php -v # same as `phpctl php -v` +``` + +Or + +```shell +composer --version # same as `phpctl composer --version` +``` + +**Have fun!** Feel free to open any issues or PRs. \ No newline at end of file diff --git a/docs/install.sh b/docs/install.sh new file mode 100755 index 0000000..0a536e3 --- /dev/null +++ b/docs/install.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +INSTALL_DIR=~/.phpctl +if [ -z "1ドル" ]; then + SUDO=sudo + SYMLINK_DIR=/usr/local/bin +else + SUDO="" + SYMLINK_DIR=1ドル +fi + +echo -e "033円[0;33mInstalling phpctl at 033円[0m$INSTALL_DIR" +if [ -d "$INSTALL_DIR" ]; then + echo "The install directory is not empty. Attempting to remove it..." + rm -rf $INSTALL_DIR +fi + +echo -n "" +git clone --quiet https://github.com/opencodeco/phpctl.git $INSTALL_DIR & +PID=$! +while kill -0 $PID 2> /dev/null; do + for CHAR in '-' '/' '|' '\'; do + printf "\b$CHAR" + sleep 0.1 + done +done +printf "\r" + + +if [ -z "1ドル" ]; then + echo -n "Sudo will be prompted to symlink the phpctl files." +else + echo -n "Files will be symlinked to ${SYMLINK_DIR}." +fi +echo -e -n " 033円[0;32mDo you want to continue? (Y/n)033円[0m " +read -r answer +if [ "$answer" != "${answer#[Nn]}" ]; then + echo -e "033円[0;31mTo use phpctl globally, link the cloned script to your bin directory, like:033円[0m" + echo "" + for file in "${INSTALL_DIR}"/bin/*; do + bin=$(basename "$file") + echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/$bin ${SYMLINK_DIR}/$bin" + done +else + $SUDO ${INSTALL_DIR}/scripts/symlink-bins.sh ${INSTALL_DIR} +fi diff --git a/docs/phpctlini.md b/docs/phpctlini.md new file mode 100644 index 0000000..39d7711 --- /dev/null +++ b/docs/phpctlini.md @@ -0,0 +1,14 @@ +--- +nav_order: 4 +--- + +# The `phpctl.ini` file +You can also add a `phpctl.ini` file at project's root directory to set any [`php.ini` directive](https://www.php.net/manual/en/ini.list.php). +```ini +memory_limit = 1337M +``` + +```shell +$ phpctl php -i | grep memory_limit +memory_limit => 1337M => 1337M +``` diff --git a/docs/phpctlrc.md b/docs/phpctlrc.md new file mode 100644 index 0000000..2f0d9ae --- /dev/null +++ b/docs/phpctlrc.md @@ -0,0 +1,51 @@ +--- +nav_order: 3 +--- + +# The `.phpctlrc` file +You can also add a `.phpctlrc` file at project's root directory with some overrides like: + +## Environment variables + +| Variable | Description | +|:----------------|:-----------------------------------| +| `PHP_VERSION` | Values can be `82` and `83` | +| `PHPCTL_IMAGE` | Use to name your own custom image | + +For example: +```shell +PHP_VERSION=83 +``` + +## Docker behaviour + +### Run options + +You can also provide any additional [Docker `run` arguments](https://docs.docker.com/engine/reference/commandline/run/#options) using the `args` variable. + +For example, suppose you want to bind the `9501` port from the running `phpctl` container to your host, +you can add the following to your `.phpctlrc` file: +```shell +args=(-p 9501:9501) +``` + +### Build options + +You can also provide [build options](https://docs.docker.com/engine/reference/commandline/build/) to the build command using the `build` variable: +```shell +build=(--build-arg APP_ENV=dev --label phprocks) +``` + +## Podman + +You can use Podman instead of Docker by setting the `PHPCTL_RUNTIME` variable to `podman` in your environment variables or at `.phpctlrc` file. +```shell +PHPCTL_RUNTIME=podman phpctl php -v +``` + +## Host user + +By default, `phpctl` creates an user considering host user. You can change from `root` to host user through `PHPCTL_USER` variable. +```shell +PHPCTL_USER=your_user phpctl sh whoami +``` diff --git a/docs/uninstall.sh b/docs/uninstall.sh new file mode 100644 index 0000000..ba40433 --- /dev/null +++ b/docs/uninstall.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +INSTALL_DIR=~/.phpctl +SYMLINK_DIR=/usr/local/bin + +LINKS=( + composer + composer-require-checker + co-phpunit + couscous + deptrac + exakat + frankenphp + infection + notty + pest + php + phpcbf + phpcs + php-cs-fixer + phpctl + phpmd + phpstan + phpunit + pint + rector + watchr +) + +# Removing symlink +echo "Removing symbolic links..." +for link in "${LINKS[@]}"; do + if [ -L "${SYMLINK_DIR}/${link}" ]; then + sudo rm "${SYMLINK_DIR}/${link}" + echo "Removed ${SYMLINK_DIR}/${link}" + else + echo "Link ${SYMLINK_DIR}/${link} does not exist, skipping." + fi +done + +# Opcional: removing directory +echo -e "\nDo you want to remove the installation directory (${INSTALL_DIR})? (Y/n) " +read -r answer +if [ "$answer" != "${answer#[Yy]}" ]; then + rm -rf "$INSTALL_DIR" + echo "Removed installation directory: ${INSTALL_DIR}" +else + echo "Skipping removal of installation directory." +fi + +echo "Uninstallation complete." \ No newline at end of file diff --git a/docs/why.md b/docs/why.md new file mode 100644 index 0000000..aa23db3 --- /dev/null +++ b/docs/why.md @@ -0,0 +1,7 @@ +--- +nav_order: 6 +--- + +# Why it exists? + +After some years struggling with different PHP distributions into different operating systems, dealing with different PHP versions and sets of extensions, I came out with `phpctl` to use the power of containers to seamlessly run PHP for development environments. diff --git a/examples/pest/composer.lock b/examples/pest/composer.lock index fe3c9f4..fbc6d7a 100644 --- a/examples/pest/composer.lock +++ b/examples/pest/composer.lock @@ -104,16 +104,16 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", - "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", "shasum": "" }, "require": { @@ -145,9 +145,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" }, - "time": "2023-09-27T20:04:15+00:00" + "time": "2024-01-30T19:34:25+00:00" }, { "name": "fidry/cpu-core-counter", @@ -401,25 +401,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -427,7 +429,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -451,44 +453,44 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-01-07T17:17:35+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.0.1", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "0225cf94fc9aaf645bbc42b5fc838aace025e0e3" + "reference": "0d655ffbf3edf9b366e0eea5ab9c7871e0ab3357" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/0225cf94fc9aaf645bbc42b5fc838aace025e0e3", - "reference": "0225cf94fc9aaf645bbc42b5fc838aace025e0e3", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/0d655ffbf3edf9b366e0eea5ab9c7871e0ab3357", + "reference": "0d655ffbf3edf9b366e0eea5ab9c7871e0ab3357", "shasum": "" }, "require": { "filp/whoops": "^2.15.4", "nunomaduro/termwind": "^2.0.0", "php": "^8.2.0", - "symfony/console": "^7.0.1" + "symfony/console": "^7.0.2" }, "conflict": { "laravel/framework": "<11.0.0 ||>=12.0.0", - "phpunit/phpunit": "<10.5.1 ||>=11.0.0" + "phpunit/phpunit": "<10.5.1 ||>=12.0.0" }, "require-dev": { + "larastan/larastan": "^2.8.1", "laravel/framework": "^11.0.0", - "laravel/pint": "^1.13.6", - "laravel/sail": "^1.26.2", + "laravel/pint": "^1.13.8", + "laravel/sail": "^1.27.0", "laravel/sanctum": "^4.0.0", - "laravel/tinker": "dev-develop", - "nunomaduro/larastan": "^3.0.0", + "laravel/tinker": "^2.9.0", "orchestra/testbench-core": "^9.0.0", - "pestphp/pest": "^2.27.0", - "sebastian/environment": "^6.0.1" + "pestphp/pest": "^2.31.0 || ^3.0.0", + "sebastian/environment": "^6.0.1 || ^7.0.0" }, "type": "library", "extra": { @@ -550,7 +552,7 @@ "type": "patreon" } ], - "time": "2023-12-08T16:03:53+00:00" + "time": "2024-01-12T13:38:24+00:00" }, { "name": "nunomaduro/termwind", @@ -642,36 +644,36 @@ }, { "name": "pestphp/pest", - "version": "v2.30.0", + "version": "v2.33.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "97dc32f9d24b84dd071d9e89438a19e43c833f6f" + "reference": "eeade88ad236f881f044430e0e9fefaad255718f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/97dc32f9d24b84dd071d9e89438a19e43c833f6f", - "reference": "97dc32f9d24b84dd071d9e89438a19e43c833f6f", + "url": "https://api.github.com/repos/pestphp/pest/zipball/eeade88ad236f881f044430e0e9fefaad255718f", + "reference": "eeade88ad236f881f044430e0e9fefaad255718f", "shasum": "" }, "require": { "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.10.0|^8.0.1", + "nunomaduro/collision": "^7.10.0|^8.1.0", "nunomaduro/termwind": "^1.15.1|^2.0.0", "pestphp/pest-plugin": "^2.1.1", - "pestphp/pest-plugin-arch": "^2.5.0", + "pestphp/pest-plugin-arch": "^2.7.0", "php": "^8.1.0", - "phpunit/phpunit": "^10.5.5" + "phpunit/phpunit": "^10.5.9" }, "conflict": { - "phpunit/phpunit": ">10.5.5", + "phpunit/phpunit": ">10.5.9", "sebastian/exporter": "<5.1.0", "webmozart/assert": "<1.11.0" }, "require-dev": { "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.6.0", - "symfony/process": "^6.4.0|^7.0.0" + "pestphp/pest-plugin-type-coverage": "^2.8.0", + "symfony/process": "^6.4.0|^7.0.2" }, "bin": [ "bin/pest" @@ -734,7 +736,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.30.0" + "source": "https://github.com/pestphp/pest/tree/v2.33.2" }, "funding": [ { @@ -746,7 +748,7 @@ "type": "github" } ], - "time": "2023-12-28T10:36:40+00:00" + "time": "2024-01-29T12:50:00+00:00" }, { "name": "pestphp/pest-plugin", @@ -820,26 +822,26 @@ }, { "name": "pestphp/pest-plugin-arch", - "version": "v2.5.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "8d850753f0192c3fa1ed6c6cac6f76b718d131db" + "reference": "d23b2d7498475354522c3818c42ef355dca3fcda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/8d850753f0192c3fa1ed6c6cac6f76b718d131db", - "reference": "8d850753f0192c3fa1ed6c6cac6f76b718d131db", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/d23b2d7498475354522c3818c42ef355dca3fcda", + "reference": "d23b2d7498475354522c3818c42ef355dca3fcda", "shasum": "" }, "require": { - "nunomaduro/collision": "^7.10.0|^8.0.0", + "nunomaduro/collision": "^7.10.0|^8.1.0", "pestphp/pest-plugin": "^2.1.1", "php": "^8.1", - "ta-tikoma/phpunit-architecture-test": "^0.7.5" + "ta-tikoma/phpunit-architecture-test": "^0.8.4" }, "require-dev": { - "pestphp/pest": "^2.27.0", + "pestphp/pest": "^2.33.0", "pestphp/pest-dev-tools": "^2.16.0" }, "type": "library", @@ -875,7 +877,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v2.5.0" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v2.7.0" }, "funding": [ { @@ -887,7 +889,7 @@ "type": "github" } ], - "time": "2023-12-05T19:01:10+00:00" + "time": "2024-01-26T09:46:42+00:00" }, { "name": "phar-io/manifest", @@ -1112,16 +1114,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.3", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", "shasum": "" }, "require": { @@ -1164,22 +1166,22 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" }, - "time": "2023-08-12T11:01:26+00:00" + "time": "2024-01-11T11:49:22+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.5", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { @@ -1211,9 +1213,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-12-16T09:33:33+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1538,16 +1540,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.5", + "version": "10.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ed21115d505b4b4f7dc7b5651464e19a2c7f7856" + "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ed21115d505b4b4f7dc7b5651464e19a2c7f7856", - "reference": "ed21115d505b4b4f7dc7b5651464e19a2c7f7856", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", + "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", "shasum": "" }, "require": { @@ -1619,7 +1621,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.5" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.9" }, "funding": [ { @@ -1635,7 +1637,7 @@ "type": "tidelift" } ], - "time": "2023-12-27T15:13:52+00:00" + "time": "2024-01-22T14:35:40+00:00" }, { "name": "psr/container", @@ -2657,16 +2659,16 @@ }, { "name": "symfony/console", - "version": "v7.0.1", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cdce5c684b2f920bb1343deecdfba356ffad83d5" + "reference": "c5010d50f1ee4b25cfa0201d9915cf1b14071456" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cdce5c684b2f920bb1343deecdfba356ffad83d5", - "reference": "cdce5c684b2f920bb1343deecdfba356ffad83d5", + "url": "https://api.github.com/repos/symfony/console/zipball/c5010d50f1ee4b25cfa0201d9915cf1b14071456", + "reference": "c5010d50f1ee4b25cfa0201d9915cf1b14071456", "shasum": "" }, "require": { @@ -2730,7 +2732,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.0.1" + "source": "https://github.com/symfony/console/tree/v7.0.3" }, "funding": [ { @@ -2746,7 +2748,7 @@ "type": "tidelift" } ], - "time": "2023-12-01T15:10:06+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/finder", @@ -3144,16 +3146,16 @@ }, { "name": "symfony/process", - "version": "v7.0.0", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "13bdb1670c7f510494e04fcb2bfa29af63db9c0d" + "reference": "937a195147e0c27b2759ade834169ed006d0bc74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/13bdb1670c7f510494e04fcb2bfa29af63db9c0d", - "reference": "13bdb1670c7f510494e04fcb2bfa29af63db9c0d", + "url": "https://api.github.com/repos/symfony/process/zipball/937a195147e0c27b2759ade834169ed006d0bc74", + "reference": "937a195147e0c27b2759ade834169ed006d0bc74", "shasum": "" }, "require": { @@ -3185,7 +3187,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.0" + "source": "https://github.com/symfony/process/tree/v7.0.3" }, "funding": [ { @@ -3201,7 +3203,7 @@ "type": "tidelift" } ], - "time": "2023-11-20T16:43:42+00:00" + "time": "2024-01-23T15:02:46+00:00" }, { "name": "symfony/service-contracts", @@ -3287,16 +3289,16 @@ }, { "name": "symfony/string", - "version": "v7.0.0", + "version": "v7.0.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "92bd2bfbba476d4a1838e5e12168bef2fd1e6620" + "reference": "524aac4a280b90a4420d8d6a040718d0586505ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/92bd2bfbba476d4a1838e5e12168bef2fd1e6620", - "reference": "92bd2bfbba476d4a1838e5e12168bef2fd1e6620", + "url": "https://api.github.com/repos/symfony/string/zipball/524aac4a280b90a4420d8d6a040718d0586505ac", + "reference": "524aac4a280b90a4420d8d6a040718d0586505ac", "shasum": "" }, "require": { @@ -3353,7 +3355,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.0" + "source": "https://github.com/symfony/string/tree/v7.0.3" }, "funding": [ { @@ -3369,32 +3371,32 @@ "type": "tidelift" } ], - "time": "2023-11-29T08:40:23+00:00" + "time": "2024-01-29T15:41:16+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", - "version": "0.7.5", + "version": "0.8.4", "source": { "type": "git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", - "reference": "9eb08437e8f0c0c75cc947a373cf49672c335827" + "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/9eb08437e8f0c0c75cc947a373cf49672c335827", - "reference": "9eb08437e8f0c0c75cc947a373cf49672c335827", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/89f0dea1cb0f0d5744d3ec1764a286af5e006636", + "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636", "shasum": "" }, "require": { - "nikic/php-parser": "^4.15.4", + "nikic/php-parser": "^4.18.0 || ^5.0.0", "php": "^8.1.0", "phpdocumentor/reflection-docblock": "^5.3.0", - "phpunit/phpunit": "^10.1.1", - "symfony/finder": "^6.2.7 || ^7.0.0" + "phpunit/phpunit": "^10.5.5 || ^11.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0" }, "require-dev": { - "laravel/pint": "^1.9.0", - "phpstan/phpstan": "^1.10.13" + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" }, "type": "library", "autoload": { @@ -3426,9 +3428,9 @@ ], "support": { "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", - "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.7.5" + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.4" }, - "time": "2023-10-12T15:31:50+00:00" + "time": "2024-01-05T14:10:56+00:00" }, { "name": "theseer/tokenizer", diff --git a/examples/phpmd/.gitignore b/examples/phpmd/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/examples/phpmd/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/examples/phpmd/README.md b/examples/phpmd/README.md new file mode 100644 index 0000000..4342cec --- /dev/null +++ b/examples/phpmd/README.md @@ -0,0 +1,16 @@ +# PHP Mess Detector + +An example of [PHP Mess Detector](https://phpmd.org/) usage. + +Run `phpctl phpmd src text cleancode,controversial,design,naming,unusedcode` to get PHPMD output execution for [Example](./src/Example.php) class. + +You should have an output as following: +```shell +/usr/local/src/src/Example.php:6 LongVariable Avoid excessively long variable names like $thiIsAnAmazingVariable. Keep variable name length under 20. +/usr/local/src/src/Example.php:8 UnusedLocalVariable Avoid unused local variables such as '$anotherVariable'. +/usr/local/src/src/Example.php:11 CamelCaseMethodName The method snake_case_method is not named in camelCase. +/usr/local/src/src/Example.php:17 MissingImport Missing class import via use statement (line '17', column '23'). +/usr/local/src/src/Example.php:18 EmptyCatchBlock Avoid using empty try-catch blocks in uselessCatchBlock. +/usr/local/src/src/Example.php:24 UndefinedVariable Avoid using undefined variables such as '$age' which will lead to PHP notices. +/usr/local/src/src/Example.php:24 UnusedLocalVariable Avoid unused local variables such as '$age'. +``` \ No newline at end of file diff --git a/examples/phpmd/composer.json b/examples/phpmd/composer.json new file mode 100644 index 0000000..cd83c81 --- /dev/null +++ b/examples/phpmd/composer.json @@ -0,0 +1,10 @@ +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "require-dev": { + "phpmd/phpmd": "^2.15" + } +} diff --git a/examples/phpmd/composer.lock b/examples/phpmd/composer.lock new file mode 100644 index 0000000..0fe7da1 --- /dev/null +++ b/examples/phpmd/composer.lock @@ -0,0 +1,1005 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ba732fb5b882b63abfb5116571fdf5b7", + "packages": [], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc WΓΌrth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "dev", + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2023-12-11T08:22:20+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "symfony/config", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16", + "reference": "86a5027869ca3d6bdecae6d5d6c2f77c8f2c1d16", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-30T08:34:29+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "e915c6684b8e3ae90a4441f6823ebbb40edf0b92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e915c6684b8e3ae90a4441f6823ebbb40edf0b92", + "reference": "e915c6684b8e3ae90a4441f6823ebbb40edf0b92", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-30T08:34:29+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1fb79308cb5fc2b44bff6e8af10a5af6812e05b8", + "reference": "1fb79308cb5fc2b44bff6e8af10a5af6812e05b8", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/examples/phpmd/src/Example.php b/examples/phpmd/src/Example.php new file mode 100644 index 0000000..7d96bc2 --- /dev/null +++ b/examples/phpmd/src/Example.php @@ -0,0 +1,28 @@ + 10) { + echo "Number is greater than 10"; + } + } +} diff --git a/examples/swoole/.gitignore b/examples/swoole/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/examples/swoole/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/examples/swoole/composer.json b/examples/swoole/composer.json new file mode 100644 index 0000000..ab6d850 --- /dev/null +++ b/examples/swoole/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "swoole/ide-helper": "^5.1" + } +} diff --git a/examples/swoole/composer.lock b/examples/swoole/composer.lock new file mode 100644 index 0000000..bf9890b --- /dev/null +++ b/examples/swoole/composer.lock @@ -0,0 +1,51 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "87773a601065b2642cf9f7714bfada34", + "packages": [], + "packages-dev": [ + { + "name": "swoole/ide-helper", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/swoole/ide-helper.git", + "reference": "33ec7af9111b76d06a70dd31191cc74793551112" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/33ec7af9111b76d06a70dd31191cc74793551112", + "reference": "33ec7af9111b76d06a70dd31191cc74793551112", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Team Swoole", + "email": "team@swoole.com" + } + ], + "description": "IDE help files for Swoole.", + "support": { + "issues": "https://github.com/swoole/ide-helper/issues", + "source": "https://github.com/swoole/ide-helper/tree/5.1.2" + }, + "time": "2024-02-01T22:28:11+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/examples/swoole/server.php b/examples/swoole/server.php new file mode 100644 index 0000000..339ceda --- /dev/null +++ b/examples/swoole/server.php @@ -0,0 +1,17 @@ +on(\Swoole\Constant::EVENT_REQUEST, function (\Swoole\Http\Request $req, \Swoole\Http\Response $res) { + $res->end('Hello, World!'); +}); + +$srv->on(\Swoole\Constant::EVENT_START, function (\Swoole\Http\Server $srv) { + echo "Server started at http://localhost:{$srv->port}\n"; +}); + +$srv->start(); diff --git a/installer.sh b/installer.sh deleted file mode 100755 index 511c6f9..0000000 --- a/installer.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env sh - -INSTALL_DIR=~/.phpctl -if [ -z "1ドル" ]; then - SUDO=sudo - SYMLINK_DIR=/usr/local/bin/ -else - SUDO="" - SYMLINK_DIR=1ドル -fi - -symlink() { - $SUDO ln -sf "${INSTALL_DIR}/bin/composer" "${SYMLINK_DIR}/composer" - $SUDO ln -sf "${INSTALL_DIR}/bin/php" "${SYMLINK_DIR}/php" - $SUDO ln -sf "${INSTALL_DIR}/bin/php-cs-fixer" "${SYMLINK_DIR}/php-cs-fixer" - $SUDO ln -sf "${INSTALL_DIR}/bin/phpctl" "${SYMLINK_DIR}/pctl" - $SUDO ln -sf "${INSTALL_DIR}/bin/phpctl" "${SYMLINK_DIR}/phpctl" - $SUDO ln -sf "${INSTALL_DIR}/bin/phpstan" "${SYMLINK_DIR}/phpstan" - $SUDO ln -sf "${INSTALL_DIR}/bin/infection" "${SYMLINK_DIR}/infection" - $SUDO ln -sf "${INSTALL_DIR}/bin/phpunit" "${SYMLINK_DIR}/phpunit" - $SUDO ln -sf "${INSTALL_DIR}/bin/pest" "${SYMLINK_DIR}/pest" - $SUDO ln -sf "${INSTALL_DIR}/bin/pint" "${SYMLINK_DIR}/pint" - $SUDO ln -sf "${INSTALL_DIR}/bin/exakat" "${SYMLINK_DIR}/exakat" - $SUDO ln -sf "${INSTALL_DIR}/bin/frankenphp" "${SYMLINK_DIR}/frankenphp" - $SUDO ln -sf "${INSTALL_DIR}/bin/rector" "${SYMLINK_DIR}/rector" -} - -echo "033円[0;33mInstalling phpctl at 033円[0m$INSTALL_DIR" -if [ -d "$INSTALL_DIR" ]; then - echo "The install directory is not empty. Attempting to remove it..." - rm -rI $INSTALL_DIR -fi - -echo -n "" -git clone --quiet https://github.com/opencodeco/phpctl.git $INSTALL_DIR & -PID=$! -while kill -0 $PID 2> /dev/null; do - for CHAR in '-' '/' '|' '\'; do - printf "\b$CHAR" - sleep 0.1 - done -done - -echo "\b " -if [ -z "1ドル" ]; then - echo -n "Sudo will be prompted to symlink the phpctl files." -else - echo -n "Files will be symlinked to ${SYMLINK_DIR}." -fi -echo -n " 033円[0;32mDo you want to continue? (y/n)033円[0m " -read -r answer -if [ "$answer" != "${answer#[Yy]}" ]; then - symlink -else - echo "033円[0;31mTo use phpctl globally, link the cloned script to your bin directory, like:033円[0m" - echo "" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/php ${SYMLINK_DIR}/php" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/composer ${SYMLINK_DIR}/composer" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/phpctl ${SYMLINK_DIR}/phpctl" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/phpctl ${SYMLINK_DIR}/pctl" - echo "" - echo "033円[0;31mYou can also complement with another useful binaries:033円[0m" - echo "" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/phpunit ${SYMLINK_DIR}/phpunit" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/php-cs-fixer ${SYMLINK_DIR}/php-cs-fixer" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/phpstan ${SYMLINK_DIR}/phpstan" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/infection ${SYMLINK_DIR}/infection" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/pest ${SYMLINK_DIR}/pest" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/pint ${SYMLINK_DIR}/pint" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/exakat ${SYMLINK_DIR}/exakat" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/frankenphp ${SYMLINK_DIR}/frankenphp" - echo " ${SUDO} ln -sf ${INSTALL_DIR}/bin/rector ${SYMLINK_DIR}/rector" - echo "" -fi diff --git a/lib/bashunit b/lib/bashunit index 8a20bca..4663b3c 100755 --- a/lib/bashunit +++ b/lib/bashunit @@ -1,11 +1,22 @@ #!/bin/bash +# src/assert.sh + +function fail() { + local message=1ドル + + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + state::add_assertions_failed + console_results::print_failure_message "${label}" "$message" +} function assert_equals() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$expected" != "$actual" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${expected}" "but got" "${actual}" return @@ -17,19 +28,27 @@ function assert_equals() { function assert_equals_ignore_colors() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" local actual_without_colors actual_without_colors=$(echo -e "$actual" | sed "s/\x1B\[[0-9;]*[JKmsu]//g") - assert_equals "$expected" "$actual_without_colors" "$label" + if [[ "$expected" != "$actual_without_colors" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + state::add_assertions_failed + console_results::print_failed_test "${label}" "${expected}" "but got" "${actual_without_colors}" + return + fi + + state::add_assertions_passed } function assert_empty() { local expected="1ドル" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$expected" != "" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "to be empty" "but got" "${expected}" return @@ -40,9 +59,10 @@ function assert_empty() { function assert_not_empty() { local expected="1ドル" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$expected" == "" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "to not be empty" "but got" "${expected}" return @@ -54,9 +74,10 @@ function assert_not_empty() { function assert_not_equals() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$expected" == "$actual" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${expected}" "but got" "${actual}" return @@ -67,10 +88,13 @@ function assert_not_equals() { function assert_contains() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual == *"$expected"* ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}" return @@ -82,14 +106,14 @@ function assert_contains() { function assert_contains_ignore_case() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" shopt -s nocasematch if ! [[ $actual =~ $expected ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}" - shopt -u nocasematch return fi @@ -100,10 +124,13 @@ function assert_contains_ignore_case() { function assert_not_contains() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual == *"$expected"* ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to not contain" "${expected}" return @@ -114,10 +141,13 @@ function assert_not_contains() { function assert_matches() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual =~ $expected ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to match" "${expected}" return @@ -128,10 +158,13 @@ function assert_matches() { function assert_not_matches() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual =~ $expected ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to not match" "${expected}" return @@ -143,9 +176,10 @@ function assert_not_matches() { function assert_exit_code() { local actual_exit_code=${3-"$?"} local expected_exit_code="1ドル" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be" "${expected_exit_code}" return @@ -157,9 +191,10 @@ function assert_exit_code() { function assert_successful_code() { local actual_exit_code=${3-"$?"} local expected_exit_code=0 - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -171,9 +206,10 @@ function assert_successful_code() { function assert_general_error() { local actual_exit_code=${3-"$?"} local expected_exit_code=1 - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ $actual_exit_code -ne "$expected_exit_code" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -185,9 +221,10 @@ function assert_general_error() { function assert_command_not_found() { local actual_exit_code=${3-"$?"} local expected_exit_code=127 - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ $actual_exit_code -ne "$expected_exit_code" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -198,10 +235,13 @@ function assert_command_not_found() { function assert_string_starts_with() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual =~ ^"$expected"* ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to start with" "${expected}" return @@ -213,9 +253,10 @@ function assert_string_starts_with() { function assert_string_not_starts_with() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if [[ $actual =~ ^"$expected"* ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to not start with" "${expected}" return @@ -226,10 +267,13 @@ function assert_string_not_starts_with() { function assert_string_ends_with() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual =~ .*"$expected"$ ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to end with" "${expected}" return @@ -240,10 +284,13 @@ function assert_string_ends_with() { function assert_string_not_ends_with() { local expected="1ドル" - local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local actual_arr=("${@:2}") + local actual + actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual =~ .*"$expected"$ ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to not end with" "${expected}" return @@ -255,9 +302,10 @@ function assert_string_not_ends_with() { function assert_less_than() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if ! [[ "$actual" -lt "$expected" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to be less than" "${expected}" return @@ -269,9 +317,10 @@ function assert_less_than() { function assert_less_or_equal_than() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if ! [[ "$actual" -le "$expected" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to be less or equal than" "${expected}" return @@ -283,9 +332,10 @@ function assert_less_or_equal_than() { function assert_greater_than() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if ! [[ "$actual" -gt "$expected" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to be greater than" "${expected}" return @@ -297,9 +347,10 @@ function assert_greater_than() { function assert_greater_or_equal_than() { local expected="1ドル" local actual="2ドル" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" if ! [[ "$actual" -ge "$expected" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed console_results::print_failed_test "${label}" "${actual}" "to be greater or equal than" "${expected}" return @@ -307,7 +358,37 @@ function assert_greater_or_equal_than() { state::add_assertions_passed } -#!/bin/bash + +function assert_line_count() { + local expected="1ドル" + local input_arr=("${@:2}") + local input_str + input_str=$(printf '%s\n' "${input_arr[@]}") + + if [ -z "$input_str" ]; then + local actual=0 + else + local actual + actual=$(echo "$input_str" | wc -l | tr -d '[:blank:]') + additional_new_lines=$(grep -o '\\n' <<< "$input_str" | wc -l | tr -d '[:blank:]') + ((actual+=additional_new_lines)) + fi + + if [[ "$expected" != "$actual" ]]; then + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + + state::add_assertions_failed + console_results::print_failed_test "${label}" "${input_str}"\ + "to contain number of lines equal to" "${expected}"\ + "but found" "${actual}" + return + fi + + state::add_assertions_passed +} + +# src/assert_arrays.sh function assert_array_contains() { local expected="1ドル" @@ -340,7 +421,8 @@ function assert_array_not_contains() { state::add_assertions_passed } -#!/bin/bash + +# src/assert_files.sh function assert_file_exists() { local expected="1ドル" @@ -393,7 +475,8 @@ function assert_is_file_empty() { state::add_assertions_passed } -#!/bin/bash + +# src/assert_folders.sh function assert_directory_exists() { local expected="1ドル" @@ -511,7 +594,8 @@ function assert_is_directory_not_writable() { state::add_assertions_passed } -#!/bin/bash + +# src/assert_snapshot.sh function assert_match_snapshot() { local actual @@ -549,9 +633,33 @@ function assert_match_snapshot() { state::add_assertions_passed } -#!/bin/bash -#!/bin/bash +# src/assertions.sh + + +# src/bashunit.sh + +# This file provides a facade to developers who wants +# to interact with the internals of bashunit. +# e.g. adding custom assertions + +function bashunit::assertion_failed() { + local expected=1ドル + local actual=2ドル + local failure_condition_message=${3:-"but got"} + + local label + label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + state::add_assertions_failed + console_results::print_failed_test "${label}" "${expected}" \ + "$failure_condition_message" "${actual}" +} + +function bashunit::assertion_passed() { + state::add_assertions_passed +} + +# src/check_os.sh # shellcheck disable=SC2034 _OS="Unknown" @@ -563,23 +671,65 @@ elif [[ "$(uname)" == "Darwin" ]]; then elif [[ $(uname) == *"MINGW"* ]]; then _OS="Windows" fi -#!/bin/bash -# shellcheck disable=SC2034 -_COLOR_DEFAULT=$'\e[0m' -_COLOR_BOLD=$'\e[1m' -_COLOR_FAINT=$'\e[2m' -_COLOR_FAILED=$'\e[31m' -_COLOR_PASSED=$'\e[32m' -_COLOR_SKIPPED=$'\e[33m' -_COLOR_INCOMPLETE=$'\e[36m' -_COLOR_SNAPSHOT=$'\e[34m' -_COLOR_RETURN_ERROR=$'\e[41m' -_COLOR_RETURN_SUCCESS=$'\e[42m' -_COLOR_RETURN_SKIPPED=$'\e[43m' -_COLOR_RETURN_INCOMPLETE=$'\e[46m' -_COLOR_RETURN_SNAPSHOT=$'\e[44m' -#!/bin/bash +# src/clock.sh + +function clock::now() { + if perl --version> /dev/null 2>&1; then + perl -MTime::HiRes -e 'printf("%.0f\n",Time::HiRes::time()*1000)' + elif [[ "$_OS" != "OSX" ]]; then + date +%s%N + else + echo "" + fi +} + +_START_TIME=$(clock::now) + +function clock::runtime_in_milliseconds() { + end_time=$(clock::now) + if [[ -n $end_time ]]; then + echo $(( end_time - _START_TIME )) + else + echo "" + fi +} + +# src/colors.sh + +# Pass in any number of ANSI SGR codes. +# +# Code reference: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +# Credit: +# https://superuser.com/a/1119396 +sgr() { + local codes=${1:-0} + shift + + for c in "$@"; do + codes="$codes;$c" + done + + echo $'\e'"[${codes}m" +} + +_COLOR_BOLD="$(sgr 1)" +_COLOR_FAINT="$(sgr 2)" +_COLOR_BLACK="$(sgr 30)" +_COLOR_FAILED="$(sgr 31)" +_COLOR_PASSED="$(sgr 32)" +_COLOR_SKIPPED="$(sgr 33)" +_COLOR_INCOMPLETE="$(sgr 36)" +_COLOR_SNAPSHOT="$(sgr 34)" +_COLOR_RETURN_ERROR="$(sgr 41)$_COLOR_BLACK$_COLOR_BOLD" +_COLOR_RETURN_SUCCESS="$(sgr 42)$_COLOR_BLACK$_COLOR_BOLD" +_COLOR_RETURN_SKIPPED="$(sgr 43)$_COLOR_BLACK$_COLOR_BOLD" +_COLOR_RETURN_INCOMPLETE="$(sgr 46)$_COLOR_BLACK$_COLOR_BOLD" +_COLOR_RETURN_SNAPSHOT="$(sgr 44)$_COLOR_BLACK$_COLOR_BOLD" +_COLOR_DEFAULT="$(sgr 0)" + +# src/console_header.sh function console_header::print_version() { if [[ $HEADER_ASCII_ART == true ]]; then @@ -614,30 +764,45 @@ Arguments: If you use wildcards, bashunit will run any tests it finds. Options: - -f|--filer + -a|--assert + Run a core assert function standalone without a test context. + + --debug + Print all executed shell commands to the terminal. + + -e|--env + Load a custom env file overriding the .env environment variables. + + -f|--filter Filters the tests to run based on the test name. + -l|--log-junit + Create a report JUnit XML file that contains information about the test results. + + -r|--report-html + Create a report HTML file that contains information about the test results. + -s|simple || -v|verbose Enables simplified or verbose output to the console. -S|--stop-on-failure Force to stop the runner right after encountering one failing test. - -e|--env - Load a custom env file overriding the .env environment variables. - --version Displays the current version of bashunit. + --upgrade + Upgrade to latest version of bashunit. + --help This message. See more: https://bashunit.typeddevs.com/command-line EOF } -#!/bin/bash -_START_TIME=$(date +%s%N); +# src/console_results.sh + _SUCCESSFUL_TEST_COUNT=0 function console_results::render_result() { @@ -646,24 +811,24 @@ function console_results::render_result() { printf "%s%s%s\n" "${_COLOR_RETURN_ERROR}" "Duplicate test functions found" "${_COLOR_DEFAULT}" printf "File with duplicate functions: %s\n" "$(state::get_file_with_duplicated_function_names)" printf "Duplicate functions: %s\n" "$(state::get_duplicated_function_names)" - exit 1 + return 1 fi echo "" local total_tests=0 - ((total_tests+=$(state::get_tests_passed))) - ((total_tests+=$(state::get_tests_skipped))) - ((total_tests+=$(state::get_tests_incomplete))) - ((total_tests+=$(state::get_tests_snapshot))) - ((total_tests+=$(state::get_tests_failed))) + ((total_tests += $(state::get_tests_passed))) || true + ((total_tests += $(state::get_tests_skipped))) || true + ((total_tests += $(state::get_tests_incomplete))) || true + ((total_tests += $(state::get_tests_snapshot))) || true + ((total_tests += $(state::get_tests_failed))) || true local total_assertions=0 - ((total_assertions+=$(state::get_assertions_passed))) - ((total_assertions+=$(state::get_assertions_skipped))) - ((total_assertions+=$(state::get_assertions_incomplete))) - ((total_assertions+=$(state::get_assertions_snapshot))) - ((total_assertions+=$(state::get_assertions_failed))) + ((total_assertions += $(state::get_assertions_passed))) || true + ((total_assertions += $(state::get_assertions_skipped))) || true + ((total_assertions += $(state::get_assertions_incomplete))) || true + ((total_assertions += $(state::get_assertions_snapshot))) || true + ((total_assertions += $(state::get_assertions_failed))) || true printf "%sTests: %s" "$_COLOR_FAINT" "$_COLOR_DEFAULT" if [[ "$(state::get_tests_passed)" -gt 0 ]] || [[ "$(state::get_assertions_passed)" -gt 0 ]]; then @@ -702,38 +867,38 @@ function console_results::render_result() { printf " %s total\n" "$total_assertions" if [[ "$(state::get_tests_failed)" -gt 0 ]]; then - printf "%s%s%s\n" "$_COLOR_RETURN_ERROR" "Some tests failed" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_ERROR" " Some tests failed " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 1 + return 1 fi if [[ "$(state::get_tests_incomplete)" -gt 0 ]]; then - printf "%s%s%s\n" "$_COLOR_RETURN_INCOMPLETE" "Some tests incomplete" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_INCOMPLETE" " Some tests incomplete " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 0 + return 0 fi if [[ "$(state::get_tests_skipped)" -gt 0 ]]; then - printf "%s%s%s\n" "$_COLOR_RETURN_SKIPPED" "Some tests skipped" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_SKIPPED" " Some tests skipped " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 0 + return 0 fi if [[ "$(state::get_tests_snapshot)" -gt 0 ]]; then - printf "%s%s%s\n" "$_COLOR_RETURN_SNAPSHOT" "Some snapshots created" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_SNAPSHOT" " Some snapshots created " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 0 + return 0 fi if [[ $total_tests -eq 0 ]]; then - printf "%s%s%s\n" "$_COLOR_RETURN_ERROR" "No tests found" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_ERROR" " No tests found " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 1 + return 1 fi - printf "%s%s%s\n" "$_COLOR_RETURN_SUCCESS" "All tests passed" "$_COLOR_DEFAULT" + printf "\n%s%s%s\n" "$_COLOR_RETURN_SUCCESS" " All tests passed " "$_COLOR_DEFAULT" console_results::print_execution_time - exit 0 + return 0 } function console_results::print_execution_time() { @@ -741,14 +906,12 @@ function console_results::print_execution_time() { return fi - if [[ "$_OS" != "OSX" ]]; then - _EXECUTION_TIME=$((($(date +%s%N) - "$_START_TIME") / 1000000)) - printf "${_COLOR_BOLD}%s${_COLOR_DEFAULT}\n" "Time taken: ${_EXECUTION_TIME} ms" - fi + _EXECUTION_TIME=$(clock::runtime_in_milliseconds) + printf "${_COLOR_BOLD}%s${_COLOR_DEFAULT}\n" "Time taken: ${_EXECUTION_TIME} ms" } function console_results::print_successful_test() { - ((_SUCCESSFUL_TEST_COUNT++)) + ((_SUCCESSFUL_TEST_COUNT++)) || true if [[ "$SIMPLE_OUTPUT" == true ]]; then if (( _SUCCESSFUL_TEST_COUNT % 50 != 0 )); then @@ -758,27 +921,45 @@ function console_results::print_successful_test() { fi else local test_name=1ドル - local data=2ドル + shift - if [[ -z "$data" ]]; then + if [[ -z "$*" ]]; then printf "%sβœ“ Passed%s: %s\n" "$_COLOR_PASSED" "$_COLOR_DEFAULT" "${test_name}" else - printf "%sβœ“ Passed%s: %s (%s)\n" "$_COLOR_PASSED" "$_COLOR_DEFAULT" "${test_name}" "${data}" + printf "%sβœ“ Passed%s: %s (%s)\n" "$_COLOR_PASSED" "$_COLOR_DEFAULT" "${test_name}" "$*" fi fi } +function console_results::print_failure_message() { + local test_name=1ドル + local failure_message=2ドル + + printf "\ +${_COLOR_FAILED}βœ— Failed${_COLOR_DEFAULT}: %s + ${_COLOR_FAINT}Message:${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT}\n"\ + "${test_name}" "${failure_message}" +} + function console_results::print_failed_test() { local test_name=1ドル local expected=2ドル local failure_condition_message=3ドル local actual=4ドル + local extra_key=${5-} + local extra_value=${6-} printf "\ ${_COLOR_FAILED}βœ— Failed${_COLOR_DEFAULT}: %s ${_COLOR_FAINT}Expected${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT} ${_COLOR_FAINT}%s${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT}\n"\ "${test_name}" "${expected}" "${failure_condition_message}" "${actual}" + + if [ -n "$extra_key" ]; then + printf "\ + ${_COLOR_FAINT}%s${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT}\n"\ + "${extra_key}" "${extra_value}" + fi } function console_results::print_failed_snapshot_test() { @@ -800,7 +981,7 @@ function console_results::print_failed_snapshot_test() { function console_results::print_skipped_test() { local test_name=1ドル - local reason=2ドル + local reason=${2-} printf "${_COLOR_SKIPPED}β†· Skipped${_COLOR_DEFAULT}: %s\n" "${test_name}" @@ -811,7 +992,7 @@ function console_results::print_skipped_test() { function console_results::print_incomplete_test() { local test_name=1ドル - local pending=2ドル + local pending=${2-} printf "${_COLOR_INCOMPLETE}βœ’ Incomplete${_COLOR_DEFAULT}: %s\n" "${test_name}" @@ -835,7 +1016,8 @@ function console_results::print_error_test() { printf "${_COLOR_FAILED}βœ— Failed${_COLOR_DEFAULT}: %s ${_COLOR_FAINT}%s${_COLOR_DEFAULT}\n" "${test_name}" "${error}" } -#!/bin/bash + +# src/default_env_config.sh # shellcheck disable=SC2034 _DEFAULT_PARALLEL_RUN=false @@ -845,8 +1027,11 @@ _DEFAULT_SIMPLE_OUTPUT=false _DEFAULT_STOP_ON_FAILURE=false _DEFAULT_SHOW_EXECUTION_TIME=true _DEFAULT_DEFAULT_PATH= +_DEFAULT_LOG_JUNIT= +_DEFAULT_REPORT_HTML= CAT="$(which cat)" -#!/bin/bash + +# src/deprecated_assert.sh # Deprecated: Please use assert_equals instead. function assertEquals() { @@ -945,41 +1130,27 @@ function assertArrayContains() { function assertArrayNotContains() { assert_array_not_contains "1ドル" "${@:1}" } -#!/bin/bash + +# src/env_configuration.sh set -o allexport # shellcheck source=/dev/null [[ -f ".env" ]] && source .env set set +o allexport -if [[ -z "$PARALLEL_RUN" ]]; then - PARALLEL_RUN=$_DEFAULT_PARALLEL_RUN -fi - -if [[ -z "$SHOW_HEADER" ]]; then - SHOW_HEADER=$_DEFAULT_SHOW_HEADER -fi - -if [[ -z "$HEADER_ASCII_ART" ]]; then - HEADER_ASCII_ART=$_DEFAULT_HEADER_ASCII_ART -fi - -if [[ -z "$SIMPLE_OUTPUT" ]]; then - SIMPLE_OUTPUT=$_DEFAULT_SIMPLE_OUTPUT -fi +: "${PARALLEL_RUN:=$_DEFAULT_PARALLEL_RUN}" +: "${SHOW_HEADER:=$_DEFAULT_SHOW_HEADER}" +: "${HEADER_ASCII_ART:=$_DEFAULT_HEADER_ASCII_ART}" +: "${SIMPLE_OUTPUT:=$_DEFAULT_SIMPLE_OUTPUT}" +: "${STOP_ON_FAILURE:=$_DEFAULT_STOP_ON_FAILURE}" +: "${SHOW_EXECUTION_TIME:=$_DEFAULT_SHOW_EXECUTION_TIME}" +: "${DEFAULT_PATH:=$_DEFAULT_DEFAULT_PATH}" +: "${LOG_JUNIT:=$_DEFAULT_LOG_JUNIT}" +: "${REPORT_HTML:=$_DEFAULT_REPORT_HTML}" -if [[ -z "$STOP_ON_FAILURE" ]]; then - STOP_ON_FAILURE=$_DEFAULT_STOP_ON_FAILURE -fi - -if [[ -z "$SHOW_EXECUTION_TIME" ]]; then - SHOW_EXECUTION_TIME=$_DEFAULT_SHOW_EXECUTION_TIME -fi +# src/helpers.sh -if [[ -z "$DEFAULT_PATH" ]]; then - DEFAULT_PATH=$_DEFAULT_DEFAULT_PATH -fi -#!/bin/bash +declare -r BASHUNIT_GIT_REPO="https://github.com/TypedDevs/bashunit" # # @param 1ドル string Eg: "test_some_logic_camelCase" @@ -987,7 +1158,7 @@ fi # @return string Eg: "Some logic camelCase" # function helper::normalize_test_function_name() { - local original_function_name="1ドル" + local original_function_name="${1-}" local result # Remove "test_" prefix @@ -1034,40 +1205,26 @@ function helper::get_functions_to_run() { local filter=2ドル local function_names=3ドル - local functions_to_run=() + local filtered_functions="" - for function_name in $function_names; do - if [[ $function_name != ${prefix}* ]]; then - continue - fi - - local lower_case_function_name - lower_case_function_name=$(echo "$function_name" | tr '[:upper:]' '[:lower:]') - local lower_case_filter - lower_case_filter=$(echo "$filter" | tr '[:upper:]' '[:lower:]') - - if [[ -n $filter && $lower_case_function_name != *"$lower_case_filter"* ]]; then - continue - fi - - if [[ "${functions_to_run[*]}" =~ ${function_name} ]]; then - return 1 + for fn in $function_names; do + if [[ $fn == ${prefix}_${filter}* ]]; then + if [[ $filtered_functions == *" $fn"* ]]; then + return 1 + fi + filtered_functions+=" $fn" fi - - functions_to_run+=("$function_name") done - echo "${functions_to_run[@]}" + echo "${filtered_functions# }" } # # @param 1ドル string Eg: "do_something" # function helper::execute_function_if_exists() { - local function_name=1ドル - - if declare -F | awk '{print 3ドル}' | grep -Eq "^${function_name}$"; then - "$function_name" + if [[ "$(type -t "1ドル")" == "function" ]]; then + "1ドル" 2>/dev/null fi } @@ -1075,11 +1232,7 @@ function helper::execute_function_if_exists() { # @param 1ドル string Eg: "do_something" # function helper::unset_if_exists() { - local function_name=1ドル - - if declare -F | awk '{print 3ドル}' | grep -Eq "^${function_name}$"; then - unset "$function_name" - fi + unset "1ドル" 2>/dev/null } function helper::find_files_recursive() { @@ -1120,6 +1273,7 @@ function helper::get_provider_data() { grep -B 1 "function $function_name()" "$script" |\ grep "# data_provider " |\ sed -E -e 's/\ *# data_provider (.*)$/1円/g'\ + || true ) if [[ -n "$data_provider_function" ]]; then @@ -1127,16 +1281,287 @@ function helper::get_provider_data() { fi } +function helper::get_multi_invoker_function() { + local function_name="1ドル" + local script="2ドル" + local multi_invoker_function + + if [[ ! -f "$script" ]]; then + return + fi + + multi_invoker_function=$(\ + grep -B 1 "function $function_name()" "$script" |\ + grep "# multi_invoker " |\ + sed -E -e 's/\ *# multi_invoker (.*)$/1円/g'\ + ) + func_exists=$(declare -f "$multi_invoker_function") + if [[ -n "$func_exists" ]]; then + echo "$multi_invoker_function" + fi +} + function helper::trim() { - local input_string="1ドル" - local trimmed_string + local input_string="1ドル" + local trimmed_string - trimmed_string="${input_string#"${input_string%%[![:space:]]*}"}" - trimmed_string="${trimmed_string%"${trimmed_string##*[![:space:]]}"}" + trimmed_string="${input_string#"${input_string%%[![:space:]]*}"}" + trimmed_string="${trimmed_string%"${trimmed_string##*[![:space:]]}"}" - echo "$trimmed_string" + echo "$trimmed_string" +} + +function helpers::get_latest_tag() { + git ls-remote --tags "$BASHUNIT_GIT_REPO" | + awk '{print 2ドル}' | + sed 's|^refs/tags/||' | + sort -Vr | + head -n 1 +} + +# src/logger.sh + +TEST_NAMES=() +TEST_STATUSES=() +TEST_DURATIONS=() + +function logger::test_snapshot() { + logger::log "1ドル" "2ドル" "3ドル" "snapshot" +} + +function logger::test_incomplete() { + logger::log "1ドル" "2ドル" "3ドル" "incomplete" +} + +function logger::test_skipped() { + logger::log "1ドル" "2ドル" "3ドル" "skipped" +} + +function logger::test_passed() { + logger::log "1ドル" "2ドル" "3ドル" "passed" +} + +function logger::test_failed() { + logger::log "1ドル" "2ドル" "3ドル" "failed" +} + +function logger::log() { + local file="1ドル" + local test_name="2ドル" + local start_time="3ドル" + local status="4ドル" + + local end_time + end_time=$(clock::now) + local duration=$((end_time - start_time)) + + TEST_FILES+=("$file") + TEST_NAMES+=("$test_name") + TEST_STATUSES+=("$status") + TEST_DURATIONS+=("$duration") +} + +function logger::generate_junit_xml() { + local output_file="1ドル" + local test_passed + test_passed=$(state::get_tests_passed) + local tests_skipped + tests_skipped=$(state::get_tests_skipped) + local tests_incomplete + tests_incomplete=$(state::get_tests_incomplete) + local tests_snapshot + tests_snapshot=$(state::get_tests_snapshot) + local tests_failed + tests_failed=$(state::get_tests_failed) + local time + time=$(clock::runtime_in_milliseconds) + + { + echo "" + echo "" + echo " " + + for i in "${!TEST_NAMES[@]}"; do + local file="${TEST_FILES[$i]}" + local name="${TEST_NAMES[$i]}" + local status="${TEST_STATUSES[$i]}" + local test_time="${TEST_DURATIONS[$i]}" + + echo " " + echo " " + done + + echo " " + echo "" + }> "$output_file" +} + +function logger::generate_report_html() { + local output_file="1ドル" + local test_passed + test_passed=$(state::get_tests_passed) + local tests_skipped + tests_skipped=$(state::get_tests_skipped) + local tests_incomplete + tests_incomplete=$(state::get_tests_incomplete) + local tests_snapshot + tests_snapshot=$(state::get_tests_snapshot) + local tests_failed + tests_failed=$(state::get_tests_failed) + local time + time=$(clock::runtime_in_milliseconds) + + # Temporary file to store test cases by file + local temp_file="temp_test_cases.txt" + + # Collect test cases by file + :> "$temp_file" # Clear temp file if it exists + for i in "${!TEST_NAMES[@]}"; do + local file="${TEST_FILES[$i]}" + local name="${TEST_NAMES[$i]}" + local status="${TEST_STATUSES[$i]}" + local test_time="${TEST_DURATIONS[$i]}" + local test_case="$file|$name|$status|$test_time" + + echo "$test_case">> "$temp_file" + done + + { + echo "" + echo "" + echo " " + echo " " + echo " " + echo " Test Report" + echo " " + echo "" + echo "
" + echo "

Test Report

" + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo "
Total TestsPassedFailedIncompleteSkippedSnapshotTime (ms)
${#TEST_NAMES[@]}$test_passed$tests_failed$tests_incomplete$tests_skipped$tests_snapshot${time}
" + echo "

Time: $time ms

" + + # Read the temporary file and group by file + local current_file="" + while IFS='|' read -r file name status test_time; do + if [ "$file" != "$current_file" ]; then + if [ -n "$current_file" ]; then + echo " " + echo " " + fi + echo "

File: $file

" + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + echo " " + current_file="$file" + fi + echo " " + echo " " + echo " " + echo " " + echo " " + done < "$temp_file" + + # Close the last table + if [ -n "$current_file" ]; then + echo " " + echo "
Test NameStatusTime (ms)
$name$status$test_time
" + fi + + echo "

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /

" + echo "" + }> "$output_file" + + # Clean up temporary file + rm -f "$temp_file" } -#!/bin/bash + +# src/main.sh + +function main::exec_tests() { + local filter=1ドル + local files=("${@:2}") + + console_header::print_version_with_env + runner::load_test_files "$filter" "${files[@]}" + console_results::render_result + exit_code=$? + + if [[ -n "$LOG_JUNIT" ]]; then + logger::generate_junit_xml "$LOG_JUNIT" + fi + + if [[ -n "$REPORT_HTML" ]]; then + logger::generate_report_html "$REPORT_HTML" + fi + + exit $exit_code +} + +function main::exec_assert() { + local original_assert_fn=1ドル + local assert_fn=$original_assert_fn + local args=("${@:2}") + + if ! type "$assert_fn"> /dev/null 2>&1; then + # try again using prefix `assert_` + assert_fn="assert_$assert_fn" + if ! type "$assert_fn"> /dev/null 2>&1; then + echo "Function $original_assert_fn does not exist." + exit 127 + fi + fi + + "$assert_fn" "${args[@]}" + + if [[ "$(state::get_tests_failed)" -gt 0 ]] || [[ "$(state::get_assertions_failed)" -gt 0 ]]; then + exit 1 + fi +} + +# src/runner.sh function runner::load_test_files() { local filter=1ドル @@ -1172,23 +1597,44 @@ function runner::load_test_files() { done } +function runner::functions_for_script() { + local script="1ドル" + local all_function_names="2ドル" + + # Filter the names down to the ones defined in the script, sort them by line number + shopt -s extdebug + for f in $all_function_names; do + declare -F "$f" | grep "$script" + done | sort -k2 -n | awk '{print 1ドル}' + shopt -u extdebug +} + +# Helper function for test authors to invoke a named test case +function run_test() { + runner::run_test "testing-fn" "$function_name" "$@" +} + function runner::call_test_functions() { local script="1ドル" local filter="2ドル" local prefix="test" # Use declare -F to list all function names - local function_names - function_names=$(declare -F | awk '{print 3ドル}') + local all_function_names + all_function_names=$(declare -F | awk '{print 3ドル}') + local filtered_functions + # shellcheck disable=SC2207 + filtered_functions=$(helper::get_functions_to_run "$prefix" "$filter" "$all_function_names") + local functions_to_run # shellcheck disable=SC2207 - functions_to_run=($(helper::get_functions_to_run "$prefix" "$filter" "$function_names")) + functions_to_run=($(runner::functions_for_script "$script" "$filtered_functions")) if [[ "${#functions_to_run[@]}" -gt 0 ]]; then if [[ "$SIMPLE_OUTPUT" == false ]]; then echo "Running $script" fi - helper::check_duplicate_functions "$script" + helper::check_duplicate_functions "$script" || true for function_name in "${functions_to_run[@]}"; do local provider_data=() @@ -1196,13 +1642,19 @@ function runner::call_test_functions() { if [[ "${#provider_data[@]}" -gt 0 ]]; then for data in "${provider_data[@]}"; do - runner::run_test "$function_name" "$data" + runner::run_test "$script" "$function_name" "$data" done else - runner::run_test "$function_name" + local multi_invoker + multi_invoker=$(helper::get_multi_invoker_function "$function_name" "$script") + if [[ -n "${multi_invoker}" ]]; then + helper::execute_function_if_exists "${multi_invoker}" + else + runner::run_test "$script" "$function_name" + fi fi - unset "$function_name" + unset function_name done fi } @@ -1245,16 +1697,21 @@ function runner::parse_execution_result() { sed -E -e 's/.*##ASSERTIONS_SNAPSHOT=([0-9]*)##.*/1円/g'\ ) - _ASSERTIONS_PASSED=$((_ASSERTIONS_PASSED + assertions_passed)) - _ASSERTIONS_FAILED=$((_ASSERTIONS_FAILED + assertions_failed)) - _ASSERTIONS_SKIPPED=$((_ASSERTIONS_SKIPPED + assertions_skipped)) - _ASSERTIONS_INCOMPLETE=$((_ASSERTIONS_INCOMPLETE + assertions_incomplete)) - _ASSERTIONS_SNAPSHOT=$((_ASSERTIONS_SNAPSHOT + assertions_snapshot)) + ((_ASSERTIONS_PASSED += assertions_passed)) || true + ((_ASSERTIONS_FAILED += assertions_failed)) || true + ((_ASSERTIONS_SKIPPED += assertions_skipped)) || true + ((_ASSERTIONS_INCOMPLETE += assertions_incomplete)) || true + ((_ASSERTIONS_SNAPSHOT += assertions_snapshot)) || true } function runner::run_test() { + local start_time + start_time=$(clock::now) + + local test_file="1ドル" + shift local function_name="1ドル" - local data="2ドル" + shift local current_assertions_failed current_assertions_failed="$(state::get_assertions_failed)" local current_assertions_snapshot @@ -1264,6 +1721,9 @@ function runner::run_test() { local current_assertions_skipped current_assertions_skipped="$(state::get_assertions_skipped)" + # (FD = File Descriptor) + # Duplicate the current std-output (FD 1) and assigns it to FD 3. + # This means that FD 3 now points to wherever the std-output was pointing. exec 3>&1 local test_execution_result @@ -1271,12 +1731,17 @@ function runner::run_test() { state::initialize_assertions_count runner::run_set_up - "$function_name" "$data" 2>&1 1>&3 + # 2>&1: Redirects the std-error (FD 2) to the std-output (FD 1). + # 1>&3: Redirects the std-output (FD 1) to FD 3, which, as set up earlier, + # points to the original std-output. + "$function_name" "$@" 2>&1 1>&3 runner::run_tear_down + runner::clear_mocks state::export_assertions_count ) + # Closes FD 3, which was used temporarily to hold the original std-output. exec 3>&- runner::parse_execution_result "$test_execution_result" @@ -1284,18 +1749,20 @@ function runner::run_test() { local runtime_error runtime_error=$(\ echo "$test_execution_result" |\ - head -n 1 |\ + tail -n 1 |\ sed -E -e 's/(.*)##ASSERTIONS_FAILED=.*/1円/g'\ ) if [[ -n $runtime_error ]]; then state::add_tests_failed console_results::print_error_test "$function_name" "$runtime_error" + logger::test_failed "$test_file" "$function_name" "$start_time" return fi if [[ "$current_assertions_failed" != "$(state::get_assertions_failed)" ]]; then state::add_tests_failed + logger::test_failed "$test_file" "$function_name" "$start_time" if [ "$STOP_ON_FAILURE" = true ]; then exit 1 @@ -1307,60 +1774,63 @@ function runner::run_test() { if [[ "$current_assertions_snapshot" != "$(state::get_assertions_snapshot)" ]]; then state::add_tests_snapshot console_results::print_snapshot_test "$function_name" + logger::test_snapshot "$test_file" "$function_name" "$start_time" return fi if [[ "$current_assertions_incomplete" != "$(state::get_assertions_incomplete)" ]]; then state::add_tests_incomplete + logger::test_incomplete "$test_file" "$function_name" "$start_time" return fi if [[ "$current_assertions_skipped" != "$(state::get_assertions_skipped)" ]]; then state::add_tests_skipped + logger::test_skipped "$test_file" "$function_name" "$start_time" return fi local label label="$(helper::normalize_test_function_name "$function_name")" - console_results::print_successful_test "${label}" "${data}" + console_results::print_successful_test "${label}" "$@" state::add_tests_passed + logger::test_passed "$test_file" "$function_name" "$start_time" } function runner::run_set_up() { - helper::execute_function_if_exists 'setUp' # Deprecated: please use set_up instead. helper::execute_function_if_exists 'set_up' } function runner::run_set_up_before_script() { - helper::execute_function_if_exists 'setUpBeforeScript' # Deprecated: please use set_up_before_script instead. helper::execute_function_if_exists 'set_up_before_script' } function runner::run_tear_down() { - helper::execute_function_if_exists 'tearDown' # Deprecated: please use tear_down instead. helper::execute_function_if_exists 'tear_down' } +function runner::clear_mocks() { + for i in "${!MOCKED_FUNCTIONS[@]}"; do + unmock "${MOCKED_FUNCTIONS[$i]}" + done +} + function runner::run_tear_down_after_script() { - helper::execute_function_if_exists 'tearDownAfterScript' # Deprecated: please use tear_down_after_script instead. helper::execute_function_if_exists 'tear_down_after_script' } function runner::clean_set_up_and_tear_down_after_script() { - helper::unset_if_exists 'setUp' # Deprecated: please use set_up instead. helper::unset_if_exists 'set_up' - helper::unset_if_exists 'tearDown' # Deprecated: please use tear_down instead. helper::unset_if_exists 'tear_down' - helper::unset_if_exists 'setUpBeforeScript' # Deprecated: please use set_up_before_script instead. helper::unset_if_exists 'set_up_before_script' - helper::unset_if_exists 'tearDownAfterScript' # Deprecated: please use tear_down_after_script instead. helper::unset_if_exists 'tear_down_after_script' } -#!/bin/bash + +# src/skip_todo.sh function skip() { - local reason=1ドル + local reason=${1-} local label label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" @@ -1370,7 +1840,7 @@ function skip() { } function todo() { - local pending=1ドル + local pending=${1-} local label label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" @@ -1378,7 +1848,8 @@ function todo() { state::add_assertions_incomplete } -#!/bin/bash + +# src/state.sh _TESTS_PASSED=0 _TESTS_FAILED=0 @@ -1521,7 +1992,22 @@ function state::export_assertions_count() { ##ASSERTIONS_SNAPSHOT=$_ASSERTIONS_SNAPSHOT\ ##" } -#!/bin/bash + +# src/test_doubles.sh + +declare -a MOCKED_FUNCTIONS=() + +function unmock() { + local command=1ドル + + for i in "${!MOCKED_FUNCTIONS[@]}"; do + if [[ "${MOCKED_FUNCTIONS[$i]}" == "$command" ]]; then + unset "MOCKED_FUNCTIONS[$i]" + unset -f "$command" + break + fi + done +} function mock() { local command=1ドル @@ -1534,6 +2020,8 @@ function mock() { fi export -f "${command?}" + + MOCKED_FUNCTIONS+=("$command") } function spy() { @@ -1547,6 +2035,8 @@ function spy() { eval "function $command() { ${variable}_params=(\"\$*\"); ((${variable}_times++)) || true; }" export -f "${command?}" + + MOCKED_FUNCTIONS+=("$command") } function assert_have_been_called() { @@ -1593,7 +2083,7 @@ function assert_have_been_called_times() { actual="${variable}_times" local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" - if [[ ${!actual} -ne $expected ]]; then + if [[ -z "${!actual-}" && $expected -ne 0 || ${!actual-0} -ne $expected ]]; then state::add_assertions_failed console_results::print_failed_test "${label}" "${command}" "to has been called" "${expected} times" return @@ -1601,40 +2091,70 @@ function assert_have_been_called_times() { state::add_assertions_passed } + +# src/upgrade.sh + +function upgrade::upgrade() { + local script_path + script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local latest_tag + latest_tag="$(helpers::get_latest_tag)" + + if [[ "$BASHUNIT_VERSION" == "$latest_tag" ]]; then + echo "> You are already on latest version" + return + fi + + echo "> Upgrading bashunit to latest version" + cd "$script_path" || exit + curl -L -J -o bashunit "https://github.com/TypedDevs/bashunit/releases/download/$latest_tag/bashunit" 2>/dev/null + chmod u+x "bashunit" + + echo "> bashunit upgraded successfully to latest version $latest_tag" +} + #!/bin/bash +set -euo pipefail # shellcheck disable=SC2034 -declare -r BASHUNIT_VERSION="0.10.1" +declare -r BASHUNIT_VERSION="0.14.0" -readonly BASHUNIT_ROOT_DIR="$(dirname "${BASH_SOURCE[0]}")" +# shellcheck disable=SC2155 +declare -r BASHUNIT_ROOT_DIR="$(dirname "${BASH_SOURCE[0]}")" export BASHUNIT_ROOT_DIR -############### -#### MAIN ##### -############### - +_ASSERT_FN="" _FILTER="" -_FILES=() +_ARGS=() while [[ $# -gt 0 ]]; do argument="1ドル" case $argument in + -a|--assert) + _ASSERT_FN="2ドル" + shift + shift + ;; -f|--filter) _FILTER="2ドル" shift shift ;; -s|--simple) - SIMPLE_OUTPUT=true + export SIMPLE_OUTPUT=true shift ;; -v|--verbose) - SIMPLE_OUTPUT=false + export SIMPLE_OUTPUT=false + shift + ;; + --debug) + set -x shift ;; -S|--stop-on-failure) - STOP_ON_FAILURE=true + export STOP_ON_FAILURE=true shift ;; -e|--env) @@ -1643,25 +2163,41 @@ while [[ $# -gt 0 ]]; do shift shift ;; + -l|--log-junit) + export LOG_JUNIT="2ドル"; + shift + shift + ;; + -r|--report-html) + export REPORT_HTML="2ドル"; + shift + shift + ;; --version) console_header::print_version trap '' EXIT && exit 0 ;; + --upgrade) + upgrade::upgrade + trap '' EXIT && exit 0 + ;; --help) console_header::print_help trap '' EXIT && exit 0 ;; *) while IFS='' read -r line; do - _FILES+=("$line"); + _ARGS+=("$line"); done < <(helper::find_files_recursive "$argument") shift ;; esac done -console_header::print_version_with_env -runner::load_test_files "$_FILTER" "${_FILES[@]}" -console_results::render_result +set +eu -exit 0 +if [[ -n "$_ASSERT_FN" ]]; then + main::exec_assert "$_ASSERT_FN" "${_ARGS[@]}" +else + main::exec_tests "$_FILTER" "${_ARGS[@]}" +fi diff --git a/php.ini b/php.ini new file mode 100644 index 0000000..4fdcd22 --- /dev/null +++ b/php.ini @@ -0,0 +1 @@ +sys_temp_dir = /tmp diff --git a/rootfs/etc/apk/repositories b/rootfs/etc/apk/repositories deleted file mode 100644 index f1fb265..0000000 --- a/rootfs/etc/apk/repositories +++ /dev/null @@ -1,4 +0,0 @@ -https://dl-cdn.alpinelinux.org/alpine/latest-stable/main -https://dl-cdn.alpinelinux.org/alpine/latest-stable/community -https://dl-cdn.alpinelinux.org/alpine/edge/main -https://dl-cdn.alpinelinux.org/alpine/edge/community diff --git a/rootfs/usr/local/bin/install-swoole b/rootfs/usr/local/bin/install-swoole new file mode 100755 index 0000000..3017b47 --- /dev/null +++ b/rootfs/usr/local/bin/install-swoole @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +set -e + +SWOOLE_VERSION="5.1.4" +echo "Installing Swoole $SWOOLE_VERSION" + +# Download +cd /usr/local/src +wget -q "https://github.com/swoole/swoole-src/archive/refs/tags/v${SWOOLE_VERSION}.zip" +unzip -q "v${SWOOLE_VERSION}.zip" + +# Install +cd "swoole-src-${SWOOLE_VERSION}" +phpize +./configure --enable-sockets --enable-openssl --enable-brotli --enable-mysqlnd --enable-cares --enable-swoole-curl --enable-swoole-pgsql --enable-swoole-sqlite +make -j$(nproc) +make install +echo "extension=swoole.so">> /usr/local/etc/php/conf.d/docker-php-ext-swoole.ini + +# Clean up +cd .. +rm "v${SWOOLE_VERSION}.zip" +rm -r "swoole-src-${SWOOLE_VERSION}" diff --git a/rootfs/usr/local/bin/install-tools b/rootfs/usr/local/bin/install-tools index 9d81ef1..1e59ff6 100755 --- a/rootfs/usr/local/bin/install-tools +++ b/rootfs/usr/local/bin/install-tools @@ -1,57 +1,118 @@ #!/usr/bin/env sh +set -e box() { - echo "Installing Box" - wget -q https://github.com/box-project/box/releases/download/4.5.1/box.phar -O /usr/local/bin/box + local version="4.6.1" + if [ "$PHP_VERSION" = "81" ]; then + version="4.5.1" + fi + echo "Installing Box $version" + wget -q "https://github.com/box-project/box/releases/download/$version/box.phar" -O /usr/local/bin/box chmod a+x /usr/local/bin/box } +co_phpunit() { + local version="3.1.3" + echo "Installing co-phpunit $version" + wget -q "https://raw.githubusercontent.com/hyperf/testing/v$version/co-phpunit" -O /usr/local/bin/co-phpunit + chmod a+x /usr/local/bin/co-phpunit +} + composer() { - echo "Installing Composer" - wget https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer -O - -q | php -- --quiet + local version="993f9fec74930f32f7015e71543243bf6d9b9e93" + echo "Installing Composer $version" + wget "https://raw.githubusercontent.com/composer/getcomposer.org/$version/web/installer" -O - -q | php -- --quiet mv composer.phar /usr/local/bin/composer chmod a+x /usr/local/bin/composer } +composer_require_checker() { + local version="4.10.0" + if [ "$PHP_VERSION" = "81" ]; then + version="4.7.1" + fi + echo "Installing ComposerRequireChecker $version" + wget -q "https://github.com/maglnet/ComposerRequireChecker/releases/download/$version/composer-require-checker.phar" -O /usr/local/bin/composer-require-checker + chmod a+x /usr/local/bin/composer-require-checker +} + couscous() { - echo "Installing Couscous" - wget -q https://github.com/CouscousPHP/Couscous/releases/download/1.10.0/couscous.phar -O /usr/local/bin/couscous + local version="1.10.0" + echo "Installing Couscous $version" + wget -q "https://github.com/CouscousPHP/Couscous/releases/download/$version/couscous.phar" -O /usr/local/bin/couscous chmod a+x /usr/local/bin/couscous } +deptrac() { + local version="1.0.2" + echo "Installing Deptrac $version" + wget -q "https://github.com/qossmic/deptrac/releases/download/$version/deptrac.phar" -O /usr/local/bin/deptrac + chmod a+x /usr/local/bin/deptrac +} + exakat() { - echo "Installing Exakat" - wget -q https://www.exakat.io/versions/index.php?file=exakat-2.6.2.phar -O /usr/local/bin/exakat + local version="2.6.2" + echo "Installing Exakat $version" + wget -q "https://www.exakat.io/versions/index.php?file=exakat-$version.phar" -O /usr/local/bin/exakat chmod a+x /usr/local/bin/exakat } infection() { - echo "Installing Infection" - wget -q https://github.com/infection/infection/releases/download/0.27.9/infection.phar -O /usr/local/bin/infection + local version="0.27.10" + echo "Installing Infection $version" + wget -q "https://github.com/infection/infection/releases/download/$version/infection.phar" -O /usr/local/bin/infection chmod a+x /usr/local/bin/infection } php_cs_fixer() { - echo "Installing PHP CS Fixer" - wget -q https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v3.46.0/php-cs-fixer.phar -O /usr/local/bin/php-cs-fixer + local version="3.51.0" + echo "Installing PHP CS Fixer $version" + wget -q "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v$version/php-cs-fixer.phar" -O /usr/local/bin/php-cs-fixer chmod a+x /usr/local/bin/php-cs-fixer } +phpcbf() { + local version="3.7.2" + echo "Installing PHP CodeSniffer $version (phpcbf)" + wget -q "https://github.com/squizlabs/PHP_CodeSniffer/releases/download/$version/phpcbf.phar" -O /usr/local/bin/phpcbf + chmod a+x /usr/local/bin/phpcbf +} + +phpcs() { + local version="3.7.2" + echo "Installing PHP CodeSniffer $version (phpcs)" + wget -q "https://github.com/squizlabs/PHP_CodeSniffer/releases/download/$version/phpcs.phar" -O /usr/local/bin/phpcs + chmod a+x /usr/local/bin/phpcs +} + +phpmd() { + local version="2.15.0" + echo "Installing PHP Mess Detector $version" + wget -q "https://github.com/phpmd/phpmd/releases/download/$version/phpmd.phar" -O /usr/local/bin/phpmd + chmod a+x /usr/local/bin/phpmd +} + phpstan() { - echo "Installing PHPStan" - wget -q https://github.com/phpstan/phpstan/releases/download/1.10.55/phpstan.phar -O /usr/local/bin/phpstan + local version="1.10.59" + echo "Installing PHPStan $version" + wget -q "https://github.com/phpstan/phpstan/releases/download/$version/phpstan.phar" -O /usr/local/bin/phpstan chmod a+x /usr/local/bin/phpstan } phpunit() { - echo "Installing PHPUnit" - wget -q https://phar.phpunit.de/phpunit-10.phar -O /usr/local/bin/phpunit + local version="11" + if [ "$PHP_VERSION" = "81" ]; then + version="10" + fi + echo "Installing PHPUnit $version" + wget -q "https://phar.phpunit.de/phpunit-$version.phar" -O /usr/local/bin/phpunit chmod a+x /usr/local/bin/phpunit } pint() { - echo "Installing Pint" - wget -q https://github.com/laravel/pint/releases/download/v1.13.7/pint.phar -O /usr/local/bin/pint + local version="1.14.0" + echo "Installing Pint $version" + wget -q "https://github.com/laravel/pint/releases/download/v$version/pint.phar" -O /usr/local/bin/pint chmod a+x /usr/local/bin/pint } @@ -61,17 +122,32 @@ psysh() { chmod a+x /usr/local/bin/psysh } +watchr() { + local version="0.5.3" + echo "Installing watchr $version" + wget -q "https://github.com/flavioheleno/watchr/releases/download/v$version/watchr.phar" -O /usr/local/bin/watchr + chmod a+x /usr/local/bin/watchr +} + install() { box + co_phpunit composer + composer_require_checker couscous - exakat + deptrac + [ -n "$WITH_EXAKAT" ] && exakat infection php_cs_fixer + phpcbf + phpcs + phpmd phpstan phpunit pint psysh + [ -z "$WITHOUT_WATCHR" ] && watchr + echo "Done!" } install diff --git a/scripts/symlink-bins.sh b/scripts/symlink-bins.sh new file mode 100755 index 0000000..bb01640 --- /dev/null +++ b/scripts/symlink-bins.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +for file in "${1:-.}"/bin/*; do + ln -sf "$(realpath "$file")" "/usr/local/bin/$(basename "$file")" +done diff --git a/src-devc/.devcontainer/Dockerfile b/src-devc/.devcontainer/Dockerfile new file mode 100644 index 0000000..2b3012f --- /dev/null +++ b/src-devc/.devcontainer/Dockerfile @@ -0,0 +1,11 @@ +ARG PHP_VERSION 83 +ARG PHP_VERSION_STR 8.3 +FROM php:${PHP_VERSION_STR}-cli-bookworm +ENV PHP_VERSION $PHP_VERSION +COPY rootfs / +ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends libcurl4-openssl-dev libc-ares-dev libsqlite3-dev libpq-dev git parallel unzip wget libbrotli-dev \ + && mv /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini \ + && mv /etc/php/php.ini /usr/local/etc/php/conf.d/zzphp.ini \ + && install-php-extensions sockets && install-swoole && install-tools diff --git a/src-devc/.devcontainer/devcontainer.json b/src-devc/.devcontainer/devcontainer.json new file mode 100644 index 0000000..af6ada6 --- /dev/null +++ b/src-devc/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "build": { + "dockerfile": "./Dockerfile", + "context": "../..", + "args": { + "PHP_VERSION": "${localEnv:PHP_VERSION}", + "PHP_VERSION_STR": "${localEnv:PHP_VERSION_STR}" + } + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "phpctl", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/opencodeco/devcontainers/install-php-extensions:latest": { + "extensions": "decimal gd intl mongodb pcntl pcov pdo_mysql pdo_pgsql rdkafka redis xdebug" + } + }, + "overrideFeatureInstallOrder": [ + "ghcr.io/devcontainers/features/common-utils" + ], + "remoteUser": "phpctl" +} diff --git a/src-devc/build.sh b/src-devc/build.sh new file mode 100755 index 0000000..40ba4c4 --- /dev/null +++ b/src-devc/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +export PHP_VERSION_STR="${PHP_VERSION:0:1}.${PHP_VERSION:1}" +devcontainer build --workspace-folder src-devc --push true --image-name "opencodeco/phpctl:php$PHP_VERSION-devcontainer" diff --git a/src/docker.sh b/src/docker.sh index 033d277..9b09d8e 100644 --- a/src/docker.sh +++ b/src/docker.sh @@ -1,9 +1,23 @@ build() { + local with_exakat="" + if [[ "$*" == *--with-exakat* ]]; then + with_exakat="--build-arg WITH_EXAKAT=1" + fi + + local without_watchr="" + if [[ "$*" == *--without-watchr* ]] || [ "$PHP_VERSION" = "81" ]; then + without_watchr="--build-arg WITHOUT_WATCHR=1" + fi + echo -e "Building 033円[0;32m$PHPCTL_IMAGE033円[0m" + # shellcheck disable=SC2068 # shellcheck disable=SC2154 - $PHPCTL_RUNTIME build \ + $PHPCTL_RUNTIME buildx build \ --build-arg PHP="$PHP_VERSION" \ --build-arg COMPOSER_AUTH="$COMPOSER_AUTH" \ + --build-arg HOST_USER="$(whoami)" \ + $with_exakat \ + $without_watchr \ ${build[@]} -t "$PHPCTL_IMAGE" . } @@ -22,18 +36,48 @@ images() { } run() { - echo -e "Running 033円[0;32m$PHPCTL_IMAGE033円[0m" + if [ -n "$PHPCTL_VERBOSE" ]; then + echo -e "Running 033円[0;32m$PHPCTL_IMAGE033円[0m" + fi + + local phpctl_ini="" + if [ -s phpctl.ini ]; then + phpctl_ini="-v $(pwd)/phpctl.ini:/etc/php$PHP_VERSION/conf.d/zzzphp.ini" + fi + + local composer_home="" + composer_home="$(composer-home)" + if [ -n "$composer_home" ]; then + composer_home="-v $composer_home:$composer_home" + fi + + local gitconfig="" + if [ -f ~/.gitconfig ]; then + gitconfig="-v $HOME/.gitconfig:/root/.gitconfig:ro" + fi + + if [ -n "$GIT_EXEC_PATH" ]; then + # In a Git hook environment, we need to disable TTY allocation + PHPCTL_TTY="--label=no-tty" + fi + # shellcheck disable=SC2046 + # shellcheck disable=SC2068 + # shellcheck disable=SC2086 # shellcheck disable=SC2154 $PHPCTL_RUNTIME run \ + --init \ --platform linux/x86_64 \ --rm "$PHPCTL_TTY" \ --name "phpctl_$(openssl rand -hex 6)" \ + --user "$PHPCTL_USER" \ $(env | awk -F= '/^[[:alpha:]]/{print 1ドル}' | sed 's/^/-e/') \ -v /var/run/docker.sock:/var/run/docker.sock \ - -v ~/.gitconfig:/root/.gitconfig:ro \ + $gitconfig \ -v "$(pwd)":/usr/local/src -w /usr/local/src \ - -v "$(pwd)"/phpctl.ini:/etc/php"$PHP_VERSION"/conf.d/zzzphp.ini \ + -v "$PHPCTL_DIR/php.ini:/etc/php$PHP_VERSION/conf.d/zphp.ini" \ + $phpctl_ini \ + $composer_home \ --net host --entrypoint sh \ ${args[@]} "1ドル" "$PHPCTL_IMAGE" -c "${*:2}" } diff --git a/src/doctor.sh b/src/doctor.sh index c9bcef5..61722e6 100644 --- a/src/doctor.sh +++ b/src/doctor.sh @@ -1,4 +1,9 @@ doctor() { - echo -e "033円[0;32mPHP_VERSION033円[0m=$PHP_VERSION" - echo -e "PHPCTL_IMAGE=033円[0;33m$PHPCTL_IMAGE033円[0m" + echo -e "PHP_VERSION=033円[0;32m$PHP_VERSION033円[0m" + + for var in $(set | awk -F= '/^[[:alpha:]]/{print 1ドル}'); do + if [[ $var == PHPCTL_* ]]; then + echo -e "$var=033円[0;32m${!var}033円[0m" + fi + done } diff --git a/src/help.sh b/src/help.sh index 50bf94e..0c63e4c 100644 --- a/src/help.sh +++ b/src/help.sh @@ -33,15 +33,23 @@ help() { echo -e "033円[0;32m images 033円[0m Shows local phpctl images" echo -e "" echo -e "033円[0;33mTools:033円[0m" - echo -e "033円[0;32m phpunit 033円[0m Runs PHPUnit" - echo -e "033円[0;32m php-cs-fixer 033円[0m Runs PHP-CS-Fixer" - echo -e "033円[0;32m phpstan 033円[0m Runs PHPStan" - echo -e "033円[0;32m infection 033円[0m Runs Infection, a PHP Mutation Testing Framework" - echo -e "033円[0;32m pest 033円[0m Runs Pest" - echo -e "033円[0;32m pint 033円[0m Runs Pint" echo -e "033円[0;32m box 033円[0m Runs Box (PHAR builder)" + echo -e "033円[0;32m co-phpunit 033円[0m Runs co-phpunit (Coroutine-aware PHPUnit for Hyperf)" + echo -e "033円[0;32m composer-require-checker 033円[0m Runs ComposerRequireChecker" + echo -e "033円[0;32m couscous 033円[0m Runs Couscous" + echo -e "033円[0;32m deptrac 033円[0m Runs Deptrac" echo -e "033円[0;32m exakat 033円[0m Runs Exakat" echo -e "033円[0;32m frankenphp 033円[0m Runs FrankenPHP" + echo -e "033円[0;32m infection 033円[0m Runs Infection, a PHP Mutation Testing Framework" + echo -e "033円[0;32m pest 033円[0m Runs Pest" + echo -e "033円[0;32m php-cs-fixer 033円[0m Runs PHP-CS-Fixer" + echo -e "033円[0;32m phpcbf 033円[0m Runs PHP_CodeSniffer (phpcbf)" + echo -e "033円[0;32m phpcs 033円[0m Runs PHP_CodeSniffer (phpcs)" + echo -e "033円[0;32m phpmd 033円[0m Runs PHP Mess Detector" + echo -e "033円[0;32m phpstan 033円[0m Runs PHPStan" + echo -e "033円[0;32m phpunit 033円[0m Runs PHPUnit" + echo -e "033円[0;32m pint 033円[0m Runs Pint" echo -e "033円[0;32m rector 033円[0m Runs Rector" + echo -e "033円[0;32m watchr 033円[0m Runs watchr" echo -e "" } diff --git a/src/php.sh b/src/php.sh index 30fd356..54cfbf0 100644 --- a/src/php.sh +++ b/src/php.sh @@ -5,7 +5,19 @@ php() { } composer() { - run -- composer ${@} + if [ "1ドル" = "global" ]; then + COMPOSER_HOME=$(composer-home) + if [ -z "$COMPOSER_HOME" ]; then + echo -e "033円[0;31mCOMPOSER_HOME not set.033円[0m" + exit 1 + fi + + cd "$COMPOSER_HOME" || exit 1 + echo -e "033円[0;32mChanged current directory to $COMPOSER_HOME033円[0m" + run -- composer ${@:2} + else + run -- composer ${@} + fi } repl() { @@ -17,3 +29,15 @@ server() { dir=${2:-.} run -- php -S 0.0.0.0:"$port" -t "$dir" } + +composer-home() { + if [ -z "$COMPOSER_HOME" ]; then + if [ -d ~/.config/composer ]; then + COMPOSER_HOME="$HOME/.config/composer" + elif [ -d ~/.composer ]; then + COMPOSER_HOME="$HOME/.composer" + fi + fi + + echo "$COMPOSER_HOME" +} diff --git a/src/self-update.sh b/src/self-update.sh index 1f42cce..0a0b37a 100644 --- a/src/self-update.sh +++ b/src/self-update.sh @@ -1,6 +1,22 @@ +is_brew_installed() { + command -v brew>/dev/null 2>&1 +} + +is_tap_installed() { + local tap_name="1ドル" + brew tap | grep -q "^$tap_name$" +} + self-update() { + local tap_to_check="opencodeco/phpctl" + local full_tap_pkg="$tap_to_check/phpctl" + + if is_brew_installed && is_tap_installed "$tap_to_check"; then + echo -e "033円[32mSeems phpctl was installed by Homebrew, to update it run033円[0m brew upgrade $full_tap_pkg" + else echo -e "Heading to 033円[33m$PHPCTL_DIR033円[0m to update..." cd "$PHPCTL_DIR" || exit 1 git pull origin HEAD echo -e "033円[32mUpdated!033円[0m" + fi } diff --git a/src/tools.sh b/src/tools.sh index 536f5d6..80dd187 100644 --- a/src/tools.sh +++ b/src/tools.sh @@ -6,6 +6,18 @@ box() { fi; } +co-phpunit() { + if [ -f vendor/bin/co-phpunit ]; then + run -- vendor/bin/co-phpunit ${@} + else + run -- co-phpunit ${@} + fi; +} + +composer-require-checker() { + run -- composer-require-checker ${@} +} + couscous() { if [ -f vendor/bin/couscous ]; then run -- vendor/bin/couscous ${@} @@ -14,6 +26,10 @@ couscous() { fi; } +deptrac() { + run -- deptrac ${@} +} + exakat() { if [ -f vendor/bin/exakat ]; then run -- vendor/bin/exakat ${@} @@ -42,6 +58,30 @@ php-cs-fixer() { fi; } +phpcbf() { + if [ -f vendor/bin/phpcbf ]; then + run -- vendor/bin/phpcbf ${@} + else + run -- phpcbf ${@} + fi; +} + +phpcs() { + if [ -f vendor/bin/phpcs ]; then + run -- vendor/bin/phpcs ${@} + else + run -- phpcs ${@} + fi; +} + +phpmd() { + if [ -f vendor/bin/phpmd ]; then + run -- vendor/bin/phpmd ${@} + else + run -- phpmd ${@} + fi; +} + phpstan() { if [ -f vendor/bin/phpstan ]; then run -- vendor/bin/phpstan ${@} @@ -69,3 +109,7 @@ pint() { rector() { run -- vendor/bin/rector ${@} } + +watchr() { + run -- watchr ${@} +} diff --git a/tests/install/Makefile b/tests/install/Makefile new file mode 100644 index 0000000..b642b85 --- /dev/null +++ b/tests/install/Makefile @@ -0,0 +1,7 @@ +.PHONY: build +build: + @parallel --line-buffer ./build {} ::: alpine archlinux ubuntu + +.PHONY: test +test: + @parallel --line-buffer ./test {} ::: alpine archlinux ubuntu diff --git a/tests/install/README.md b/tests/install/README.md new file mode 100644 index 0000000..a77d308 --- /dev/null +++ b/tests/install/README.md @@ -0,0 +1,5 @@ +This tests the `phpctl` installation in a blank Alpine distribution. + +Run `make OS=` then inside the container run `make install`. + +To check the installation, run `make test`. diff --git a/tests/install/alpine.Dockerfile b/tests/install/alpine.Dockerfile new file mode 100644 index 0000000..a5b7144 --- /dev/null +++ b/tests/install/alpine.Dockerfile @@ -0,0 +1,4 @@ +FROM alpine +RUN apk add bash curl docker git make neofetch openssl sudo +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/tests/install/archlinux.Dockerfile b/tests/install/archlinux.Dockerfile new file mode 100644 index 0000000..fbe2c61 --- /dev/null +++ b/tests/install/archlinux.Dockerfile @@ -0,0 +1,4 @@ +FROM archlinux +RUN pacman -Syu --noconfirm curl docker git make neofetch sudo +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/tests/install/build b/tests/install/build new file mode 100755 index 0000000..b403ef9 --- /dev/null +++ b/tests/install/build @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +echo -e "[1ドル] 033円[0;32mBuilding...033円[0m" +docker build -t "phpctl-1ドル-test" -f "1ドル.Dockerfile" . &> /dev/null +echo -e "[1ドル] 033円[0;32mDone!033円[0m" diff --git a/tests/install/docker-entrypoint.sh b/tests/install/docker-entrypoint.sh new file mode 100755 index 0000000..e62e540 --- /dev/null +++ b/tests/install/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +neofetch +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/phpctl/refs/heads/main/docs/install.sh)" +echo "" +notty phpctl doctor +notty php --version +notty composer --version diff --git a/tests/install/test b/tests/install/test new file mode 100755 index 0000000..127dd2a --- /dev/null +++ b/tests/install/test @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo -e "[1ドル] 033円[0;32mTesting...033円[0m" +echo -e "$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "phpctl-1ドル-test")" diff --git a/tests/install/ubuntu.Dockerfile b/tests/install/ubuntu.Dockerfile new file mode 100644 index 0000000..2962b97 --- /dev/null +++ b/tests/install/ubuntu.Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu +RUN apt update && apt install -y bash curl docker.io git make neofetch openssl sudo +COPY docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/tests/php_test.sh b/tests/php_test.sh index ffa36a6..9ebab2d 100644 --- a/tests/php_test.sh +++ b/tests/php_test.sh @@ -8,5 +8,10 @@ function test_php_accepts_arguments() { } function test_composer() { - assert_contains "version" "$(./bin/phpctl composer --version)" + assert_contains "Composer version 2" "$(./bin/phpctl composer)" +} + +function test_phpctl_ini() { + echo "memory_limit=1337M"> phpctl.ini + assert_contains "memory_limit => 1337M => 1337M" "$(./bin/phpctl php -i | grep memory_limit)" } diff --git a/tests/tools_test.sh b/tests/tools_test.sh index 4275891..f3bc3db 100644 --- a/tests/tools_test.sh +++ b/tests/tools_test.sh @@ -1,31 +1,67 @@ function test_box() { - assert_contains "Box version 4.5.1" "$(./bin/phpctl box --version)" + assert_matches "Box version 4\." "$(./bin/phpctl box --version)" +} + +function test_co_phpunit() { + assert_contains "Swoole\Coroutine\run" "$(./bin/phpctl sh cat /usr/local/bin/co-phpunit)" +} + +function test_composer_require_checker() { + assert_matches "ComposerRequireChecker 4\." "$(./bin/phpctl composer-require-checker --version)" } function test_couscous() { - assert_contains "Couscous 1.10.0" "$(./bin/phpctl couscous --version)" + assert_matches "Couscous 1\." "$(./bin/phpctl couscous --version)" +} + +function test_deptrac() { + assert_matches "deptrac 1\." "$(./bin/phpctl deptrac --version)" } function test_exakat() { - assert_contains "Version : 2.6.2" "$(./bin/phpctl exakat version)" + if [ -n "$WITH_EXAKAT" ]; then + assert_matches "Version : 2\." "$(./bin/phpctl exakat version)" + fi } function test_infection() { - assert_contains "Infection - PHP Mutation Testing Framework version 0.27.9" "$(./bin/phpctl infection --version)" + assert_matches "Infection - PHP Mutation Testing Framework version 0\.27\." "$(./bin/phpctl infection --version)" } function test_php_cs_fixer() { - assert_contains "PHP CS Fixer 3.46.0" "$(./bin/phpctl php-cs-fixer --version)" + assert_matches "PHP CS Fixer 3\." "$(./bin/phpctl php-cs-fixer --version)" +} + +function test_phpcbf() { + assert_matches "PHP_CodeSniffer version 3\." "$(./bin/phpctl phpcbf --version)" +} + +function test_phpcs() { + assert_matches "PHP_CodeSniffer version 3\." "$(./bin/phpctl phpcs --version)" +} + +function test_phpmd() { + assert_matches "PHPMD 2\." "$(./bin/phpctl phpmd --version)" } function test_phpstan() { - assert_contains "PHPStan - PHP Static Analysis Tool 1.10.55" "$(./bin/phpctl phpstan --version)" + assert_matches "PHPStan - PHP Static Analysis Tool 1\." "$(./bin/phpctl phpstan --version)" } function test_phpunit() { - assert_contains "PHPUnit 10.5.9" "$(./bin/phpctl phpunit --version)" + if [ "$PHP_VERSION" = "81" ]; then + assert_matches "PHPUnit 10\." "$(./bin/phpctl phpunit --version)" + else + assert_matches "PHPUnit 11\." "$(./bin/phpctl phpunit --version)" + fi } function test_pint() { - assert_contains "Pint 1.13.7" "$(./bin/phpctl pint --version)" + assert_matches "Pint 1\." "$(./bin/phpctl pint --version)" +} + +function test_watchr() { + if [ -z "$WITHOUT_WATCHR" ] && ! [ "$PHP_VERSION" = "81" ]; then + assert_matches "watchr command-line utility v0\.5\." "$(./bin/phpctl watchr --version)" + fi }