diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..ebb5767 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,35 @@ +name: "Static analysis" + +on: + push: + branches: + - "main" + - "master" + pull_request: null + +jobs: + static-analysis: + runs-on: "ubuntu-latest" + name: "PHPStan on PHP ${{ matrix.php }}" + strategy: + fail-fast: false + matrix: + php: + - "8.1" + steps: + - name: "Check out repository code" + uses: "actions/checkout@v2" + + - name: "Setup PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php }}" + tools: "composer" + + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Perform static analysis" + run: "make phpstan" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..1dce3a3 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,78 @@ +name: "Unit Tests" + +on: + push: + branches: + - "main" + - "master" + pull_request: null + +jobs: + unit-tests: + runs-on: "ubuntu-latest" + name: "Unit Tests on PHP ${{ matrix.php }} and ${{ matrix.tools }}" + strategy: + fail-fast: false + matrix: + php: + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + tools: [ "composer" ] + include: + - php: "7.2" + tools: "composer:v2.0" + + steps: + - name: "Check out repository code" + uses: "actions/checkout@v2" + + - name: "Setup PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php }}" + tools: "${{ matrix.tools }}" + + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Validate composer.json" + run: "composer validate --strict --no-check-lock" + + - name: "Run tests" + run: "vendor/bin/phpunit --group default" + + e2e-tests: + runs-on: "ubuntu-latest" + name: "E2E Tests on PHP ${{ matrix.php }}" + strategy: + fail-fast: false + matrix: + php: + - "8.1" + + steps: + - name: "Check out repository code" + uses: "actions/checkout@v2" + + - name: "Setup PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php }}" + tools: "composer" + + - name: "Correct bin plugin version for e2e scenarios (PR-only)" + if: github.event_name == 'pull_request' + run: find e2e -maxdepth 1 -mindepth 1 -type d -exec bash -c "cd {} && composer require --dev bamarni/composer-bin-plugin:dev-${GITHUB_SHA} --no-update" \; + + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + + - name: "Run tests" + run: "vendor/bin/phpunit --group e2e" diff --git a/.gitignore b/.gitignore index d45ca06..42e182a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ -composer.lock -vendor -vendor-bin/*/vendor +/composer.lock +/dist/ +/tools/ +/vendor/ +/vendor-bin/*/vendor/ +/.phpunit.result.cache +/.php-cs-fixer.cache diff --git a/.makefile/touch.sh b/.makefile/touch.sh new file mode 100755 index 0000000..5c0a775 --- /dev/null +++ b/.makefile/touch.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Takes a given string, e.g. 'bin/console' or 'docker-compose exec php bin/console' +# and split it by words. For each words, if the target is a file, it is touched. +# +# This allows to implement a similar rule to: +# +# ```Makefile +# bin/php-cs-fixer: vendor +# touch $@ +# ``` +# +# Indeed when the rule `bin/php-cs-fixer` is replaced with a docker-compose +# equivalent, it will not play out as nicely. +# +# Arguments: +# $1 - {string} Command potentially containing a file +# + +set -Eeuo pipefail; + + +readonly ERROR_COLOR="\e[41m"; +readonly NO_COLOR="\e[0m"; + + +if [ $# -ne 1 ]; then + printf "${ERROR_COLOR}Illegal number of parameters.${NO_COLOR}\n"; + + exit 1; +fi + + +readonly FILES="$1"; + + +####################################### +# Touch the given file path if the target is a file and do not create the file +# if does not exist. +# +# Globals: +# None +# +# Arguments: +# $1 - {string} File path +# +# Returns: +# None +####################################### +touch_file() { + local file="$1"; + + if [ -e ${file} ]; then + touch -c ${file}; + fi +} + +for file in ${FILES} +do + touch_file ${file}; +done diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 0000000..335086e --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..507763a --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,17 @@ +files() + ->in(['src', 'tests']); + +$config = new PhpCsFixer\Config(); + +return $config + ->setRules([ + '@PSR12' => true, + 'strict_param' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_unused_imports' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b1da73c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,51 +0,0 @@ -language: php - -cache: - directories: - - "$HOME/.composer/cache" - -jobs: - include: - - name: PHP 5.5.9 - php: 5.5.9 - dist: trusty - env: COMPOSER_FLAGS='--prefer-lowest' - - name: PHP 5.5 - php: 5.5 - dist: trusty - - name: PHP 5.6 - php: 5.6 - dist: xenial - - name: PHP 7.0 - php: 7.0 - dist: xenial - - name: PHP 7.1 - php: 7.1 - dist: bionic - - name: PHP 7.2 - php: 7.2 - dist: bionic - - name: PHP 7.3 - php: 7.3 - dist: bionic - - name: PHP 7.4 - php: 7.4 - dist: bionic - - name: PHP 8.0 - php: nightly - dist: bionic - allow_failures: - - php: nightly - -before_install: - - composer global config repositories.bin path $PWD - - composer global require bamarni/composer-bin-plugin:dev-master - -install: - - composer update --no-interaction - -script: - - vendor/bin/phpunit - -notifications: - email: false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..031dfde --- /dev/null +++ b/Makefile @@ -0,0 +1,121 @@ +# See https://tech.davis-hansson.com/p/make/ +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +# General variables +TOUCH = bash .makefile/touch.sh + +# PHP variables +COMPOSER=composer +COVERAGE_DIR = dist/coverage +INFECTION_BIN = tools/infection +INFECTION = php -d zend.enable_gc=0 $(INFECTION_BIN) --skip-initial-tests --coverage=$(COVERAGE_DIR) --only-covered --threads=4 --min-msi=100 --min-covered-msi=100 --ansi +PHPUNIT_BIN = vendor/bin/phpunit +PHPUNIT = php -d zend.enable_gc=0 $(PHPUNIT_BIN) +PHPUNIT_COVERAGE = XDEBUG_MODE=coverage $(PHPUNIT) --group default --coverage-xml=$(COVERAGE_DIR)/coverage-xml --log-junit=$(COVERAGE_DIR)/phpunit.junit.xml +PHPSTAN_BIN = vendor/bin/phpstan +PHPSTAN = $(PHPSTAN_BIN) analyse --level=5 src tests +PHP_CS_FIXER_BIN = tools/php-cs-fixer +PHP_CS_FIXER = $(PHP_CS_FIXER_BIN) fix --ansi --verbose --config=.php-cs-fixer.php +COMPOSER_NORMALIZE_BIN=tools/composer-normalize +COMPOSER_NORMALIZE = ./$(COMPOSER_NORMALIZE_BIN) + + +.DEFAULT_GOAL := default + + +# +# Command +#--------------------------------------------------------------------------- + +.PHONY: help +help: ## Shows the help +help: + @printf "\033[33mUsage:\033[0m\n make TARGET\n\n\033[32m#\n# Commands\n#---------------------------------------------------------------------------\033[0m\n" + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | awk 'BEGIN {FS = ":"}; {printf "\033[33m%s:\033[0m%s\n", $$1, $$2}' + + +.PHONY: default +default: ## Runs the default task: CS fix and all the tests +default: cs test + + +.PHONY: cs +cs: ## Runs PHP-CS-Fixer +cs: $(PHP_CS_FIXER_BIN) $(COMPOSER_NORMALIZE_BIN) + $(PHP_CS_FIXER) + $(COMPOSER_NORMALIZE) + + +.PHONY: phpstan +phpstan: ## Runs PHPStan +phpstan: + $(PHPSTAN) + + +.PHONY: infection +infection: ## Runs infection +infection: $(INFECTION_BIN) $(COVERAGE_DIR) vendor + if [ -d $(COVERAGE_DIR)/coverage-xml ]; then $(INFECTION); fi + + +.PHONY: test +test: ## Runs all the tests +test: validate-package phpstan $(COVERAGE_DIR) e2e #infection include infection later + + +.PHONY: validate-package +validate-package: ## Validates the Composer package +validate-package: vendor + $(COMPOSER) validate --strict + + +.PHONY: coverage +coverage: ## Runs PHPUnit with code coverage +coverage: $(PHPUNIT_BIN) vendor + $(PHPUNIT_COVERAGE) + + +.PHONY: unit-test +unit-test: ## Runs PHPUnit (default group) +unit-test: $(PHPUNIT_BIN) vendor + $(PHPUNIT) --group default + + +.PHONY: e2e +e2e: ## Runs PHPUnit end-to-end tests +e2e: $(PHPUNIT_BIN) vendor + $(PHPUNIT) --group e2e + + +# +# Rules +#--------------------------------------------------------------------------- + +# Vendor does not depend on the composer.lock since the later is not tracked +# or committed. +vendor: composer.json + $(COMPOSER) update + $(TOUCH) "$@" + +$(PHPUNIT_BIN): vendor + $(TOUCH) "$@" + +$(INFECTION_BIN): ./.phive/phars.xml + phive install infection + $(TOUCH) "$@" + +$(COMPOSER_NORMALIZE_BIN): ./.phive/phars.xml + phive install composer-normalize + $(TOUCH) "$@" + +$(COVERAGE_DIR): $(PHPUNIT_BIN) src tests phpunit.xml.dist + $(PHPUNIT_COVERAGE) + $(TOUCH) "$@" + +$(PHP_CS_FIXER_BIN): vendor + phive install php-cs-fixer + $(TOUCH) "$@" + +$(PHPSTAN_BIN): vendor + $(TOUCH) "$@" diff --git a/README.md b/README.md index 46c6349..4c8021b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ 1. [Forward mode](#forward-mode) 1. [Reduce clutter](#reduce-clutter) 1. [Related plugins](#related-plugins) +1. [Backward Compatibility Promise](#backward-compatibility-promise) +1. [Contributing](#contributing) ## Why? @@ -178,6 +180,10 @@ wish to disable that behaviour, you can do so by adding a little setting in the } ``` +Note that otherwise, in case of conflicts (e.g. `phpstan` is present in two namespaces), only the +first one is linked and the second one is ignored. + + ### Change directory By default, the packages are looked for in the `vendor-bin` directory. The location can be changed using `target-directory` value in the extra config: @@ -206,7 +212,7 @@ There is a `forward mode` which is disabled by default. This can be activated by } ``` -If this mode is activated, all your `composer install` and `composer update` commands are forwared to all bin directories. +If this mode is activated, all your `composer install` and `composer update` commands are forwarded to all bin directories. This is an replacement for the tasks shown in section [Auto-installation](#auto-installation). ### Reduce clutter @@ -229,6 +235,30 @@ vendor-bin/**/composer.lock binary * [theofidry/composer-inheritance-plugin][7]: Opinionated version of [Wikimedia composer-merge-plugin][8] to work in pair with this plugin. +## Backward Compatibility Promise + +The backward compatibility promise only applies to the following API: + +- The commands registered by the plugin +- The behaviour of the commands (but not their logging/output) +- The Composer configuration + +The plugin implementation is considered to be strictly internal and its code may +change at any time in a non back-ward compatible way. + + +## Contributing + +A makefile is available to help out: + +```bash +$ make # Runs all checks +$ make help # List all available commands +``` + +**Note:** you do need to install [phive][phive] first. + + [1]: https://github.com/etsy/phan [2]: https://github.com/phpmetrics/PhpMetrics [3]: https://getcomposer.org/doc/06-config.md#bin-dir @@ -237,3 +267,5 @@ vendor-bin/**/composer.lock binary [6]: https://github.com/nikic/PHP-Parser [7]: https://github.com/theofidry/composer-inheritance-plugin [8]: https://github.com/wikimedia/composer-merge-plugin +[phive]: https://phar.io/ +[symfony-bc-policy]: https://symfony.com/doc/current/contributing/code/bc.html diff --git a/composer.json b/composer.json index 89752c4..06a4b5e 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,8 @@ { "name": "bamarni/composer-bin-plugin", - "type": "composer-plugin", "description": "No conflicts for your bin dependencies", + "license": "MIT", + "type": "composer-plugin", "keywords": [ "composer", "dependency", @@ -10,20 +11,20 @@ "conflict", "executable" ], - "license": "MIT", "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", - "composer-plugin-api": "^1.0 || ^2.0" + "php": "^7.2.5 || ^8.0", + "composer-plugin-api": "^2.0" }, "require-dev": { - "composer/composer": "^1.0 || ^2.0", - "symfony/console": "^2.5 || ^3.0 || ^4.0" - }, - "config": { - "sort-packages": true - }, - "extra": { - "class": "Bamarni\\Composer\\Bin\\Plugin" + "ext-json": "*", + "composer/composer": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^5.4.7 || ^6.0.7", + "symfony/finder": "^5.4.7 || ^6.0.7", + "symfony/process": "^5.4.7 || ^6.0.7" }, "autoload": { "psr-4": { @@ -35,12 +36,15 @@ "Bamarni\\Composer\\Bin\\Tests\\": "tests" } }, - "scripts": { - "post-install-cmd": [ - "@composer bin phpunit install" - ], - "post-update-cmd": [ - "@post-install-cmd" - ] + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "ergebnis/composer-normalize": true, + "infection/extension-installer": true + }, + "sort-packages": true + }, + "extra": { + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" } } diff --git a/e2e/scenario0/.gitignore b/e2e/scenario0/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario0/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario0/README.md b/e2e/scenario0/README.md new file mode 100644 index 0000000..6b2a3c9 --- /dev/null +++ b/e2e/scenario0/README.md @@ -0,0 +1 @@ +Regular installation on a project with multiple namespaces. diff --git a/e2e/scenario0/composer.json b/e2e/scenario0/composer.json new file mode 100644 index 0000000..dff4cca --- /dev/null +++ b/e2e/scenario0/composer.json @@ -0,0 +1,16 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + } +} diff --git a/e2e/scenario0/expected.txt b/e2e/scenario0/expected.txt new file mode 100644 index 0000000..a192945 --- /dev/null +++ b/e2e/scenario0/expected.txt @@ -0,0 +1,16 @@ +[bamarni-bin] Checking namespace vendor-bin/ns1 +Loading composer repositories with package information +Updating dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Checking namespace vendor-bin/ns2 +Loading composer repositories with package information +Updating dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files diff --git a/e2e/scenario0/script.sh b/e2e/scenario0/script.sh new file mode 100755 index 0000000..3557486 --- /dev/null +++ b/e2e/scenario0/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update + +# Actual command to execute the test itself +composer bin all update 2>&1 | tee > actual.txt diff --git a/e2e/scenario0/vendor-bin/ns1/composer.json b/e2e/scenario0/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario0/vendor-bin/ns1/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario0/vendor-bin/ns2/composer.json b/e2e/scenario0/vendor-bin/ns2/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario0/vendor-bin/ns2/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario1/.gitignore b/e2e/scenario1/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario1/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario1/README.md b/e2e/scenario1/README.md new file mode 100644 index 0000000..4552c26 --- /dev/null +++ b/e2e/scenario1/README.md @@ -0,0 +1 @@ +Regular installation on a project with multiple namespaces in verbose mode. diff --git a/e2e/scenario1/composer.json b/e2e/scenario1/composer.json new file mode 100644 index 0000000..dff4cca --- /dev/null +++ b/e2e/scenario1/composer.json @@ -0,0 +1,16 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + } +} diff --git a/e2e/scenario1/expected.txt b/e2e/scenario1/expected.txt new file mode 100644 index 0000000..d0dcc1c --- /dev/null +++ b/e2e/scenario1/expected.txt @@ -0,0 +1,28 @@ +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario1 +[bamarni-bin] Configuring bin directory to /path/to/project/e2e/scenario1/vendor/bin. +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --verbose --working-dir='.'`. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario1. +[bamarni-bin] Checking namespace vendor-bin/ns2 +[bamarni-bin] Changed current directory to vendor-bin/ns2. +[bamarni-bin] Running `@composer update --verbose --working-dir='.'`. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario1. diff --git a/e2e/scenario1/script.sh b/e2e/scenario1/script.sh new file mode 100755 index 0000000..575b36b --- /dev/null +++ b/e2e/scenario1/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update + +# Actual command to execute the test itself +composer bin all update --verbose 2>&1 | tee > actual.txt diff --git a/e2e/scenario1/vendor-bin/ns1/composer.json b/e2e/scenario1/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario1/vendor-bin/ns1/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario1/vendor-bin/ns2/composer.json b/e2e/scenario1/vendor-bin/ns2/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario1/vendor-bin/ns2/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario10/.gitignore b/e2e/scenario10/.gitignore new file mode 100644 index 0000000..1fced8e --- /dev/null +++ b/e2e/scenario10/.gitignore @@ -0,0 +1,6 @@ +/actual.txt +/.composer/ +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario10/README.md b/e2e/scenario10/README.md new file mode 100644 index 0000000..0ffbf03 --- /dev/null +++ b/e2e/scenario10/README.md @@ -0,0 +1 @@ +Check that the composer environment variables are well respected when commands are forwarded to the namespaces. diff --git a/e2e/scenario10/composer.json b/e2e/scenario10/composer.json new file mode 100644 index 0000000..3061d0c --- /dev/null +++ b/e2e/scenario10/composer.json @@ -0,0 +1,21 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "forward-command": true + } + } +} diff --git a/e2e/scenario10/expected.txt b/e2e/scenario10/expected.txt new file mode 100644 index 0000000..033cf0b --- /dev/null +++ b/e2e/scenario10/expected.txt @@ -0,0 +1,2 @@ +./.composer +.composer diff --git a/e2e/scenario10/script.sh b/e2e/scenario10/script.sh new file mode 100755 index 0000000..4177416 --- /dev/null +++ b/e2e/scenario10/script.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf .composer || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true +rm -rf vendor-bin/*/.composer || true + +readonly CUSTOM_COMPOSER_DIR=$(pwd)/.composer +COMPOSER_CACHE_DIR=$CUSTOM_COMPOSER_DIR composer update + +# Actual command to execute the test itself +find . ".composer" -name ".composer" -type d 2>&1 | sort -n | tee > actual.txt || true diff --git a/e2e/scenario10/vendor-bin/ns1/composer.json b/e2e/scenario10/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..9871ea3 --- /dev/null +++ b/e2e/scenario10/vendor-bin/ns1/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "nikic/iter": "v1.6.0" + } +} diff --git a/e2e/scenario11/.gitignore b/e2e/scenario11/.gitignore new file mode 100644 index 0000000..1fced8e --- /dev/null +++ b/e2e/scenario11/.gitignore @@ -0,0 +1,6 @@ +/actual.txt +/.composer/ +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario11/README.md b/e2e/scenario11/README.md new file mode 100644 index 0000000..03ab64e --- /dev/null +++ b/e2e/scenario11/README.md @@ -0,0 +1 @@ +Check that the deprecation messages are well rendered. diff --git a/e2e/scenario11/composer.json b/e2e/scenario11/composer.json new file mode 100644 index 0000000..3061d0c --- /dev/null +++ b/e2e/scenario11/composer.json @@ -0,0 +1,21 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "forward-command": true + } + } +} diff --git a/e2e/scenario11/expected.txt b/e2e/scenario11/expected.txt new file mode 100644 index 0000000..0368aeb --- /dev/null +++ b/e2e/scenario11/expected.txt @@ -0,0 +1,19 @@ +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking bamarni/composer-bin-plugin (dev-hash) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals + - Installing bamarni/composer-bin-plugin (dev-hash): Symlinking from ../.. +Generating autoload files +[bamarni-bin] The setting "bamarni-bin.bin-links" will be set to "false" from 2.x onwards. If you wish to keep it to "true", you need to set it explicitly. +[bamarni-bin] The command is being forwarded. +[bamarni-bin] Checking namespace vendor-bin/ns1 +Loading composer repositories with package information +Updating dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files diff --git a/e2e/scenario11/script.sh b/e2e/scenario11/script.sh new file mode 100755 index 0000000..e73e86f --- /dev/null +++ b/e2e/scenario11/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf .composer || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true +rm -rf vendor-bin/*/.composer || true + +# Actual command to execute the test itself +composer update 2>&1 | tee > actual.txt diff --git a/e2e/scenario11/vendor-bin/ns1/composer.json b/e2e/scenario11/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario11/vendor-bin/ns1/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario2/.gitignore b/e2e/scenario2/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario2/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario2/README.md b/e2e/scenario2/README.md new file mode 100644 index 0000000..432e6c8 --- /dev/null +++ b/e2e/scenario2/README.md @@ -0,0 +1 @@ +Regular script (same name) in different namespaces diff --git a/e2e/scenario2/composer.json b/e2e/scenario2/composer.json new file mode 100644 index 0000000..dff4cca --- /dev/null +++ b/e2e/scenario2/composer.json @@ -0,0 +1,16 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + } +} diff --git a/e2e/scenario2/expected.txt b/e2e/scenario2/expected.txt new file mode 100644 index 0000000..4fed774 --- /dev/null +++ b/e2e/scenario2/expected.txt @@ -0,0 +1,6 @@ +[bamarni-bin] Checking namespace vendor-bin/ns1 +> echo ns1 +ns1 +[bamarni-bin] Checking namespace vendor-bin/ns2 +> echo ns2 +ns2 diff --git a/e2e/scenario2/script.sh b/e2e/scenario2/script.sh new file mode 100755 index 0000000..8b9f863 --- /dev/null +++ b/e2e/scenario2/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update + +# Actual command to execute the test itself +composer bin all run-script foo 2>&1 | tee > actual.txt diff --git a/e2e/scenario2/vendor-bin/ns1/composer.json b/e2e/scenario2/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..7f276e9 --- /dev/null +++ b/e2e/scenario2/vendor-bin/ns1/composer.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "foo": "echo ns1" + } +} diff --git a/e2e/scenario2/vendor-bin/ns2/composer.json b/e2e/scenario2/vendor-bin/ns2/composer.json new file mode 100644 index 0000000..3b8933b --- /dev/null +++ b/e2e/scenario2/vendor-bin/ns2/composer.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "foo": "echo ns2" + } +} diff --git a/e2e/scenario3/.gitignore b/e2e/scenario3/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario3/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario3/README.md b/e2e/scenario3/README.md new file mode 100644 index 0000000..ad1c3b8 --- /dev/null +++ b/e2e/scenario3/README.md @@ -0,0 +1 @@ +Regular installation on a project with forwarded mode enabled. diff --git a/e2e/scenario3/composer.json b/e2e/scenario3/composer.json new file mode 100644 index 0000000..a1cc737 --- /dev/null +++ b/e2e/scenario3/composer.json @@ -0,0 +1,22 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": true + } + } +} diff --git a/e2e/scenario3/expected.txt b/e2e/scenario3/expected.txt new file mode 100644 index 0000000..133ffe7 --- /dev/null +++ b/e2e/scenario3/expected.txt @@ -0,0 +1,59 @@ +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Lock file operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash + - Locking bamarni/composer-bin-plugin (dev-hash) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash + - Installing bamarni/composer-bin-plugin (dev-hash): Symlinking from ../.. +Generating autoload files +> post-autoload-dump: Bamarni\Composer\Bin\BamarniBinPlugin->onPostAutoloadDump +[bamarni-bin] Calling onPostAutoloadDump(). +[bamarni-bin] The command is being forwarded. +[bamarni-bin] Original input: update --verbose. +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario3 +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --verbose --working-dir='.'`. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario3. +––––––––––––––––––––– +[bamarni-bin] Calling onCommandEvent(). +[bamarni-bin] The command is being forwarded. +[bamarni-bin] Original input: update --verbose. +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario3 +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --verbose --working-dir='.'`. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario3. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +> post-autoload-dump: Bamarni\Composer\Bin\BamarniBinPlugin->onPostAutoloadDump +[bamarni-bin] Calling onPostAutoloadDump(). +[bamarni-bin] Command already forwarded within the process: skipping. diff --git a/e2e/scenario3/script.sh b/e2e/scenario3/script.sh new file mode 100755 index 0000000..fa19247 --- /dev/null +++ b/e2e/scenario3/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +# Actual command to execute the test itself +composer update --verbose 2>&1 | tee > actual.txt +echo "–––––––––––––––––––––" >> actual.txt +composer update --verbose 2>&1 | tee >> actual.txt diff --git a/e2e/scenario3/vendor-bin/ns1/composer.json b/e2e/scenario3/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario3/vendor-bin/ns1/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario5/.gitignore b/e2e/scenario5/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario5/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario5/README.md b/e2e/scenario5/README.md new file mode 100644 index 0000000..71f75d8 --- /dev/null +++ b/e2e/scenario5/README.md @@ -0,0 +1 @@ +Regular installation on a project with multiple namespaces and with links disabled. diff --git a/e2e/scenario5/composer.json b/e2e/scenario5/composer.json new file mode 100644 index 0000000..3d06a59 --- /dev/null +++ b/e2e/scenario5/composer.json @@ -0,0 +1,21 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": false + } + } +} diff --git a/e2e/scenario5/expected.txt b/e2e/scenario5/expected.txt new file mode 100644 index 0000000..3cdb7f2 --- /dev/null +++ b/e2e/scenario5/expected.txt @@ -0,0 +1,5 @@ +find: vendor/bin: No such file or directory +vendor-bin/ns1/vendor/bin/phpstan +vendor-bin/ns1/vendor/bin/phpstan.phar +vendor-bin/ns2/vendor/bin/phpstan +vendor-bin/ns2/vendor/bin/phpstan.phar diff --git a/e2e/scenario5/script.sh b/e2e/scenario5/script.sh new file mode 100755 index 0000000..4e58fb6 --- /dev/null +++ b/e2e/scenario5/script.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update +composer bin all update + +# Actual command to execute the test itself +find vendor/bin vendor-bin/*/vendor/bin -maxdepth 1 -type f 2>&1 | sort -n | tee > actual.txt || true diff --git a/e2e/scenario5/vendor-bin/ns1/composer.json b/e2e/scenario5/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..ba924f7 --- /dev/null +++ b/e2e/scenario5/vendor-bin/ns1/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpstan/phpstan": "1.8.0" + } +} diff --git a/e2e/scenario5/vendor-bin/ns2/composer.json b/e2e/scenario5/vendor-bin/ns2/composer.json new file mode 100644 index 0000000..ba924f7 --- /dev/null +++ b/e2e/scenario5/vendor-bin/ns2/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpstan/phpstan": "1.8.0" + } +} diff --git a/e2e/scenario6/.gitignore b/e2e/scenario6/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario6/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario6/README.md b/e2e/scenario6/README.md new file mode 100644 index 0000000..2174f99 --- /dev/null +++ b/e2e/scenario6/README.md @@ -0,0 +1 @@ +Regular installation on a project with multiple namespaces and with links enabled and conflicting symlinks. diff --git a/e2e/scenario6/composer.json b/e2e/scenario6/composer.json new file mode 100644 index 0000000..23f275b --- /dev/null +++ b/e2e/scenario6/composer.json @@ -0,0 +1,21 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": true + } + } +} diff --git a/e2e/scenario6/expected.txt b/e2e/scenario6/expected.txt new file mode 100644 index 0000000..25f159f --- /dev/null +++ b/e2e/scenario6/expected.txt @@ -0,0 +1,4 @@ +find: vendor-bin/*/vendor/bin: No such file or directory +vendor/bin/phpstan +vendor/bin/phpstan.phar +PHPStan - PHP Static Analysis Tool 1.8.0 diff --git a/e2e/scenario6/script.sh b/e2e/scenario6/script.sh new file mode 100755 index 0000000..1b972f5 --- /dev/null +++ b/e2e/scenario6/script.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update +composer bin all update + +# Actual command to execute the test itself +find vendor/bin vendor-bin/*/vendor/bin -maxdepth 1 -type f 2>&1 | sort -n | tee > actual.txt || true +vendor/bin/phpstan --version >> actual.txt diff --git a/e2e/scenario6/vendor-bin/ns1/composer.json b/e2e/scenario6/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..ba924f7 --- /dev/null +++ b/e2e/scenario6/vendor-bin/ns1/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpstan/phpstan": "1.8.0" + } +} diff --git a/e2e/scenario6/vendor-bin/ns2/composer.json b/e2e/scenario6/vendor-bin/ns2/composer.json new file mode 100644 index 0000000..7826cef --- /dev/null +++ b/e2e/scenario6/vendor-bin/ns2/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpstan/phpstan": "1.6.0" + } +} diff --git a/e2e/scenario7/.gitignore b/e2e/scenario7/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario7/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario7/README.md b/e2e/scenario7/README.md new file mode 100644 index 0000000..87140b4 --- /dev/null +++ b/e2e/scenario7/README.md @@ -0,0 +1 @@ +Tests that dev dependencies are not installed if no-dev is passed. diff --git a/e2e/scenario7/composer.json b/e2e/scenario7/composer.json new file mode 100644 index 0000000..bff880f --- /dev/null +++ b/e2e/scenario7/composer.json @@ -0,0 +1,16 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + } +} diff --git a/e2e/scenario7/expected.txt b/e2e/scenario7/expected.txt new file mode 100644 index 0000000..c00cece --- /dev/null +++ b/e2e/scenario7/expected.txt @@ -0,0 +1,10 @@ +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario7 +[bamarni-bin] Configuring bin directory to /path/to/project/e2e/scenario7/vendor/bin. +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --no-dev --verbose --working-dir='.' -- foo`. +Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file. +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario7. +––––––––––––––––––––– +[bamarni-bin] Checking namespace vendor-bin/ns1 +No dependencies installed. Try running composer install or update. diff --git a/e2e/scenario7/script.sh b/e2e/scenario7/script.sh new file mode 100755 index 0000000..ac11060 --- /dev/null +++ b/e2e/scenario7/script.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update + +# Actual command to execute the test itself +composer bin ns1 update --no-dev --verbose -- foo 2>&1 | tee > actual.txt || true +echo "–––––––––––––––––––––" >> actual.txt +composer bin ns1 show --direct --name-only 2>&1 | tee >> actual.txt || true diff --git a/e2e/scenario7/vendor-bin/ns1/composer.json b/e2e/scenario7/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..7b72340 --- /dev/null +++ b/e2e/scenario7/vendor-bin/ns1/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "nikic/iter": "v1.6.0" + }, + "require-dev": { + "phpstan/phpstan": "1.8.0" + } +} diff --git a/e2e/scenario8/.gitignore b/e2e/scenario8/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario8/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario8/README.md b/e2e/scenario8/README.md new file mode 100644 index 0000000..9af1b6d --- /dev/null +++ b/e2e/scenario8/README.md @@ -0,0 +1 @@ +Tests that extra arguments and options are not lost when forwarding the command to a bin namespace. diff --git a/e2e/scenario8/composer.json b/e2e/scenario8/composer.json new file mode 100644 index 0000000..7681c07 --- /dev/null +++ b/e2e/scenario8/composer.json @@ -0,0 +1,22 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": true + } + } +} diff --git a/e2e/scenario8/expected.txt b/e2e/scenario8/expected.txt new file mode 100644 index 0000000..625e41f --- /dev/null +++ b/e2e/scenario8/expected.txt @@ -0,0 +1,31 @@ +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Lock file operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash + - Locking bamarni/composer-bin-plugin (dev-hash) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals +Installs: bamarni/composer-bin-plugin:dev-hash + - Installing bamarni/composer-bin-plugin (dev-hash): Symlinking from ../.. +Generating autoload files +> post-autoload-dump: Bamarni\Composer\Bin\BamarniBinPlugin->onPostAutoloadDump +[bamarni-bin] Calling onPostAutoloadDump(). +[bamarni-bin] The command is being forwarded. +[bamarni-bin] Original input: update --prefer-lowest --verbose. +[bamarni-bin] Current working directory: /path/to/project/e2e/scenario8 +[bamarni-bin] Checking namespace vendor-bin/ns1 +[bamarni-bin] Changed current directory to vendor-bin/ns1. +[bamarni-bin] Running `@composer update --prefer-lowest --verbose --working-dir='.'`. +Loading composer repositories with package information +Updating dependencies +Analyzed 90 packages to resolve dependencies +Analyzed 90 rules to resolve dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files +[bamarni-bin] Changed current directory to /path/to/project/e2e/scenario8. diff --git a/e2e/scenario8/script.sh b/e2e/scenario8/script.sh new file mode 100755 index 0000000..93a8c2a --- /dev/null +++ b/e2e/scenario8/script.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +# Actual command to execute the test itself +composer update --prefer-lowest --verbose 2>&1 | tee >> actual.txt || true diff --git a/e2e/scenario8/vendor-bin/ns1/composer.json b/e2e/scenario8/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/e2e/scenario8/vendor-bin/ns1/composer.json @@ -0,0 +1 @@ +{} diff --git a/e2e/scenario9/.gitignore b/e2e/scenario9/.gitignore new file mode 100644 index 0000000..cac9c03 --- /dev/null +++ b/e2e/scenario9/.gitignore @@ -0,0 +1,5 @@ +/actual.txt +/composer.lock +/vendor/ +/vendor-bin/*/composer.lock +/vendor-bin/*/vendor/ diff --git a/e2e/scenario9/README.md b/e2e/scenario9/README.md new file mode 100644 index 0000000..80f120b --- /dev/null +++ b/e2e/scenario9/README.md @@ -0,0 +1 @@ +Tests that plugins installed in a namespace are loaded when a command is executed in the namespace. diff --git a/e2e/scenario9/composer.json b/e2e/scenario9/composer.json new file mode 100644 index 0000000..3061d0c --- /dev/null +++ b/e2e/scenario9/composer.json @@ -0,0 +1,21 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "forward-command": true + } + } +} diff --git a/e2e/scenario9/expected.txt b/e2e/scenario9/expected.txt new file mode 100644 index 0000000..0515c41 --- /dev/null +++ b/e2e/scenario9/expected.txt @@ -0,0 +1,9 @@ +[bamarni-bin] Checking namespace vendor-bin/composer-unused + +Loading packages +---------------- + + ! [NOTE] Found 0 package(s) to be checked. + + [OK] Done. No required packages to scan. + diff --git a/e2e/scenario9/script.sh b/e2e/scenario9/script.sh new file mode 100755 index 0000000..4decef4 --- /dev/null +++ b/e2e/scenario9/script.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env envariables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +composer update + +# Actual command to execute the test itself +composer bin composer-unused unused --no-progress 2>&1 | tee > actual.txt || true diff --git a/e2e/scenario9/vendor-bin/composer-unused/.gitignore b/e2e/scenario9/vendor-bin/composer-unused/.gitignore new file mode 100644 index 0000000..57494c2 --- /dev/null +++ b/e2e/scenario9/vendor-bin/composer-unused/.gitignore @@ -0,0 +1,2 @@ +/composer-unused-dump-* + diff --git a/e2e/scenario9/vendor-bin/composer-unused/composer.json b/e2e/scenario9/vendor-bin/composer-unused/composer.json new file mode 100644 index 0000000..3c5a312 --- /dev/null +++ b/e2e/scenario9/vendor-bin/composer-unused/composer.json @@ -0,0 +1,11 @@ +{ + "require": { + "composer-runtime-api": "^2.2", + "icanhazstring/composer-unused": "~0.7.12" + }, + "config": { + "allow-plugins": { + "icanhazstring/composer-unused": true + } + } +} diff --git a/infection.json b/infection.json new file mode 100644 index 0000000..4db6121 --- /dev/null +++ b/infection.json @@ -0,0 +1,14 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "dist/infection.txt" + }, + "mutators": { + "@default": true + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9a40036..6d57a8f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,7 @@ - - - ./tests/ + tests - \ No newline at end of file + + + src + + + + diff --git a/src/ApplicationFactory/FreshInstanceApplicationFactory.php b/src/ApplicationFactory/FreshInstanceApplicationFactory.php new file mode 100644 index 0000000..a4dcd58 --- /dev/null +++ b/src/ApplicationFactory/FreshInstanceApplicationFactory.php @@ -0,0 +1,15 @@ +composer = $composer; + $this->io = $io; + $this->logger = new Logger($io); + } + + public function getCapabilities(): array + { + return [ + ComposerPluginCommandProvider::class => BamarniCommandProvider::class, + ]; + } + + public function deactivate(Composer $composer, IOInterface $io): void + { + } + + public function uninstall(Composer $composer, IOInterface $io): void + { + } + + public static function getSubscribedEvents(): array + { + return [ + PluginEvents::COMMAND => 'onCommandEvent', + ScriptEvents::POST_AUTOLOAD_DUMP => 'onPostAutoloadDump', + ]; + } + + public function onPostAutoloadDump(Event $event): void + { + $this->logger->logDebug('Calling onPostAutoloadDump().'); + + $eventIO = $event->getIO(); + + if (!($eventIO instanceof ConsoleIO)) { + return; + } + + // This is a bit convoluted but Event does not expose the input unlike + // CommandEvent. + $publicIO = PublicIO::fromConsoleIO($eventIO); + $eventInput = $publicIO->getInput(); + + $this->onEvent( + $eventInput->getArgument('command'), + $eventInput, + $publicIO->getOutput() + ); + } + + public function onCommandEvent(CommandEvent $event): bool + { + $this->logger->logDebug('Calling onCommandEvent().'); + + return $this->onEvent( + $event->getCommandName(), + $event->getInput(), + $event->getOutput() + ); + } + + private function onEvent( + string $commandName, + InputInterface $input, + OutputInterface $output + ): bool { + $config = Config::fromComposer($this->composer); + + $deprecations = $config->getDeprecations(); + + if (count($deprecations) > 0) { + foreach ($deprecations as $deprecation) { + $this->logger->logStandard($deprecation); + } + } + + if ($config->isCommandForwarded() + && in_array($commandName, self::FORWARDED_COMMANDS, true) + ) { + return $this->onForwardedCommand($input, $output); + } + + return true; + } + + protected function onForwardedCommand( + InputInterface $input, + OutputInterface $output + ): bool { + if ($this->forwarded) { + $this->logger->logDebug('Command already forwarded within the process: skipping.'); + + return true; + } + + $this->forwarded = true; + + $this->logger->logStandard('The command is being forwarded.'); + $this->logger->logDebug( + sprintf( + 'Original input: %s.', + $input->__toString() + ) + ); + + // Note that the input & output of $io should be the same as the event + // input & output. + $io = $this->io; + + $application = new Application(); + + $command = new BinCommand(); + $command->setComposer($this->composer); + $command->setApplication($application); + $command->setIO($io); + + $forwardedCommandInput = BinInputFactory::createForwardedCommandInput($input); + + try { + return Command::SUCCESS === $command->run( + $forwardedCommandInput, + $output + ); + } catch (Throwable $throwable) { + return false; + } + } +} diff --git a/src/BinCommand.php b/src/BinCommand.php deleted file mode 100644 index e7fd540..0000000 --- a/src/BinCommand.php +++ /dev/null @@ -1,186 +0,0 @@ -setName('bin') - ->setDescription('Run a command inside a bin namespace') - ->setDefinition([ - new InputArgument('namespace', InputArgument::REQUIRED), - new InputArgument('args', InputArgument::REQUIRED | InputArgument::IS_ARRAY), - ]) - ->ignoreValidationErrors() - ; - } - - /** - * {@inheritDoc} - */ - public function execute(InputInterface $input, OutputInterface $output) - { - $config = new Config($this->getComposer()); - $this->resetComposers($application = $this->getApplication()); - /** @var ComposerApplication $application */ - - if ($config->binLinksAreEnabled()) { - putenv('COMPOSER_BIN_DIR='.$this->createConfig()->get('bin-dir')); - } - - $vendorRoot = $config->getTargetDirectory(); - $namespace = $input->getArgument('namespace'); - - $input = new StringInput(preg_replace( - sprintf('/bin\s+(--ansi\s)?%s(\s.+)/', preg_quote($namespace, '/')), - '$1$2', - (string) $input, - 1 - )); - - return ('all' !== $namespace) - ? $this->executeInNamespace($application, $vendorRoot.'/'.$namespace, $input, $output) - : $this->executeAllNamespaces($application, $vendorRoot, $input, $output) - ; - } - - /** - * @param ComposerApplication $application - * @param string $binVendorRoot - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int Exit code - */ - private function executeAllNamespaces(ComposerApplication $application, $binVendorRoot, InputInterface $input, OutputInterface $output) - { - $binRoots = glob($binVendorRoot.'/*', GLOB_ONLYDIR); - if (empty($binRoots)) { - $this->getIO()->writeError('Couldn\'t find any bin namespace.'); - - return 0; // Is a valid scenario: the user may not have setup any bin namespace yet - } - - $originalWorkingDir = getcwd(); - $exitCode = 0; - foreach ($binRoots as $namespace) { - $output->writeln( - sprintf('Run in namespace %s', $namespace), - OutputInterface::VERBOSITY_VERBOSE - ); - $exitCode += $this->executeInNamespace($application, $namespace, $input, $output); - - chdir($originalWorkingDir); - $this->resetComposers($application); - } - - return min($exitCode, 255); - } - - /** - * @param ComposerApplication $application - * @param string $namespace - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int Exit code - */ - private function executeInNamespace(ComposerApplication $application, $namespace, InputInterface $input, OutputInterface $output) - { - if (!file_exists($namespace)) { - mkdir($namespace, 0777, true); - } - - $this->chdir($namespace); - - // some plugins require access to composer file e.g. Symfony Flex - if (!file_exists(Factory::getComposerFile())) { - file_put_contents(Factory::getComposerFile(), '{}'); - } - - $input = new StringInput((string) $input . ' --working-dir=.'); - - $this->getIO()->writeError( - sprintf('Run with %s', $input->__toString()), - true, - IOInterface::VERBOSE - ); - - return $application->doRun($input, $output); - } - - /** - * {@inheritDoc} - */ - public function isProxyCommand() - { - return true; - } - - /** - * Resets all Composer references in the application. - * - * @param ComposerApplication $application - * @return void - */ - private function resetComposers(ComposerApplication $application) - { - $application->resetComposer(); - - foreach ($this->getApplication()->all() as $command) { - if ($command instanceof BaseCommand) { - $command->resetComposer(); - } - } - } - - /** - * @param $dir - * @return void - */ - private function chdir($dir) - { - chdir($dir); - - $this->getIO()->writeError( - sprintf('Changed current directory to %s', $dir), - true, - IOInterface::VERBOSE - ); - } - - /** - * @return \Composer\Config - * @throws \Composer\Json\JsonValidationException - * @throws \Seld\JsonLint\ParsingException - */ - private function createConfig() - { - $config = Factory::createConfig(); - - $file = new JsonFile(Factory::getComposerFile()); - if (!$file->exists()) { - return $config; - } - $file->validateSchema(JsonFile::LAX_SCHEMA); - - $config->merge($file->read()); - - return $config; - } -} diff --git a/src/Command/BinCommand.php b/src/Command/BinCommand.php new file mode 100644 index 0000000..348a04a --- /dev/null +++ b/src/Command/BinCommand.php @@ -0,0 +1,325 @@ +applicationFactory = $applicationFactory ?? new FreshInstanceApplicationFactory(); + $this->logger = $logger ?? new Logger(new NullIO()); + } + + protected function configure(): void + { + $this + ->setDescription('Run a command inside a bin namespace') + ->addArgument( + self::NAMESPACE_ARG, + InputArgument::REQUIRED + ) + ->ignoreValidationErrors(); + } + + public function setIO(IOInterface $io): void + { + parent::setIO($io); + + $this->logger = new Logger($io); + } + + public function getIO(): IOInterface + { + $io = parent::getIO(); + + $this->logger = new Logger($io); + + return $io; + } + + public function isProxyCommand(): bool + { + return true; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + // Switch to requireComposer() once Composer 2.3 is set as the minimum + $config = Config::fromComposer($this->getComposer()); + $currentWorkingDir = getcwd(); + + $this->logger->logDebug( + sprintf( + 'Current working directory: %s', + $currentWorkingDir + ) + ); + + // Ensures Composer is reset – we are setting some environment variables + // & co. so a fresh Composer instance is required. + $this->resetComposers(); + + $this->configureBinLinksDir($config); + + $vendorRoot = $config->getTargetDirectory(); + $namespace = $input->getArgument(self::NAMESPACE_ARG); + + $binInput = BinInputFactory::createInput( + $namespace, + $input + ); + + return (self::ALL_NAMESPACES !== $namespace) + ? $this->executeInNamespace( + $currentWorkingDir, + $vendorRoot.'/'.$namespace, + $binInput, + $output + ) + : $this->executeAllNamespaces( + $currentWorkingDir, + $vendorRoot, + $binInput, + $output + ); + } + + /** + * @return list + */ + private static function getBinNamespaces(string $binVendorRoot): array + { + return glob($binVendorRoot.'/*', GLOB_ONLYDIR); + } + + private function executeAllNamespaces( + string $originalWorkingDir, + string $binVendorRoot, + InputInterface $input, + OutputInterface $output + ): int { + $namespaces = self::getBinNamespaces($binVendorRoot); + + if (count($namespaces) === 0) { + $this->logger->logStandard('Could not find any bin namespace.'); + + // Is a valid scenario: the user may not have set up any bin + // namespace yet + return self::SUCCESS; + } + + $exitCode = self::SUCCESS; + + foreach ($namespaces as $namespace) { + $exitCode += $this->executeInNamespace( + $originalWorkingDir, + $namespace, + $input, + $output + ); + } + + return min($exitCode, self::FAILURE); + } + + private function executeInNamespace( + string $originalWorkingDir, + string $namespace, + InputInterface $input, + OutputInterface $output + ): int { + $this->logger->logStandard( + sprintf( + 'Checking namespace %s', + $namespace + ) + ); + + try { + self::createNamespaceDirIfDoesNotExist($namespace); + } catch (CouldNotCreateNamespaceDir $exception) { + $this->logger->logStandard( + sprintf( + '%s', + $exception->getMessage() + ) + ); + + return self::FAILURE; + } + + // Use a new application: this avoids a variety of issues: + // - A command may be added in a namespace which may cause side effects + // when executed in another namespace afterwards (since it is the same + // process). + // - Different plugins may be registered in the namespace in which case + // an already executed application will not pick that up. + $namespaceApplication = $this->applicationFactory->create( + $this->getApplication() + ); + + // It is important to clean up the state either for follow-up plugins + // or for example the execution in the next namespace. + $cleanUp = function () use ($originalWorkingDir): void { + $this->chdir($originalWorkingDir); + $this->resetComposers(); + }; + + $this->chdir($namespace); + + $this->ensureComposerFileExists(); + + $namespaceInput = BinInputFactory::createNamespaceInput($input); + + $this->logger->logDebug( + sprintf( + 'Running `@composer %s`.', + $namespaceInput->__toString() + ) + ); + + try { + $exitCode = $namespaceApplication->doRun($namespaceInput, $output); + } catch (Throwable $executionFailed) { + // Ensure we do the cleanup even in case of failure + $cleanUp(); + + throw $executionFailed; + } + + $cleanUp(); + + return $exitCode; + } + + /** + * @throws CouldNotCreateNamespaceDir + */ + private static function createNamespaceDirIfDoesNotExist(string $namespace): void + { + if (file_exists($namespace)) { + return; + } + + $mkdirResult = mkdir($namespace, 0777, true); + + if (!$mkdirResult && !is_dir($namespace)) { + throw CouldNotCreateNamespaceDir::forNamespace($namespace); + } + } + + private function configureBinLinksDir(Config $config): void + { + if (!$config->binLinksAreEnabled()) { + return; + } + + $binDir = ConfigFactory::createConfig()->get('bin-dir'); + + putenv( + sprintf( + 'COMPOSER_BIN_DIR=%s', + $binDir + ) + ); + + $this->logger->logDebug( + sprintf( + 'Configuring bin directory to %s.', + $binDir + ) + ); + } + + private function ensureComposerFileExists(): void + { + // Some plugins require access to the Composer file e.g. Symfony Flex + $namespaceComposerFile = Factory::getComposerFile(); + + if (file_exists($namespaceComposerFile)) { + return; + } + + file_put_contents($namespaceComposerFile, '{}'); + + $this->logger->logDebug( + sprintf( + 'Created the file %s.', + $namespaceComposerFile + ) + ); + } + + private function resetComposers(): void + { + $this->getApplication()->resetComposer(); + + foreach ($this->getApplication()->all() as $command) { + if ($command instanceof BaseCommand) { + $command->resetComposer(); + } + } + } + + private function chdir(string $dir): void + { + chdir($dir); + + $this->logger->logDebug( + sprintf( + 'Changed current directory to %s.', + $dir + ) + ); + } +} diff --git a/src/Command/CouldNotCreateNamespaceDir.php b/src/Command/CouldNotCreateNamespaceDir.php new file mode 100644 index 0000000..3314507 --- /dev/null +++ b/src/Command/CouldNotCreateNamespaceDir.php @@ -0,0 +1,21 @@ +getPackage()->getExtra(); - $this->config = array_merge( - [ - 'bin-links' => true, - 'target-directory' => 'vendor-bin', - 'forward-command' => false, - ], - isset($extra['bamarni-bin']) ? $extra['bamarni-bin'] : [] - ); - } - - /** - * @return bool - */ - public function binLinksAreEnabled() - { - return true === $this->config['bin-links']; - } - - /** - * @return string - */ - public function getTargetDirectory() - { - return $this->config['target-directory']; - } - - /** - * @return bool - */ - public function isCommandForwarded() - { - return $this->config['forward-command']; - } -} diff --git a/src/Config/Config.php b/src/Config/Config.php new file mode 100644 index 0000000..bf672cf --- /dev/null +++ b/src/Config/Config.php @@ -0,0 +1,156 @@ + true, + self::TARGET_DIRECTORY => 'vendor-bin', + self::FORWARD_COMMAND => false, + ]; + + /** + * @var bool + */ + private $binLinks; + + /** + * @var string + */ + private $targetDirectory; + + /** + * @var bool + */ + private $forwardCommand; + + /** + * @var list + */ + private $deprecations = []; + + /** + * @throws InvalidBamarniComposerExtraConfig + */ + public static function fromComposer(Composer $composer): self + { + return new self($composer->getPackage()->getExtra()); + } + + /** + * @param mixed[] $extra + * + * @throws InvalidBamarniComposerExtraConfig + */ + public function __construct(array $extra) + { + $userExtra = $extra[self::EXTRA_CONFIG_KEY] ?? []; + + $config = array_merge(self::DEFAULT_CONFIG, $userExtra); + + $getType = function_exists('get_debug_type') ? 'get_debug_type' : 'gettype'; + + $binLinks = $config[self::BIN_LINKS_ENABLED]; + + if (!is_bool($binLinks)) { + throw new InvalidBamarniComposerExtraConfig( + sprintf( + 'Expected setting "%s.%s" to be a boolean value. Got "%s".', + self::EXTRA_CONFIG_KEY, + self::BIN_LINKS_ENABLED, + $getType($binLinks) + ) + ); + } + + $binLinksSetExplicitly = array_key_exists(self::BIN_LINKS_ENABLED, $userExtra); + + if ($binLinks && !$binLinksSetExplicitly) { + $this->deprecations[] = sprintf( + 'The setting "%s.%s" will be set to "false" from 2.x onwards. If you wish to keep it to "true", you need to set it explicitly.', + self::EXTRA_CONFIG_KEY, + self::BIN_LINKS_ENABLED + ); + } + + $targetDirectory = $config[self::TARGET_DIRECTORY]; + + if (!is_string($targetDirectory)) { + throw new InvalidBamarniComposerExtraConfig( + sprintf( + 'Expected setting "%s.%s" to be a string. Got "%s".', + self::EXTRA_CONFIG_KEY, + self::TARGET_DIRECTORY, + $getType($targetDirectory) + ) + ); + } + + $forwardCommand = $config[self::FORWARD_COMMAND]; + + if (!is_bool($forwardCommand)) { + throw new InvalidBamarniComposerExtraConfig( + sprintf( + 'Expected setting "%s.%s" to be a boolean value. Got "%s".', + self::EXTRA_CONFIG_KEY, + self::FORWARD_COMMAND, + gettype($forwardCommand) + ) + ); + } + + $forwardCommandSetExplicitly = array_key_exists(self::FORWARD_COMMAND, $userExtra); + + if (!$forwardCommand && !$forwardCommandSetExplicitly) { + $this->deprecations[] = sprintf( + 'The setting "%s.%s" will be set to "true" from 2.x onwards. If you wish to keep it to "false", you need to set it explicitly.', + self::EXTRA_CONFIG_KEY, + self::FORWARD_COMMAND + ); + } + + $this->binLinks = $binLinks; + $this->targetDirectory = $targetDirectory; + $this->forwardCommand = $forwardCommand; + } + + public function binLinksAreEnabled(): bool + { + return $this->binLinks; + } + + public function getTargetDirectory(): string + { + return $this->targetDirectory; + } + + public function isCommandForwarded(): bool + { + return $this->forwardCommand; + } + + /** + * @return list + */ + public function getDeprecations(): array + { + return $this->deprecations; + } +} diff --git a/src/Config/ConfigFactory.php b/src/Config/ConfigFactory.php new file mode 100644 index 0000000..b2fc1d4 --- /dev/null +++ b/src/Config/ConfigFactory.php @@ -0,0 +1,39 @@ +exists()) { + return $config; + } + + $file->validateSchema(JsonFile::LAX_SCHEMA); + + $config->merge($file->read()); + + return $config; + } + + private function __construct() + { + } +} diff --git a/src/Config/InvalidBamarniComposerExtraConfig.php b/src/Config/InvalidBamarniComposerExtraConfig.php new file mode 100644 index 0000000..105f9ea --- /dev/null +++ b/src/Config/InvalidBamarniComposerExtraConfig.php @@ -0,0 +1,11 @@ + `update --prefer-lowest` + * + * Note that no input definition is bound in the resulting input. + */ + public static function createInput( + string $namespace, + InputInterface $previousInput + ): InputInterface { + $matchResult = preg_match( + sprintf( + '/^(?.+)?bin (?:(?.+?) )?(?:%1$s|\'%1$s\') (?.+?)(? -- .*)?$/', + preg_quote($namespace, '/') + ), + $previousInput->__toString(), + $matches + ); + + if (1 !== $matchResult) { + throw InvalidBinInput::forBinInput($previousInput); + } + + $inputParts = array_filter( + array_map( + 'trim', + [ + $matches['binCommand'], + $matches['preBinOptions2'] ?? '', + $matches['preBinOptions'] ?? '', + $matches['extraInput'] ?? '', + ] + ) + ); + + // Move the options present _before_ bin namespaceName to after, but + // before the end of option marker (--) if present. + $reorderedInput = implode(' ', $inputParts); + + return new StringInput($reorderedInput); + } + + public static function createNamespaceInput(InputInterface $previousInput): InputInterface + { + $matchResult = preg_match( + '/^(.+?\s?)(--(?: .+)?)?$/', + $previousInput->__toString(), + $matches + ); + + if (1 !== $matchResult) { + throw InvalidBinInput::forNamespaceInput($previousInput); + } + + $inputParts = array_filter( + array_map( + 'trim', + [ + $matches[1], + '--working-dir=.', + $matches[2] ?? '', + ] + ) + ); + + $newInput = implode(' ', $inputParts); + + return new StringInput($newInput); + } + + public static function createForwardedCommandInput(InputInterface $input): InputInterface + { + return new StringInput( + sprintf( + 'bin all %s', + $input->__toString() + ) + ); + } + + private function __construct() + { + } +} diff --git a/src/Input/InvalidBinInput.php b/src/Input/InvalidBinInput.php new file mode 100644 index 0000000..2418d9c --- /dev/null +++ b/src/Input/InvalidBinInput.php @@ -0,0 +1,32 @@ + ", for example "bin all update --prefer-lowest".', + $input->__toString() + ) + ); + } + + public static function forNamespaceInput(InputInterface $input): self + { + return new self( + sprintf( + 'Could not parse the input (executed within the namespace) "%s".', + $input->__toString() + ) + ); + } +} diff --git a/src/Logger.php b/src/Logger.php new file mode 100644 index 0000000..1905719 --- /dev/null +++ b/src/Logger.php @@ -0,0 +1,39 @@ +io = $io; + } + + public function logStandard(string $message): void + { + $this->log($message, false); + } + + public function logDebug(string $message): void + { + $this->log($message, true); + } + + private function log(string $message, bool $debug): void + { + $verbosity = $debug + ? IOInterface::VERBOSE + : IOInterface::NORMAL; + + $this->io->writeError('[bamarni-bin] '.$message, true, $verbosity); + } +} diff --git a/src/Plugin.php b/src/Plugin.php deleted file mode 100644 index 06ea90e..0000000 --- a/src/Plugin.php +++ /dev/null @@ -1,130 +0,0 @@ -composer = $composer; - $this->io = $io; - } - - /** - * @return string[] - */ - public function getCapabilities() - { - return [ - 'Composer\Plugin\Capability\CommandProvider' => 'Bamarni\Composer\Bin\CommandProvider', - ]; - } - - /** - * @return void - */ - public function deactivate(Composer $composer, IOInterface $io) - { - } - - /** - * @return void - */ - public function uninstall(Composer $composer, IOInterface $io) - { - } - - /** - * @return string[] - */ - public static function getSubscribedEvents() - { - return [ - PluginEvents::COMMAND => 'onCommandEvent', - ]; - } - - /** - * @param CommandEvent $event - * @return bool - */ - public function onCommandEvent(CommandEvent $event) - { - $config = new Config($this->composer); - - if ($config->isCommandForwarded()) { - switch ($event->getCommandName()) { - case 'update': - case 'install': - return $this->onCommandEventInstallUpdate($event); - } - } - - return true; - } - - /** - * @param CommandEvent $event - * @return bool - */ - protected function onCommandEventInstallUpdate(CommandEvent $event) - { - $command = new BinCommand(); - $command->setComposer($this->composer); - $command->setApplication(new Application()); - - $arguments = [ - 'command' => $command->getName(), - 'namespace' => 'all', - 'args' => [], - ]; - - foreach (array_filter($event->getInput()->getArguments()) as $argument) { - $arguments['args'][] = $argument; - } - - foreach (array_keys(array_filter($event->getInput()->getOptions())) as $option) { - $arguments['args'][] = '--' . $option; - } - - $definition = new InputDefinition(); - $definition->addArgument(new InputArgument('command', InputArgument::REQUIRED)); - $definition->addArguments($command->getDefinition()->getArguments()); - $definition->addOptions($command->getDefinition()->getOptions()); - - $input = new ArrayInput($arguments, $definition); - - try { - $returnCode = $command->run($input, $event->getOutput()); - } catch (\Exception $e) { - return false; - } - - return $returnCode === 0; - } -} diff --git a/src/PublicIO.php b/src/PublicIO.php new file mode 100644 index 0000000..6c90d84 --- /dev/null +++ b/src/PublicIO.php @@ -0,0 +1,31 @@ +input, + $io->output, + $io->helperSet + ); + } + + public function getInput(): InputInterface + { + return $this->input; + } + + public function getOutput(): OutputInterface + { + return $this->output; + } +} diff --git a/tests/BinCommandTest.php b/tests/BinCommandTest.php deleted file mode 100644 index 7e1320f..0000000 --- a/tests/BinCommandTest.php +++ /dev/null @@ -1,110 +0,0 @@ -rootDir = sys_get_temp_dir().'/'.uniqid('composer_bin_plugin_tests_'); - mkdir($this->rootDir); - chdir($this->rootDir); - - file_put_contents($this->rootDir.'/composer.json', '{}'); - - $this->application = new Application(); - $this->application->addCommands([ - new BinCommand(), - $this->myTestCommand = new MyTestCommand($this), - ]); - } - - public function tearDown() - { - putenv('COMPOSER_BIN_DIR'); - $this->myTestCommand->data = []; - } - - /** - * @dataProvider namespaceProvider - */ - public function testNamespaceCommand($input) - { - $input = new StringInput($input); - $output = new NullOutput(); - $this->application->doRun($input, $output); - - $this->assertCount(1, $this->myTestCommand->data); - $dataSet = array_shift($this->myTestCommand->data); - $this->assertEquals($dataSet['bin-dir'], $this->rootDir.'/vendor/bin'); - $this->assertEquals($dataSet['cwd'], $this->rootDir.'/vendor-bin/mynamespace'); - $this->assertEquals($dataSet['vendor-dir'], $this->rootDir.'/vendor-bin/mynamespace/vendor'); - } - - public static function namespaceProvider() - { - return [ - ['bin mynamespace mytest'], - ['bin mynamespace mytest --myoption'], - ]; - } - - public function testAllNamespaceWithoutAnyNamespace() - { - $input = new StringInput('bin all mytest'); - $output = new NullOutput(); - $this->application->doRun($input, $output); - - $this->assertEmpty($this->myTestCommand->data); - } - - public function testAllNamespaceCommand() - { - $namespaces = ['mynamespace', 'yournamespace']; - foreach ($namespaces as $ns) { - mkdir($this->rootDir.'/vendor-bin/'.$ns, 0777, true); - } - - $input = new StringInput('bin all mytest'); - $output = new NullOutput(); - $this->application->doRun($input, $output); - - $this->assertCount(count($namespaces), $this->myTestCommand->data); - - foreach ($namespaces as $ns) { - $dataSet = array_shift($this->myTestCommand->data); - $this->assertEquals($dataSet['bin-dir'], $this->rootDir . '/vendor/bin'); - $this->assertEquals($dataSet['cwd'], $this->rootDir . '/vendor-bin/'.$ns); - $this->assertEquals($dataSet['vendor-dir'], $this->rootDir . '/vendor-bin/'.$ns.'/vendor'); - } - } - - public function testBinDirFromLocalConfig() - { - $binDir = 'bin'; - $composer = [ - 'config' => [ - 'bin-dir' => $binDir - ] - ]; - file_put_contents($this->rootDir.'/composer.json', json_encode($composer)); - - $input = new StringInput('bin theirspace mytest'); - $output = new NullOutput(); - $this->application->doRun($input, $output); - - $this->assertCount(1, $this->myTestCommand->data); - $dataSet = array_shift($this->myTestCommand->data); - $this->assertEquals($dataSet['bin-dir'], $this->rootDir.'/'.$binDir); - } -} diff --git a/tests/Command/BinCommandTest.php b/tests/Command/BinCommandTest.php new file mode 100644 index 0000000..ad83072 --- /dev/null +++ b/tests/Command/BinCommandTest.php @@ -0,0 +1,237 @@ +previousCwd = getcwd(); + + $tmpDir = sys_get_temp_dir().'/composer_bin_plugin_tests'; + + if (!file_exists($tmpDir)) { + mkdir($tmpDir); + } + + chdir($tmpDir); + // On OSX sys_get_temp_dir() may return a symlink + $tmpDirRealPath = realpath($tmpDir); + self::assertNotFalse($tmpDirRealPath); + $this->tmpDir = $tmpDirRealPath; + + file_put_contents( + $this->tmpDir.'/composer.json', + '{}' + ); + + $this->testCommand = new MyTestCommand(); + + $this->application = new Application(); + $this->application->addCommands([ + new BinCommand(new ReuseApplicationFactory()), + $this->testCommand, + ]); + } + + public function tearDown(): void + { + putenv('COMPOSER_BIN_DIR'); + + chdir($this->previousCwd); + exec('rm -rf ' . $this->tmpDir); + + unset($this->application); + unset($this->testCommand); + unset($this->previousCwd); + unset($this->tmpDir); + } + + /** + * @dataProvider namespaceProvider + */ + public function test_it_can_execute_the_bin_command( + string $input, + string $expectedRelativeBinDir, + string $expectedRelativeCwd, + string $expectedRelativeVendorDir + ): void { + $input = new StringInput($input); + $output = new NullOutput(); + + $this->application->doRun($input, $output); + + $this->assertHasAccessToComposer(); + $this->assertDataSetRecordedIs( + $this->tmpDir.'/'.$expectedRelativeBinDir, + $this->tmpDir.'/'.$expectedRelativeCwd, + $this->tmpDir.'/'.$expectedRelativeVendorDir + ); + $this->assertNoMoreDataFound(); + } + + public function test_the_all_namespace_can_be_called(): void + { + $input = new StringInput('bin all mytest'); + $output = new NullOutput(); + + $this->application->doRun($input, $output); + + $this->assertNoMoreDataFound(); + } + + public function test_a_command_can_be_executed_in_each_namespace_via_the_all_namespace(): void + { + $namespaces = ['namespace1', 'namespace2']; + + foreach ($namespaces as $namespace) { + mkdir( + $this->tmpDir.'/vendor-bin/'.$namespace, + 0777, + true + ); + } + + $input = new StringInput('bin all mytest'); + $output = new NullOutput(); + + $this->application->doRun($input, $output); + + $this->assertHasAccessToComposer(); + + foreach ($namespaces as $namespace) { + $this->assertDataSetRecordedIs( + $this->tmpDir . '/vendor/bin', + $this->tmpDir . '/vendor-bin/'.$namespace, + $this->tmpDir . '/vendor-bin/'.$namespace.'/vendor' + ); + } + + $this->assertNoMoreDataFound(); + } + + public function test_the_bin_dir_can_be_changed(): void + { + $binDir = 'bin'; + $composer = [ + 'config' => [ + 'bin-dir' => $binDir + ] + ]; + + file_put_contents( + $this->tmpDir.'/composer.json', + json_encode($composer) + ); + + $input = new StringInput('bin theirspace mytest'); + $output = new NullOutput(); + + $this->application->doRun($input, $output); + + $this->assertHasAccessToComposer(); + $this->assertDataSetRecordedIs( + $this->tmpDir.'/'.$binDir, + $this->tmpDir.'/'.'vendor-bin/theirspace', + $this->tmpDir.'/'.'vendor-bin/theirspace/vendor' + ); + $this->assertNoMoreDataFound(); + } + + public static function namespaceProvider(): iterable + { + yield 'execute command from namespace' => [ + 'bin testnamespace mytest', + 'vendor/bin', + 'vendor-bin/testnamespace', + 'vendor-bin/testnamespace/vendor', + ]; + + yield 'execute command with options from namespace' => [ + 'bin testnamespace mytest --myoption', + 'vendor/bin', + 'vendor-bin/testnamespace', + 'vendor-bin/testnamespace/vendor', + ]; + } + + private function assertHasAccessToComposer(): void + { + self::assertInstanceOf( + Composer::class, + $this->testCommand->composer, + 'Some plugins may require access to composer file e.g. Symfony Flex' + ); + } + + private function assertDataSetRecordedIs( + string $expectedBinDir, + string $expectedCwd, + string $expectedVendorDir + ): void { + $data = array_shift($this->testCommand->data); + + self::assertNotNull( + $data, + 'Expected test command to contain at least one data entry' + ); + self::assertSame($expectedBinDir, $data['bin-dir']); + self::assertSame($expectedCwd, $data['cwd']); + self::assertSame($expectedVendorDir, $data['vendor-dir']); + } + + private function assertNoMoreDataFound(): void + { + $data = array_shift($this->testCommand->data); + + self::assertNull( + $data, + 'Expected test command to contain not contain any more data entries.' + ); + } +} diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php new file mode 100644 index 0000000..c96d0f5 --- /dev/null +++ b/tests/Config/ConfigTest.php @@ -0,0 +1,133 @@ + $expectedDeprecations + */ + public function test_it_can_be_instantiated( + array $extra, + bool $expectedBinLinksEnabled, + string $expectedTargetDirectory, + bool $expectedForwardCommand, + array $expectedDeprecations + ): void { + $config = new Config($extra); + + self::assertSame($expectedBinLinksEnabled, $config->binLinksAreEnabled()); + self::assertSame($expectedTargetDirectory, $config->getTargetDirectory()); + self::assertSame($expectedForwardCommand, $config->isCommandForwarded()); + self::assertSame($expectedDeprecations, $config->getDeprecations()); + } + + public static function provideExtraConfig(): iterable + { + $binLinksEnabledDeprecationMessage = 'The setting "bamarni-bin.bin-links" will be set to "false" from 2.x onwards. If you wish to keep it to "true", you need to set it explicitly.'; + $forwardCommandDeprecationMessage = 'The setting "bamarni-bin.forward-command" will be set to "true" from 2.x onwards. If you wish to keep it to "false", you need to set it explicitly.'; + + yield 'default values' => [ + [], + true, + 'vendor-bin', + false, + [ + $binLinksEnabledDeprecationMessage, + $forwardCommandDeprecationMessage, + ], + ]; + + yield 'unknown extra entry' => [ + ['unknown' => 'foo'], + true, + 'vendor-bin', + false, + [ + $binLinksEnabledDeprecationMessage, + $forwardCommandDeprecationMessage, + ], + ]; + + yield 'same as default but explicit' => [ + [ + Config::EXTRA_CONFIG_KEY => [ + Config::BIN_LINKS_ENABLED => true, + Config::FORWARD_COMMAND => false, + ], + ], + true, + 'vendor-bin', + false, + [], + ]; + + yield 'nominal' => [ + [ + Config::EXTRA_CONFIG_KEY => [ + Config::BIN_LINKS_ENABLED => false, + Config::TARGET_DIRECTORY => 'tools', + Config::FORWARD_COMMAND => true, + ], + ], + false, + 'tools', + true, + [], + ]; + } + + /** + * @dataProvider provideInvalidExtraConfig + */ + public function test_it_cannot_be_instantiated_with_invalid_config( + array $extra, + string $expectedMessage + ): void { + $this->expectException(InvalidBamarniComposerExtraConfig::class); + $this->expectExceptionMessage($expectedMessage); + + new Config($extra); + } + + public static function provideInvalidExtraConfig(): iterable + { + yield 'non bool bin links' => [ + [ + Config::EXTRA_CONFIG_KEY => [ + Config::BIN_LINKS_ENABLED => 'foo', + ], + ], + 'Expected setting "bamarni-bin.bin-links" to be a boolean value. Got "string".', + ]; + + yield 'non string target directory' => [ + [ + Config::EXTRA_CONFIG_KEY => [ + Config::TARGET_DIRECTORY => false, + ], + ], + 'Expected setting "bamarni-bin.target-directory" to be a string. Got "bool".', + ]; + + yield 'non bool forward command' => [ + [ + Config::EXTRA_CONFIG_KEY => [ + Config::FORWARD_COMMAND => 'foo', + ], + ], + 'Expected setting "bamarni-bin.forward-command" to be a boolean value. Got "string".', + ]; + } +} diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php new file mode 100644 index 0000000..83ca8f0 --- /dev/null +++ b/tests/EndToEndTest.php @@ -0,0 +1,188 @@ +run(); + + $standardOutput = $scenarioProcess->getOutput(); + $errorOutput = $scenarioProcess->getErrorOutput(); + + $actualPath = $scenarioPath.'/actual.txt'; + + if (file_exists($actualPath)) { + $originalContent = file_get_contents($scenarioPath.'/actual.txt'); + } else { + $originalContent = 'File was not created.'; + } + + $errorMessage = <<isSuccessful(), + $errorMessage + ); + + $actual = self::retrieveActualOutput( + getcwd(), + $originalContent + ); + + self::assertSame($expected, $actual, $errorMessage); + } + + public static function scenarioProvider(): iterable + { + $scenarios = Finder::create() + ->files() + ->depth(1) + ->in(self::E2E_DIR) + ->name('README.md'); + + foreach ($scenarios as $scenario) { + $scenarioPath = dirname($scenario->getPathname()); + $scenarioName = basename($scenarioPath); + $description = trim($scenario->getContents()); + + $title = sprintf( + '[%s] %s', + $scenarioName, + $description + ); + + yield $title => [ + realpath($scenarioPath), + ]; + } + } + + private static function retrieveActualOutput( + string $cwd, + string $originalContent + ): string { + $normalizedContent = str_replace( + $cwd, + '/path/to/project', + $originalContent + ); + + // Sometimes in the CI a different log is shown, e.g. in https://github.com/bamarni/composer-bin-plugin/runs/7246889244 + $normalizedContent = preg_replace( + '/> command: .+\n/', + '', + $normalizedContent + ); + + // Those values come from the expected.txt, it actually does matter how + // many they are at instant t. + $normalizedContent = preg_replace( + '/Analyzed (\d+) packages to resolve dependencies/', + 'Analyzed 90 packages to resolve dependencies', + $normalizedContent + ); + $normalizedContent = preg_replace( + '/Analyzed (\d+) rules to resolve dependencies/', + 'Analyzed 90 rules to resolve dependencies', + $normalizedContent + ); + + // We are not interested in the exact version installed especially since + // in a PR the version will be `dev-commithash` instead of `dev-master`. + $normalizedContent = preg_replace( + '/Installs: bamarni\/composer-bin-plugin:dev-.+/', + 'Installs: bamarni/composer-bin-plugin:dev-hash', + $normalizedContent + ); + $normalizedContent = preg_replace( + '/Locking bamarni\/composer-bin-plugin \(dev-.+\)/', + 'Locking bamarni/composer-bin-plugin (dev-hash)', + $normalizedContent + ); + $normalizedContent = preg_replace( + '/Installing bamarni\/composer-bin-plugin \(dev-.+\): Symlinking from \.\.\/\.\./', + 'Installing bamarni/composer-bin-plugin (dev-hash): Symlinking from ../..', + $normalizedContent + ); + + // We are not interested in the time taken which can vary from locally + // and on the CI. + // Also since the place at which this line may change depending on where + // it is run (i.e. is not deterministic), we simply remove it. + $normalizedContent = preg_replace( + '/Dependency resolution completed in \d\.\d{3} seconds\s/', + '', + $normalizedContent + ); + + // Normalize the find directory: on some versions of OSX it does not come + // with ticks but it does on Linux (at least Ubuntu). + $normalizedContent = preg_replace( + '/find: ‘?(.+?)’?: No such file or directory/u', + 'find: $1: No such file or directory', + $normalizedContent + ); + + return self::normalizeTrailingWhitespacesAndLineReturns($normalizedContent); + } + + private static function normalizeTrailingWhitespacesAndLineReturns(string $value): string + { + return implode( + "\n", + array_map('rtrim', explode(PHP_EOL, $value)) + ); + } +} diff --git a/tests/Fixtures/MyTestCommand.php b/tests/Fixtures/MyTestCommand.php index 9c9691f..8999511 100644 --- a/tests/Fixtures/MyTestCommand.php +++ b/tests/Fixtures/MyTestCommand.php @@ -1,7 +1,10 @@ + */ public $data = []; - private $assert; - - public function __construct(\PHPUnit_Framework_Assert $assert) + public function __construct() { - $this->assert = $assert; - parent::__construct('mytest'); + $this->setDefinition([ - new InputOption('myoption', null, InputOption::VALUE_NONE), + new InputOption( + 'myoption', + null, + InputOption::VALUE_NONE + ), ]); } - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { - $this->assert->assertInstanceOf( - '\Composer\Composer', - $this->getComposer(), - "Some plugins may require access to composer file e.g. Symfony Flex" - ); + $this->composer = $this->tryComposer(); $factory = Factory::create(new NullIO()); $config = $factory->getConfig(); @@ -44,6 +52,7 @@ public function execute(InputInterface $input, OutputInterface $output) $this->resetComposer(); $this->getApplication()->resetComposer(); + + return self::SUCCESS; } } - diff --git a/tests/Fixtures/ReuseApplicationFactory.php b/tests/Fixtures/ReuseApplicationFactory.php new file mode 100644 index 0000000..98cf03d --- /dev/null +++ b/tests/Fixtures/ReuseApplicationFactory.php @@ -0,0 +1,16 @@ + [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show', + $namespaceName + ) + ), + new StringInput('show'), + ]; + + yield $labelPrefix.' namespaced command' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s check:platform', + $namespaceName + ) + ), + new StringInput('check:platform'), + ]; + + yield $labelPrefix.'command with options' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i', + $namespaceName + ) + ), + new StringInput('show --tree -i'), + ]; + + yield $labelPrefix.'command with annoyingly placed options' => [ + $namespaceName, + new StringInput( + sprintf( + '--ansi bin %s -o --quiet show --tree -i', + $namespaceName + ) + ), + new StringInput('-o --quiet show --tree -i --ansi'), + ]; + + yield $labelPrefix.'command with options with option separator' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i --', + $namespaceName + ) + ), + new StringInput('show --tree -i --'), + ]; + + yield $labelPrefix.'command with options with option separator and follow up argument' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i -- foo', + $namespaceName + ) + ), + new StringInput('show --tree -i -- foo'), + ]; + + yield $labelPrefix.'command with options with option separator and follow up option' => [ + $namespaceName, + new StringInput( + sprintf( + 'bin %s show --tree -i -- --foo', + $namespaceName + ) + ), + new StringInput('show --tree -i -- --foo'), + ]; + + yield $labelPrefix.'command with annoyingly placed options and option separator and follow up option' => [ + $namespaceName, + new StringInput( + sprintf( + '--ansi bin %s -o --quiet show --tree -i -- --foo', + $namespaceName + ) + ), + new StringInput('-o --quiet show --tree -i --ansi -- --foo'), + ]; + } + + // See https://github.com/bamarni/composer-bin-plugin/pull/23 + yield [ + 'foo-namespace', + new StringInput('bin --ansi foo-namespace flex:update --prefer-lowest'), + new StringInput('flex:update --prefer-lowest --ansi'), + ]; + } + + /** + * @dataProvider namespaceInputProvider + */ + public function test_it_can_create_a_new_input_for_a_namespace( + InputInterface $previousInput, + InputInterface $expected + ): void { + $actual = BinInputFactory::createNamespaceInput($previousInput); + + self::assertEquals($expected, $actual); + } + + public static function namespaceInputProvider(): iterable + { + $namespaceNames = [ + 'simpleNamespaceName', + 'composed-namespaceName', + 'regexLimiter/namespaceName', + 'all', + ]; + + yield 'simple command' => [ + new StringInput('flex:update'), + new StringInput('flex:update --working-dir=.'), + ]; + + yield 'command with options' => [ + new StringInput('flex:update --prefer-lowest -i'), + new StringInput('flex:update --prefer-lowest -i --working-dir=.'), + ]; + + yield 'command with annoyingly placed options' => [ + new StringInput('-o --quiet flex:update --prefer-lowest -i'), + new StringInput('-o --quiet flex:update --prefer-lowest -i --working-dir=.'), + ]; + + yield 'command with options with option separator' => [ + new StringInput('flex:update --prefer-lowest -i --'), + new StringInput('flex:update --prefer-lowest -i --working-dir=. --'), + ]; + + yield 'command with options with option separator and follow up argument' => [ + new StringInput('flex:update --prefer-lowest -i -- foo'), + new StringInput('flex:update --prefer-lowest -i --working-dir=. -- foo'), + ]; + + yield 'command with annoyingly placed options and option separator and follow up option' => [ + new StringInput('-o --quiet flex:update --prefer-lowest -i -- --foo'), + new StringInput('-o --quiet flex:update --prefer-lowest -i --working-dir=. -- --foo'), + ]; + } + + /** + * @dataProvider forwardedCommandInputProvider + */ + public function test_it_can_create_a_new_input_for_forwarded_command( + InputInterface $previousInput, + InputInterface $expected + ): void { + $actual = BinInputFactory::createForwardedCommandInput($previousInput); + + self::assertEquals($expected, $actual); + } + + public static function forwardedCommandInputProvider(): iterable + { + yield [ + new StringInput('install --verbose'), + new StringInput('bin all install --verbose'), + ]; + + yield [ + new StringInput('flex:update --prefer-lowest --ansi'), + new StringInput('bin all flex:update --prefer-lowest --ansi'), + ]; + } +} diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php new file mode 100644 index 0000000..0f7a64f --- /dev/null +++ b/tests/LoggerTest.php @@ -0,0 +1,105 @@ +logStandard($message); + + self::assertSame($expected, $io->getOutput()); + } + + public static function standardMessageProvider(): iterable + { + $notLoggedVerbosities = [ + OutputInterface::VERBOSITY_QUIET, + ]; + + $loggedVerbosities = array_diff( + self::VERBOSITIES, + $notLoggedVerbosities + ); + + $message = 'Hello world!'; + $expected = '[bamarni-bin] Hello world!'.PHP_EOL; + + foreach ($notLoggedVerbosities as $verbosity) { + yield [$verbosity, $message, '']; + } + + foreach ($loggedVerbosities as $verbosity) { + yield [$verbosity, $message, $expected]; + } + } + + /** + * @dataProvider standardMessageProvider + */ + public function test_it_can_log_debug_messages( + int $verbosity, + string $message, + string $expected + ): void { + $io = new BufferIO('', $verbosity); + $logger = new Logger($io); + + $logger->logStandard($message); + + self::assertSame($expected, $io->getOutput()); + } + + public static function debugMessageProvider(): iterable + { + $notLoggedVerbosities = [ + OutputInterface::VERBOSITY_QUIET, + OutputInterface::VERBOSITY_NORMAL, + ]; + + $loggedVerbosities = array_diff( + self::VERBOSITIES, + $notLoggedVerbosities + ); + + $message = 'Hello world!'; + $expected = '[bamarni-bin] Hello world!'.PHP_EOL; + + foreach ($notLoggedVerbosities as $verbosity) { + yield [$verbosity, $message, '']; + } + + foreach ($loggedVerbosities as $verbosity) { + yield [$verbosity, $message, $expected]; + } + } +} diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json deleted file mode 100644 index 4920ed5..0000000 --- a/vendor-bin/phpunit/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require": { - "phpunit/phpunit": "^4.8||^5.0" - } -}