From 8cd928caa92c10529d6155fb5ce1e8a86c75e3ac Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Mon, 25 Mar 2024 14:51:55 +0100 Subject: [PATCH] Add extension template with PHPStan, PHPUnit and phpcs --- .editorconfig | 240 ++++++++++++++++++++++++++++++++ .github/workflows/phpcs.yml | 42 ++++++ .github/workflows/phpstan.yml | 52 +++++++ .github/workflows/phpunit.yml | 37 +++++ .gitignore | 10 ++ ci/README.md | 2 + ci/composer.json | 33 +++++ composer.json | 45 ++++++ info.xml | 2 +- phpcs.xml.dist | 77 ++++++++++ phpstan.ci.neon | 13 ++ phpstan.neon.dist | 42 ++++++ phpstan.neon.template | 13 ++ phpstanBootstrap.php | 43 ++++++ phpunit.xml.dist | 35 +++++ tests/docker-compose.yml | 33 +++++ tests/docker-phpunit.sh | 19 +++ tests/docker-prepare.sh | 45 ++++++ tests/ignored-deprecations.json | 1 + tools/phpcs/composer.json | 11 ++ tools/phpstan/composer.json | 18 +++ tools/phpunit/composer.json | 13 ++ 22 files changed, 825 insertions(+), 1 deletion(-) 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 .gitignore create mode 100644 ci/README.md 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 phpunit.xml.dist 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 0000000..f5f7003 --- /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 0000000..395cf22 --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,42 @@ +name: PHP_CodeSniffer + +on: + 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 0000000..33c30f5 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,52 @@ +name: PHPStan + +on: + 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.1'] + 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 }} && + 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 }} --optimize-autoloader --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 0000000..a30e116 --- /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-php8.1', '5-drupal-php7.4', '5.56-drupal-php7.4' ] + 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/de.systopia.twingle/tests/docker-prepare.sh + - name: Run PHPUnit + run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/de.systopia.twingle/tests/docker-phpunit.sh + - name: Remove containers + run: docker compose -f tests/docker-compose.yml down -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62dffec --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.phpcs.cache +/.phpunit.result.cache +/.phpstan/ +/ci/composer.lock +/ci/vendor/ +/composer.lock +/phpstan.neon +/tools/*/vendor/ +/tools/*/composer.lock +/vendor/ diff --git a/ci/README.md b/ci/README.md new file mode 100644 index 0000000..0bfc584 --- /dev/null +++ b/ci/README.md @@ -0,0 +1,2 @@ +The dependencies specified in composer.json of this directory are required to +run phpstan in CI. diff --git a/ci/composer.json b/ci/composer.json new file mode 100644 index 0000000..ab469d6 --- /dev/null +++ b/ci/composer.json @@ -0,0 +1,33 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "civicrm/composer-compile-plugin": false, + "civicrm/composer-downloads-plugin": true, + "cweagans/composer-patches": true + } + }, + "require": { + "civicrm/civicrm-core": "^5" + }, + "scripts": { + "post-install-or-update": [ + "# The following statements are only necessary when the extension is inside a CiviCRM installation, actually not required in CI.", + "# Avoid redeclaration of function \\GuzzleHttp\\http_build_query()", + "if [ -e vendor/civicrm/civicrm-core/guzzle_php81_shim.php ]; then echo '' >vendor/civicrm/civicrm-core/guzzle_php81_shim.php; fi", + "# Avoid redeclaration of function \\GuzzleHttp\\Promise\\queue()", + "if [ -e vendor/guzzlehttp/promises/src/functions.php ]; then echo '' >vendor/guzzlehttp/promises/src/functions.php; fi", + "# Avoid CiviCRM load extensions in vendor", + "if [ -e vendor/civicrm ]; then find vendor/civicrm -name 'info.xml' -delete; fi", + "# Avoid Class 'CRM_AfformAdmin_ExtensionUtil' not found", + "find vendor -name '*.mgd.php' -delete" + ], + "post-install-cmd": [ + "@post-install-or-update" + ], + "post-update-cmd": [ + "@post-install-or-update" + ] + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5489fbd --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "systopia/de.systopia.twingle", + "type": "civicrm-ext", + "license": "AGPL-3.0-or-later", + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "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/info.xml b/info.xml index 05ca281..32c0ab7 100644 --- a/info.xml +++ b/info.xml @@ -18,7 +18,7 @@ 1.5-dev dev - 5.19 + 5.56 diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..ce14b44 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,77 @@ + + + CiviCRM coding standard with some additional changes + + api + Civi + CRM + tests + + /CRM/[^/]+/DAO/.*\.php$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.ci.neon b/phpstan.ci.neon new file mode 100644 index 0000000..883179f --- /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 0000000..0691f8e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,42 @@ +parameters: + paths: + - api + - Civi + - CRM + - tests + excludePaths: + analyse: + - CRM/*/DAO/* + - tests/phpunit/bootstrap.php + scanFiles: + - twingle.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 + - vendor/autoload.php + - phpstanBootstrap.php + level: 9 + universalObjectCratesClasses: + - Civi\Core\Event\GenericHookEvent + 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 0000000..9f2f699 --- /dev/null +++ b/phpstan.neon.template @@ -0,0 +1,13 @@ +# 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/Civi/ + - {VENDOR_DIR}/civicrm/civicrm-core/CRM/ + - {VENDOR_DIR}/civicrm/civicrm-core/api/ + bootstrapFiles: + - {VENDOR_DIR}/autoload.php diff --git a/phpstanBootstrap.php b/phpstanBootstrap.php new file mode 100644 index 0000000..08b3403 --- /dev/null +++ b/phpstanBootstrap.php @@ -0,0 +1,43 @@ +. + */ + +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; + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..2df3e9e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + + + + + + + ./tests/phpunit + + + + + api + CRM + Civi + + CRM/*/DAO + + + + + + + + + diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..840130d --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,33 @@ +version: "3" +services: + civicrm: + image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-5-drupal-php8.1} + 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 + - 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/de.systopia.twingle:${BIND_VOLUME_PERMISSIONS:-ro} + - /var/www/html/sites/default/files/civicrm/ext/de.systopia.twingle/vendor + - /var/www/html/sites/default/files/civicrm/ext/de.systopia.twingle/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 0000000..2b0fc69 --- /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 0000000..a293c74 --- /dev/null +++ b/tests/docker-prepare.sh @@ -0,0 +1,45 @@ +#!/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. + rm -f /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php + + # 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 --no-dev +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 0000000..fe51488 --- /dev/null +++ b/tests/ignored-deprecations.json @@ -0,0 +1 @@ +[] diff --git a/tools/phpcs/composer.json b/tools/phpcs/composer.json new file mode 100644 index 0000000..980e4b9 --- /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 0000000..218fa72 --- /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 0000000..ab13d43 --- /dev/null +++ b/tools/phpunit/composer.json @@ -0,0 +1,13 @@ +{ + "require": { + "symfony/phpunit-bridge": "^6.1" + }, + "scripts": { + "post-install-cmd": [ + "@php vendor/bin/simple-phpunit install" + ], + "post-update-cmd": [ + "@php vendor/bin/simple-phpunit install" + ] + } +}