From 7d18e9c6a751f4c5a1fe29012d0103fcc226b5af Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Mon, 19 Aug 2024 15:08:25 +0200 Subject: [PATCH] Add SYSTOPIA extension template with QA tools and GitHub actions phpcs and phpstan GitHub actions are set to only run on workflow_dispatch due to too many errors. --- .editorconfig | 240 ++++++++++++++++++++++++++++++++ .github/workflows/phpcs.yml | 42 ++++++ .github/workflows/phpstan.yml | 52 +++++++ .github/workflows/phpunit.yml | 37 +++++ .gitignore | 13 +- ci/composer.json | 15 ++ composer.json | 53 +++++++ phpcs.xml.dist | 82 +++++++++++ phpstan.ci.neon | 13 ++ phpstan.neon.dist | 44 ++++++ phpstan.neon.template | 11 ++ phpstanBootstrap.php | 47 +++++++ phpunit.xml.dist | 34 ++++- tests/docker-compose.yml | 34 +++++ tests/docker-phpunit.sh | 19 +++ tests/docker-prepare.sh | 51 +++++++ tests/ignored-deprecations.json | 1 + tests/phpunit/bootstrap.php | 93 ++++++++++--- tools/phpcs/composer.json | 11 ++ tools/phpstan/composer.json | 18 +++ tools/phpunit/composer.json | 13 ++ 21 files changed, 898 insertions(+), 25 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/phpcs.yml create mode 100644 .github/workflows/phpstan.yml create mode 100644 .github/workflows/phpunit.yml create mode 100644 ci/composer.json create mode 100644 composer.json create mode 100644 phpcs.xml.dist create mode 100644 phpstan.ci.neon create mode 100644 phpstan.neon.dist create mode 100644 phpstan.neon.template create mode 100644 phpstanBootstrap.php create mode 100644 tests/docker-compose.yml create mode 100755 tests/docker-phpunit.sh create mode 100755 tests/docker-prepare.sh create mode 100644 tests/ignored-deprecations.json create mode 100644 tools/phpcs/composer.json create mode 100644 tools/phpstan/composer.json create mode 100644 tools/phpunit/composer.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..f5f70031 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,240 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80,120 + +[{*.php}] +ij_php_align_assignments = false +ij_php_align_class_constants = false +ij_php_align_enum_cases = false +ij_php_align_group_field_declarations = false +ij_php_align_inline_comments = false +ij_php_align_key_value_pairs = false +ij_php_align_match_arm_bodies = false +ij_php_align_multiline_array_initializer_expression = false +ij_php_align_multiline_binary_operation = false +ij_php_align_multiline_chained_methods = false +ij_php_align_multiline_extends_list = false +ij_php_align_multiline_for = true +ij_php_align_multiline_parameters = false +ij_php_align_multiline_parameters_in_calls = false +ij_php_align_multiline_ternary_operation = false +ij_php_align_named_arguments = false +ij_php_align_phpdoc_comments = false +ij_php_align_phpdoc_param_names = false +ij_php_anonymous_brace_style = end_of_line +ij_php_api_weight = 28 +ij_php_array_initializer_new_line_after_left_brace = true +ij_php_array_initializer_right_brace_on_new_line = true +ij_php_array_initializer_wrap = on_every_item +ij_php_assignment_wrap = normal +ij_php_attributes_wrap = normal +ij_php_author_weight = 28 +ij_php_binary_operation_sign_on_next_line = false +ij_php_binary_operation_wrap = normal +ij_php_blank_lines_after_class_header = 1 +ij_php_blank_lines_after_function = 1 +ij_php_blank_lines_after_imports = 1 +ij_php_blank_lines_after_opening_tag = 0 +ij_php_blank_lines_after_package = 1 +ij_php_blank_lines_around_class = 1 +ij_php_blank_lines_around_constants = 1 +ij_php_blank_lines_around_enum_cases = 0 +ij_php_blank_lines_around_field = 1 +ij_php_blank_lines_around_method = 1 +ij_php_blank_lines_before_class_end = 1 +ij_php_blank_lines_before_imports = 1 +ij_php_blank_lines_before_method_body = 0 +ij_php_blank_lines_before_package = 1 +ij_php_blank_lines_before_return_statement = 1 +ij_php_blank_lines_between_imports = 0 +ij_php_block_brace_style = end_of_line +ij_php_call_parameters_new_line_after_left_paren = true +ij_php_call_parameters_right_paren_on_new_line = true +ij_php_call_parameters_wrap = on_every_item +ij_php_catch_on_new_line = true +ij_php_category_weight = 28 +ij_php_class_brace_style = end_of_line +ij_php_comma_after_last_argument = false +ij_php_comma_after_last_array_element = true +ij_php_comma_after_last_closure_use_var = false +ij_php_comma_after_last_match_arm = false +ij_php_comma_after_last_parameter = false +ij_php_concat_spaces = true +ij_php_copyright_weight = 28 +ij_php_deprecated_weight = 4 +ij_php_do_while_brace_force = always +ij_php_else_if_style = as_is +ij_php_else_on_new_line = true +ij_php_example_weight = 28 +ij_php_extends_keyword_wrap = off +ij_php_extends_list_wrap = off +ij_php_fields_default_visibility = private +ij_php_filesource_weight = 28 +ij_php_finally_on_new_line = true +ij_php_for_brace_force = always +ij_php_for_statement_new_line_after_left_paren = false +ij_php_for_statement_right_paren_on_new_line = false +ij_php_for_statement_wrap = off +ij_php_force_empty_methods_in_one_line = false +ij_php_force_short_declaration_array_style = true +ij_php_getters_setters_naming_style = camel_case +ij_php_getters_setters_order_style = getters_first +ij_php_global_weight = 28 +ij_php_group_use_wrap = on_every_item +ij_php_if_brace_force = always +ij_php_if_lparen_on_next_line = false +ij_php_if_rparen_on_next_line = false +ij_php_ignore_weight = 28 +ij_php_import_sorting = alphabetic +ij_php_indent_break_from_case = true +ij_php_indent_case_from_switch = true +ij_php_indent_code_in_php_tags = false +ij_php_internal_weight = 28 +ij_php_keep_blank_lines_after_lbrace = 1 +ij_php_keep_blank_lines_before_right_brace = 1 +ij_php_keep_blank_lines_in_code = 1 +ij_php_keep_blank_lines_in_declarations = 1 +ij_php_keep_control_statement_in_one_line = false +ij_php_keep_first_column_comment = false +ij_php_keep_indents_on_empty_lines = false +ij_php_keep_line_breaks = false +ij_php_keep_rparen_and_lbrace_on_one_line = true +ij_php_keep_simple_classes_in_one_line = false +ij_php_keep_simple_methods_in_one_line = false +ij_php_lambda_brace_style = end_of_line +ij_php_license_weight = 28 +ij_php_line_comment_add_space = false +ij_php_line_comment_at_first_column = true +ij_php_link_weight = 28 +ij_php_lower_case_boolean_const = false +ij_php_lower_case_keywords = true +ij_php_lower_case_null_const = false +ij_php_method_brace_style = end_of_line +ij_php_method_call_chain_wrap = on_every_item +ij_php_method_parameters_new_line_after_left_paren = true +ij_php_method_parameters_right_paren_on_new_line = true +ij_php_method_parameters_wrap = on_every_item +ij_php_method_weight = 28 +ij_php_modifier_list_wrap = false +ij_php_multiline_chained_calls_semicolon_on_new_line = true +ij_php_namespace_brace_style = 1 +ij_php_new_line_after_php_opening_tag = true +ij_php_null_type_position = in_the_end +ij_php_package_weight = 28 +ij_php_param_weight = 1 +ij_php_parameters_attributes_wrap = normal +ij_php_parentheses_expression_new_line_after_left_paren = false +ij_php_parentheses_expression_right_paren_on_new_line = false +ij_php_phpdoc_blank_line_before_tags = true +ij_php_phpdoc_blank_lines_around_parameters = true +ij_php_phpdoc_keep_blank_lines = true +ij_php_phpdoc_param_spaces_between_name_and_description = 1 +ij_php_phpdoc_param_spaces_between_tag_and_type = 1 +ij_php_phpdoc_param_spaces_between_type_and_name = 1 +ij_php_phpdoc_use_fqcn = true +ij_php_phpdoc_wrap_long_lines = true +ij_php_place_assignment_sign_on_next_line = false +ij_php_place_parens_for_constructor = 1 +ij_php_property_read_weight = 28 +ij_php_property_weight = 28 +ij_php_property_write_weight = 28 +ij_php_return_type_on_new_line = false +ij_php_return_weight = 2 +ij_php_see_weight = 5 +ij_php_since_weight = 28 +ij_php_sort_phpdoc_elements = true +ij_php_space_after_colon = true +ij_php_space_after_colon_in_enum_backed_type = true +ij_php_space_after_colon_in_named_argument = true +ij_php_space_after_colon_in_return_type = true +ij_php_space_after_comma = true +ij_php_space_after_for_semicolon = true +ij_php_space_after_quest = true +ij_php_space_after_type_cast = true +ij_php_space_after_unary_not = false +ij_php_space_before_array_initializer_left_brace = false +ij_php_space_before_catch_keyword = true +ij_php_space_before_catch_left_brace = true +ij_php_space_before_catch_parentheses = true +ij_php_space_before_class_left_brace = true +ij_php_space_before_closure_left_parenthesis = true +ij_php_space_before_colon = true +ij_php_space_before_colon_in_enum_backed_type = false +ij_php_space_before_colon_in_named_argument = false +ij_php_space_before_colon_in_return_type = false +ij_php_space_before_comma = false +ij_php_space_before_do_left_brace = true +ij_php_space_before_else_keyword = true +ij_php_space_before_else_left_brace = true +ij_php_space_before_finally_keyword = true +ij_php_space_before_finally_left_brace = true +ij_php_space_before_for_left_brace = true +ij_php_space_before_for_parentheses = true +ij_php_space_before_for_semicolon = false +ij_php_space_before_if_left_brace = true +ij_php_space_before_if_parentheses = true +ij_php_space_before_method_call_parentheses = false +ij_php_space_before_method_left_brace = true +ij_php_space_before_method_parentheses = false +ij_php_space_before_quest = true +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_before_switch_left_brace = true +ij_php_space_before_switch_parentheses = true +ij_php_space_before_try_left_brace = true +ij_php_space_before_unary_not = false +ij_php_space_before_while_keyword = true +ij_php_space_before_while_left_brace = true +ij_php_space_before_while_parentheses = true +ij_php_space_between_ternary_quest_and_colon = false +ij_php_spaces_around_additive_operators = true +ij_php_spaces_around_arrow = false +ij_php_spaces_around_assignment_in_declare = true +ij_php_spaces_around_assignment_operators = true +ij_php_spaces_around_bitwise_operators = true +ij_php_spaces_around_equality_operators = true +ij_php_spaces_around_logical_operators = true +ij_php_spaces_around_multiplicative_operators = true +ij_php_spaces_around_null_coalesce_operator = true +ij_php_spaces_around_pipe_in_union_type = false +ij_php_spaces_around_relational_operators = true +ij_php_spaces_around_shift_operators = true +ij_php_spaces_around_unary_operator = false +ij_php_spaces_around_var_within_brackets = false +ij_php_spaces_within_array_initializer_braces = false +ij_php_spaces_within_brackets = false +ij_php_spaces_within_catch_parentheses = false +ij_php_spaces_within_for_parentheses = false +ij_php_spaces_within_if_parentheses = false +ij_php_spaces_within_method_call_parentheses = false +ij_php_spaces_within_method_parentheses = false +ij_php_spaces_within_parentheses = false +ij_php_spaces_within_short_echo_tags = true +ij_php_spaces_within_switch_parentheses = false +ij_php_spaces_within_while_parentheses = false +ij_php_special_else_if_treatment = false +ij_php_subpackage_weight = 28 +ij_php_ternary_operation_signs_on_next_line = true +ij_php_ternary_operation_wrap = on_every_item +ij_php_throws_weight = 3 +ij_php_todo_weight = 6 +ij_php_treat_multiline_arrays_and_lambdas_multiline = false +ij_php_unknown_tag_weight = 28 +ij_php_upper_case_boolean_const = true +ij_php_upper_case_null_const = true +ij_php_uses_weight = 28 +ij_php_var_weight = 0 +ij_php_variable_naming_style = camel_case +ij_php_version_weight = 28 +ij_php_while_brace_force = always +ij_php_while_on_new_line = false + +[{*.neon,*.neon.dist,*neon.template}] +indent_style = tab +tab_width = 4 diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 00000000..0d1172b2 --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,42 @@ +name: PHP_CodeSniffer + +on: workflow_dispatch +# pull_request: +# paths: +# - '**.php' +# - tools/phpcs/composer.json +# - phpcs.xml.dist + +jobs: + phpcs: + runs-on: ubuntu-latest + name: PHP_CodeSniffer + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + coverage: none + tools: cs2pr + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer composer-phpcs -- update --no-progress --prefer-dist + + - name: Run PHP_CodeSniffer + run: composer phpcs -- -q --report=checkstyle | cs2pr diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 00000000..fde8ae28 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,52 @@ +name: PHPStan + +on: workflow_dispatch +# pull_request: +# paths: +# - '**.php' +# - composer.json +# - tools/phpstan/composer.json +# - ci/composer.json +# - phpstan.ci.neon +# - phpstan.neon.dist + +jobs: + phpstan: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['7.4', '8.0', '8.3'] + prefer: ['prefer-stable', 'prefer-lowest'] + name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }} + + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + env: + fail-fast: true + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}- + + - name: Install dependencies + run: | + composer update --no-progress --prefer-dist --${{ matrix.prefer }} --optimize-autoloader && + composer composer-phpunit -- update --no-progress --prefer-dist && + composer composer-phpstan -- update --no-progress --prefer-dist --optimize-autoloader && + composer --working-dir=ci update --no-progress --prefer-dist --${{ matrix.prefer }} --ignore-platform-req=ext-gd + + - name: Run PHPStan + run: composer phpstan -- analyse -c phpstan.ci.neon diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 00000000..472bd850 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,37 @@ +name: PHPUnit + +on: + pull_request: + paths: + - '**.php' + - composer.json + - tools/phpunit/composer.json + - phpunit.xml.dist + - tests/docker-prepare.sh + +env: + # On github CI machine creating the "/vendor" volume fails otherwise with: read-only file system: unknown + BIND_VOLUME_PERMISSIONS: rw + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + matrix: + civicrm-image-tags: [ '5-drupal', '5.75-drupal-php8.1' ] + name: PHPUnit with Docker image michaelmcandrew/civicrm:${{ matrix.civicrm-image-tags }} + env: + CIVICRM_IMAGE_TAG: ${{ matrix.civicrm-image-tags }} + + steps: + - uses: actions/checkout@v3 + - name: Pull images + run: docker compose -f tests/docker-compose.yml pull --quiet + - name: Start containers + run: docker compose -f tests/docker-compose.yml up -d + - name: Prepare environment + run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/org.project60.sepa/tests/docker-prepare.sh + - name: Run PHPUnit + run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/org.project60.sepa/tests/docker-phpunit.sh + - name: Remove containers + run: docker compose -f tests/docker-compose.yml down -v diff --git a/.gitignore b/.gitignore index 5f17bbf5..e1893ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ -*.swp -.phpunit.result.cache \ No newline at end of file +/.phpcs.cache +/.phpunit.result.cache +/.phpstan/ +/ci/composer.lock +/ci/vendor/ +/composer.lock +/phpstan.neon +/tools/*/vendor/ +/tools/*/composer.lock +/vendor/ +/tests/phpunit/bootstrap.local.php diff --git a/ci/composer.json b/ci/composer.json new file mode 100644 index 00000000..9a2d2781 --- /dev/null +++ b/ci/composer.json @@ -0,0 +1,15 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "civicrm/composer-compile-plugin": false, + "civicrm/composer-downloads-plugin": true, + "cweagans/composer-patches": true + }, + "sort-packages": true + }, + "require": { + "civicrm/civicrm-core": "^5.75" + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..31b6e1d5 --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "name": "systopia/org.project60.sepa", + "type": "civicrm-ext", + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "SYSTOPIA GmbH", + "email": "info@systopia.de", + "homepage": "https://www.systopia.de" + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "prepend-autoloader": false, + "sort-packages": true + }, + "require": { + }, + "scripts": { + "composer-phpcs": [ + "@composer --working-dir=tools/phpcs" + ], + "composer-phpstan": [ + "@composer --working-dir=tools/phpstan" + ], + "composer-phpunit": [ + "@composer --working-dir=tools/phpunit" + ], + "composer-tools": [ + "@composer-phpcs", + "@composer-phpstan", + "@composer-phpunit" + ], + "phpcs": [ + "@php tools/phpcs/vendor/bin/phpcs" + ], + "phpcbf": [ + "@php tools/phpcs/vendor/bin/phpcbf" + ], + "phpstan": [ + "@php tools/phpstan/vendor/bin/phpstan" + ], + "phpunit": [ + "@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text" + ], + "test": [ + "@phpcs", + "@phpstan", + "@phpunit" + ] + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 00000000..c6686cf0 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,82 @@ + + + CiviCRM coding standard with some additional changes + + api + Civi + CRM + tests + sepa.php + + /CRM/Sepa/DAO/.*\.php$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.ci.neon b/phpstan.ci.neon new file mode 100644 index 00000000..883179fb --- /dev/null +++ b/phpstan.ci.neon @@ -0,0 +1,13 @@ +includes: + - phpstan.neon.dist + +parameters: + scanDirectories: + - ci/vendor/civicrm/civicrm-core/CRM/ + bootstrapFiles: + - ci/vendor/autoload.php + # Because we test with different versions in CI we have unmatched errors + reportUnmatchedIgnoredErrors: false + ignoreErrors: + # Errors we get when using "prefer-lowest" + - '#::getSubscribedEvents\(\) return type has no value type specified in iterable type array.$#' diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..2d2101ba --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,44 @@ +parameters: + paths: + - api + - Civi + - CRM + - tests + - sepa.php + excludePaths: + analyse: + - CRM/Sepa/DAO/* + - tests/phpunit/bootstrap.php + scanFiles: + - sepa.civix.php + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework/TestCase.php + scanDirectories: + - tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework + bootstrapFiles: + - tools/phpunit/vendor/bin/.phpunit/phpunit/vendor/autoload.php + - phpstanBootstrap.php + level: 9 + universalObjectCratesClasses: + - Civi\Core\Event\GenericHookEvent + - CRM_Core_Config + - CRM_Core_DAO + checkTooWideReturnTypesInProtectedAndPublicMethods: true + checkUninitializedProperties: true + checkMissingCallableSignature: true + treatPhpDocTypesAsCertain: false + exceptions: + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true + checkedExceptionClasses: + - \Webmozart\Assert\InvalidArgumentException + implicitThrows: false + ignoreErrors: + # Note paths are prefixed with "*/" to work with inspections in PHPStorm because of: + # https://youtrack.jetbrains.com/issue/WI-63891/PHPStan-ignoreErrors-configuration-isnt-working-with-inspections + + # Example + #- # Accessing results of API requests + #message: "#^Offset '[^']+' does not exist on array[^\\|]+\\|null.$#" + #path: */tests/phpunit/**/*Test.php + tmpDir: .phpstan diff --git a/phpstan.neon.template b/phpstan.neon.template new file mode 100644 index 00000000..34f6dfdc --- /dev/null +++ b/phpstan.neon.template @@ -0,0 +1,11 @@ +# Copy this file to phpstan.neon and replace {VENDOR_DIR} with the appropriate +# path. + +includes: + - phpstan.neon.dist + +parameters: + scanDirectories: + - {VENDOR_DIR}/civicrm/civicrm-core/CRM/ + bootstrapFiles: + - {VENDOR_DIR}/autoload.php diff --git a/phpstanBootstrap.php b/phpstanBootstrap.php new file mode 100644 index 00000000..67ad97d4 --- /dev/null +++ b/phpstanBootstrap.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types = 1); + +// phpcs:disable Drupal.Commenting.DocComment.ContentAfterOpen +/** @var \PHPStan\DependencyInjection\Container $container */ +/** @phpstan-var array $bootstrapFiles */ +$bootstrapFiles = $container->getParameter('bootstrapFiles'); +foreach ($bootstrapFiles as $bootstrapFile) { + if (str_ends_with($bootstrapFile, 'vendor/autoload.php')) { + $vendorDir = dirname($bootstrapFile); + $civiCrmVendorDir = $vendorDir . '/civicrm'; + $civiCrmCoreDir = $civiCrmVendorDir . '/civicrm-core'; + if (file_exists($civiCrmCoreDir)) { + set_include_path(get_include_path() + . PATH_SEPARATOR . $civiCrmCoreDir + . PATH_SEPARATOR . $civiCrmVendorDir . '/civicrm-packages' + ); + // $bootstrapFile might not be included, yet. It is required for the + // following require_once, though. + require_once $bootstrapFile; + // Prevent error "Class 'CRM_Core_Exception' not found in file". + require_once $civiCrmCoreDir . '/CRM/Core/Exception.php'; + + break; + } + } +} + +if (file_exists(__DIR__ . '/vendor/autoload.php')) { + require_once __DIR__ . '/vendor/autoload.php'; +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b9ae0eb4..b8ae14a7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,18 +1,38 @@ - - - - ./ - - + + + + + + + - + ./tests/phpunit + + + + api + CRM + Civi + + + CRM/Sepa/DAO + + + + diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 00000000..2508b810 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3" +services: + civicrm: + image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-5-drupal} + environment: + - PROJECT_NAME=test + - BASE_URL=http://localhost + - CIVICRM_DB_NAME=test + - CIVICRM_DB_USER=root + - CIVICRM_DB_PASS=secret + - CIVICRM_DB_HOST=mysql + - CIVICRM_DB_PORT=3306 + - CIVICRM_SITE_KEY=TEST_KEY + - DRUPAL_DB_NAME=test + - DRUPAL_DB_USER=root + - DRUPAL_DB_PASS=secret + - DRUPAL_DB_HOST=mysql + - DRUPAL_DB_PORT=3306 + - PHP_DATE_TIMEZONE=UTC + - DEBUG=ON + - SMTP_HOST=localhost + - SMTP_MAILDOMAIN=example.org + volumes: + - ../:/var/www/html/sites/default/files/civicrm/ext/org.project60.sepa:${BIND_VOLUME_PERMISSIONS:-ro} + - /var/www/html/sites/default/files/civicrm/ext/org.project60.sepa/vendor + - /var/www/html/sites/default/files/civicrm/ext/org.project60.sepa/tools/phpunit/vendor + # Don't start Apache HTTP Server, but keep container running + command: ["tail", "-f", "/dev/null"] + stop_signal: SIGKILL + mysql: + image: mariadb + environment: + MARIADB_ROOT_PASSWORD: secret + MARIADB_DATABASE: test diff --git a/tests/docker-phpunit.sh b/tests/docker-phpunit.sh new file mode 100755 index 00000000..2b0fc692 --- /dev/null +++ b/tests/docker-phpunit.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -eu -o pipefail + +SCRIPT_DIR=$(realpath "$(dirname "$0")") +EXT_DIR=$(dirname "$SCRIPT_DIR") + +cd "$EXT_DIR" +if [ ! -e tools/phpunit/vendor/bin ]; then + "$SCRIPT_DIR/docker-prepare.sh" +fi + +export XDEBUG_MODE=coverage +# TODO: Remove when not needed, anymore. +# In Docker container with CiviCRM 5.5? all deprecations are reported as direct +# deprecations so "disabling" check of deprecation count is necessary for the +# tests to pass (if baselineFile does not contain all deprecations). +export SYMFONY_DEPRECATIONS_HELPER="max[total]=99999&baselineFile=./tests/ignored-deprecations.json" + +composer phpunit -- --cache-result-file=/tmp/.phpunit.result.cache "$@" diff --git a/tests/docker-prepare.sh b/tests/docker-prepare.sh new file mode 100755 index 00000000..e95c9ce3 --- /dev/null +++ b/tests/docker-prepare.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -eu -o pipefail + +EXT_DIR=$(dirname "$(dirname "$(realpath "$0")")") +EXT_NAME=$(basename "$EXT_DIR") + +i=0 +while ! mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" -e 'SELECT 1;' >/dev/null 2>&1; do + i=$((i+1)) + if [ $i -gt 10 ]; then + echo "Failed to connect to database" >&2 + exit 1 + fi + + echo -n . + sleep 1 +done + +echo + +export XDEBUG_MODE=off +if mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" "$CIVICRM_DB_NAME" -e 'SELECT 1 FROM civicrm_setting LIMIT 1;' >/dev/null 2>&1; then + cv flush +else + # For headless tests it is required that CIVICRM_UF is defined using the corresponding env variable. + sed -E "s/define\('CIVICRM_UF', '([^']+)'\);/define('CIVICRM_UF', getenv('CIVICRM_UF') ?: '\1');/g" \ + -i /var/www/html/sites/default/civicrm.settings.php + civicrm-docker-install + + # Avoid this error: + # The autoloader expected class "Civi\ActionSchedule\Mapping" to be defined in + # file "[...]/Civi/ActionSchedule/Mapping.php". The file was found but the + # class was not in it, the class name or namespace probably has a typo. + # + # Necessary for CiviCRM 5.66.0 - 5.74.x. + # https://github.com/civicrm/civicrm-core/blob/5.66.0/Civi/ActionSchedule/Mapping.php + if [ -e /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php ] \ + && grep -q '// Empty file' /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php; then + rm /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php + fi + + # For headless tests these files need to exist. + touch /var/www/html/sites/all/modules/civicrm/sql/test_data.mysql + touch /var/www/html/sites/all/modules/civicrm/sql/test_data_second_domain.mysql + + cv ext:enable "$EXT_NAME" +fi + +cd "$EXT_DIR" +composer update --no-progress --prefer-dist --optimize-autoloader +composer composer-phpunit -- update --no-progress --prefer-dist diff --git a/tests/ignored-deprecations.json b/tests/ignored-deprecations.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/ignored-deprecations.json @@ -0,0 +1 @@ +[] diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index afa827e9..36ceaf69 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -1,16 +1,75 @@ add('CRM_', __DIR__); -$loader->add('Civi\\', __DIR__); -$loader->add('api_', __DIR__); -$loader->add('api\\', __DIR__); -$loader->register(); +// Add test classes to class loader. +addExtensionDirToClassLoader(__DIR__); +addExtensionToClassLoader('org.project60.sepa'); + +if (!function_exists('ts')) { + // Ensure function ts() is available - it's declared in the same file as CRM_Core_I18n in CiviCRM < 5.74. + // In later versions the function is registered following the composer conventions. + \CRM_Core_I18n::singleton(); +} + +/** + * Modify DI container for tests. + */ +function _sepa_test_civicrm_container(ContainerBuilder $container): void { +} + +function addExtensionToClassLoader(string $extension): void { + addExtensionDirToClassLoader(__DIR__ . '/../../../' . $extension); +} + +function addExtensionDirToClassLoader(string $extensionDir): void { + $loader = new ClassLoader(); + $loader->add('CRM_', [$extensionDir]); + $loader->addPsr4('Civi\\', [$extensionDir . '/Civi']); + $loader->add('api_', [$extensionDir]); + $loader->addPsr4('api\\', [$extensionDir . '/api']); + $loader->register(); + + if (file_exists($extensionDir . '/autoload.php')) { + require_once $extensionDir . '/autoload.php'; + } +} /** * Call the "cv" command. @@ -19,16 +78,17 @@ * The rest of the command to send. * @param string $decode * Ex: 'json' or 'phpcode'. - * @return string + * @return mixed * Response output (if the command executed normally). + * For 'raw' or 'phpcode', this will be a string. For 'json', it could be any JSON value. * @throws \RuntimeException * If the command terminates abnormally. */ -function cv($cmd, $decode = 'json') { +function cv(string $cmd, string $decode = 'json') { $cmd = 'cv ' . $cmd; - $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR); + $descriptorSpec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => STDERR]; $oldOutput = getenv('CV_OUTPUT'); - putenv("CV_OUTPUT=json"); + putenv('CV_OUTPUT=json'); // Execute `cv` in the original folder. This is a work-around for // phpunit/codeception, which seem to manipulate PWD. @@ -40,7 +100,7 @@ function cv($cmd, $decode = 'json') { $result = stream_get_contents($pipes[1]); fclose($pipes[1]); if (proc_close($process) !== 0) { - throw new RuntimeException("Command failed ($cmd):\n$result"); + throw new \RuntimeException("Command failed ($cmd):\n$result"); } switch ($decode) { case 'raw': @@ -48,15 +108,16 @@ function cv($cmd, $decode = 'json') { case 'phpcode': // If the last output is /*PHPCODE*/, then we managed to complete execution. - if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { + if (substr(trim($result), 0, 12) !== '/*BEGINPHP*/' || substr(trim($result), -10) !== '/*ENDPHP*/') { throw new \RuntimeException("Command failed ($cmd):\n$result"); } return $result; case 'json': - return json_decode($result, 1); + return json_decode($result, TRUE); default: - throw new RuntimeException("Bad decoder format ($decode)"); + throw new \RuntimeException("Bad decoder format ($decode)"); } } + diff --git a/tools/phpcs/composer.json b/tools/phpcs/composer.json new file mode 100644 index 00000000..980e4b96 --- /dev/null +++ b/tools/phpcs/composer.json @@ -0,0 +1,11 @@ +{ + "repositories": [ + { + "type": "git", + "url": "https://github.com/civicrm/coder.git" + } + ], + "require": { + "drupal/coder": "dev-8.x-2.x-civi" + } +} diff --git a/tools/phpstan/composer.json b/tools/phpstan/composer.json new file mode 100644 index 00000000..218fa72d --- /dev/null +++ b/tools/phpstan/composer.json @@ -0,0 +1,18 @@ +{ + "require": { + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpstan/phpstan-webmozart-assert": "^1.2", + "thecodingmachine/phpstan-strict-rules": "^1.0", + "voku/phpstan-rules": "^3.0" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "sort-packages": true + } +} diff --git a/tools/phpunit/composer.json b/tools/phpunit/composer.json new file mode 100644 index 00000000..ab64ce05 --- /dev/null +++ b/tools/phpunit/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "symfony/phpunit-bridge": "^7" + }, + "scripts": { + "post-install-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ], + "post-update-cmd": [ + "@php vendor/bin/simple-phpunit install --configuration ../../phpunit.xml.dist" + ] + } +}