diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e389d472f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.github export-ignore +/.phive export-ignore +/.php-cs-fixer.dist.php export-ignore +/build export-ignore +/build.xml export-ignore +/phpstan.neon export-ignore +/phpunit.xml export-ignore +/tests export-ignore +/tools export-ignore +/tools/* binary + +*.php diff=php diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ee242a803 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,28 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at sebastian@phpunit.de. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/3/0/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..fedcef3bc --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Contributing to `phpunit/php-code-coverage` + +## Welcome! + +We look forward to your contributions! Here are some examples how you can contribute: + +* [Report a bug](https://github.com/sebastianbergmann/php-code-coverage/issues/new) +* [Send a pull request to fix a bug](https://github.com/sebastianbergmann/php-code-coverage/pulls) + + +## We have a Code of Conduct + +Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. + + +## Any contributions you make will be under the BSD-3-Clause License + +When you submit code changes, your submissions are understood to be under the same [BSD-3-Clause License](https://github.com/sebastianbergmann/php-code-coverage/blob/main/LICENSE) that covers the project. By contributing to this project, you agree that your contributions will be licensed under its BSD-3-Clause License. + + +### Do Not Violate Copyright + +Only submit a pull request with your own original code. Do NOT submit a pull request containing code which you have largely copied from +another project, unless you wrote the respective code yourself. + +Open Source does not mean that copyright does not apply. Copyright infringements will not be tolerated and can lead to you being banned from this project and repository. + + +### Do Not Submit AI-Generated Pull Requests + +The same goes for (largely) AI-generated pull requests. These are not welcome as they will be based on copyrighted code from others +without accreditation and without taking the license of the original code into account, let alone getting permission +for the use of the code or for re-licensing. + +Aside from that, the experience is that AI-generated pull requests will be incorrect 100% of the time and cost reviewers too much time. +Submitting a (largely) AI-generated pull request will lead to you being banned from this project and repository. + + +## Write bug reports with detail, background, and sample code + +[This is an example](https://github.com/sebastianbergmann/phpunit/issues/4376) of a bug report I wrote, and I think it's not too bad. + +In your bug report, please provide the following: + +* A quick summary and/or background +* Steps to reproduce + * Be specific! + * Give sample code if you can. +* What you expected would happen +* What actually happens +* Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + +Please do not report a bug for a version of this library that is no longer supported. Please do not report a bug if you are using a version of PHP that is not supported by the version of this library you are using. + +The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. Support for this library follows the [support for the version of PHPUnit that uses a specific version of this library](https://phpunit.de/supported-versions.html). + +Please post code and output as text ([using proper markup](https://guides.github.com/features/mastering-markdown/)). Do not post screenshots of code or output. + + +## Workflow for Pull Requests + +1. Fork the repository. +2. Create your branch from `main` if you plan to implement new functionality or change existing code significantly; create your branch from the oldest branch that is affected by the bug if you plan to fix a bug. +3. Implement your change and add tests for it. +4. Ensure the test suite passes. +5. Ensure the code complies with our coding guidelines (see below). +6. Send that pull request! + +Please make sure you have [set up your username and email address](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup) for use with Git. Strings such as `silly nick name ` look really stupid in the commit history of a project. + +We encourage you to [sign your Git commits with your GPG key](https://docs.github.com/en/github/authenticating-to-github/signing-commits). + +Pull requests for bug fixes must be made for the oldest branch that is supported (see above). Pull requests for new features must be based on the `main` branch. + +We are trying to keep backwards compatibility breaks to an absolute minimum. Please take this into account when proposing changes. + +Due to time constraints, we are not always able to respond as quickly as we would like. Please do not take delays personal and feel free to remind us if you feel that we forgot to respond. + + +## Development + +This project uses [PHPUnit](https://phpunit.de/) for testing: + +```shell +./vendor/bin/phpunit +``` + +This project uses [PHPStan](https://phpstan.org/) for static analysis: + +```shell +./tools/phpstan +``` + +This project uses [PHP-CS-Fixer](https://cs.symfony.com/) to enforce coding guidelines: + +```shell +./tools/php-cs-fixer fix +``` + +The commands shown above require an autoloader script at `vendor/autoload.php`. This can be generated like so: + +```shell +./tools/composer dump-autoload +``` + +Please understand that we will not accept a pull request when its changes violate this project's coding guidelines or break the test suite. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d40ffea35 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: sebastianbergmann +liberapay: sebastianbergmann +thanks_dev: u/gh/sebastianbergmann +tidelift: "packagist/phpunit/php-code-coverage" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..5ca75c07d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ +| Q | A +| --------------------------| --------------- +| php-code-coverage version | x.y.z +| PHP version | x.y.z +| Driver | PCOV / Xdebug +| PCOV version (if used) | x.y.z +| Xdebug version (if used) | x.y.z +| Installation Method | Composer / PHPUnit PHAR +| Usage Method | PHPUnit / other +| PHPUnit version (if used) | x.y.z + + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..2f9c008d6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,117 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +on: + - pull_request + - push + +name: CI + +env: + COMPOSER_ROOT_VERSION: 12.3.x-dev + +jobs: + coding-guidelines: + name: Coding Guidelines + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + extensions: none, iconv, json, phar, tokenizer + coverage: none + tools: none + + - name: Run PHP-CS-Fixer + run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose + + static-analysis: + name: Static Analysis + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: none, ctype, curl, dom, iconv, mbstring, opcache, simplexml, tokenizer, xml, xmlwriter + coverage: none + tools: none + + - name: Install dependencies with Composer + run: ./tools/composer update --no-interaction --no-ansi --no-progress + + - name: Run PHPStan + run: ./tools/phpstan analyse --no-progress --error-format=github + + tests: + name: Tests + + runs-on: ${{ matrix.os }} + + env: + PHP_EXTENSIONS: none, ctype, curl, dom, json, libxml, mbstring, openssl, pdo_sqlite, soap, tokenizer, xml, xmlwriter + PHP_INI_VALUES: memory_limit=-1, assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On + + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + + php-version: + - 8.3 + - 8.4 + - 8.5 + + coverage-driver: + - pcov + - xdebug3 + + steps: + - name: Configure Git to avoid issues with line endings + if: matrix.os == 'windows-latest' + run: git config --global core.autocrlf false + + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: ${{ matrix.coverage-driver }} + extensions: ${{ env.PHP_EXTENSIONS }} + ini-values: ${{ env.PHP_INI_VALUES }} + tools: none + + - name: Install dependencies with Composer + run: php ./tools/composer update --no-ansi --no-interaction --no-progress + + - name: Run tests with PHPUnit + run: ./vendor/bin/phpunit --log-junit test-results.xml --coverage-openclover=code-coverage.xml + + - name: Upload test results to Codecov.io + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + disable_search: true + files: ./test-results.xml + + - name: Upload code coverage data to Codecov.io + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + disable_search: true + files: ./code-coverage.xml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..11111a6b7 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,43 @@ +# https://docs.github.com/en/actions + +on: + push: + tags: + - "**" + +name: Release + +jobs: + release: + name: Release + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + extensions: none + tools: none + + - name: Determine tag + run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Parse ChangeLog + run: build/scripts/extract-release-notes.php ${{ env.RELEASE_TAG }} > release-notes.md + + - name: Create release + uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ env.RELEASE_TAG }} + name: phpunit/php-code-coverage ${{ env.RELEASE_TAG }} + bodyFile: release-notes.md diff --git a/.gitignore b/.gitignore index 7aaeae10d..5344939da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -build -phpunit.xml +/.idea +/.php-cs-fixer.php +/.php-cs-fixer.cache +/.phpunit.cache +/composer.lock +/vendor diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 000000000..dac7fc73f --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 000000000..89883a868 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,369 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +$finder = PhpCsFixer\Finder::create() + ->files() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests/src') + ->in(__DIR__ . '/tests/tests') +; + +$config = new PhpCsFixer\Config; +$config->setFinder($finder) + ->setUnsupportedPhpVersionAllowed(true) + ->setRiskyAllowed(true) + ->setRules([ + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'array_push' => true, + 'array_syntax' => ['syntax' => 'short'], + 'attribute_empty_parentheses' => [ + 'use_parentheses' => false, + ], + 'backtick_to_shell_exec' => true, + 'binary_operator_spaces' => [ + 'operators' => [ + '*=' => 'align_single_space_minimal', + '+=' => 'align_single_space_minimal', + '-=' => 'align_single_space_minimal', + '/=' => 'align_single_space_minimal', + '=' => 'align_single_space_minimal', + '=>' => 'align_single_space_minimal', + ], + ], + 'blank_line_after_namespace' => true, + 'blank_line_before_statement' => [ + 'statements' => [ + 'break', + 'case', + 'continue', + 'declare', + 'default', + 'do', + 'exit', + 'for', + 'foreach', + 'goto', + 'if', + 'include', + 'include_once', + 'phpdoc', + 'require', + 'require_once', + 'return', + 'switch', + 'throw', + 'try', + 'while', + 'yield', + 'yield_from', + ], + ], + 'blank_lines_before_namespace' => [ + 'max_line_breaks' => 1, + 'min_line_breaks' => 0, + ], + 'braces_position' => [ + 'anonymous_classes_opening_brace' => 'next_line_unless_newline_at_signature_end', + 'anonymous_functions_opening_brace' => 'next_line_unless_newline_at_signature_end', + ], + 'cast_spaces' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'const' => 'none', + 'method' => 'one', + 'property' => 'only_if_meta' + ] + ], + 'class_definition' => true, + 'clean_namespace' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'compact_nullable_type_declaration' => true, + 'concat_space' => ['spacing' => 'one'], + 'constant_case' => true, + 'control_structure_braces' => true, + 'control_structure_continuation_position' => true, + 'declare_equal_normalize' => ['space' => 'none'], + 'declare_parentheses' => true, + 'declare_strict_types' => true, + 'dir_constant' => true, + 'echo_tag_syntax' => true, + 'elseif' => true, + 'encoding' => true, + 'ereg_to_preg' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'fopen_flag_order' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => ['import_symbols' => true], + 'function_declaration' => true, + 'function_to_constant' => true, + 'get_class_to_class_keyword' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => true, + ], + 'header_comment' => ['header' => $header, 'separate' => 'none'], + 'heredoc_to_nowdoc' => true, + 'implode_call' => true, + 'include' => true, + 'increment_style' => [ + 'style' => 'post', + ], + 'indentation_type' => true, + 'integer_literal_case' => true, + 'is_null' => true, + 'lambda_not_used_import' => true, + 'line_ending' => true, + 'list_syntax' => ['syntax' => 'short'], + 'logical_operators' => true, + 'lowercase_cast' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + ], + 'method_chaining_indentation' => true, + 'modernize_strpos' => true, + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'multiline_whitespace_before_semicolons' => true, + 'native_constant_invocation' => true, + 'native_function_casing' => false, + 'native_function_invocation' => [ + 'include' => [ + '@internal', + ], + ], + 'native_type_declaration_casing' => true, + 'new_with_parentheses' => [ + 'anonymous_class' => false, + 'named_class' => false, + ], + 'no_alias_functions' => true, + 'no_alias_language_construct_call' => true, + 'no_alternative_syntax' => true, + 'no_binary_string' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_break_comment' => true, + 'no_closing_tag' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'attribute', + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ], + 'no_homoglyph_names' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => ['use' => 'print'], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_multiple_statements_per_line' => true, + 'no_null_property_initialization' => true, + 'no_php4_constructor' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_space_around_double_colon' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_superfluous_elseif' => true, + 'no_superfluous_phpdoc_tags' => [ + 'allow_mixed' => true, + ], + 'no_trailing_comma_in_singleline' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_trailing_whitespace_in_string' => true, + 'no_unneeded_braces' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_final_method' => true, + 'no_unneeded_import_alias' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unset_cast' => true, + 'no_unset_on_property' => true, + 'no_unused_imports' => true, + 'no_useless_concat_operator' => true, + 'no_useless_else' => true, + 'no_useless_nullsafe_operator' => true, + 'no_useless_return' => true, + 'no_useless_sprintf' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'non_printable_character' => true, + 'normalize_index_brace' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'object_operator_without_whitespace' => true, + 'octal_notation' => true, + 'operator_linebreak' => [ + 'only_booleans' => true, + 'position' => 'end', + ], + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'property_public', + 'property_protected', + 'property_private', + 'method_public_static', + 'construct', + 'destruct', + 'magic', + 'phpunit', + 'method_public', + 'method_protected', + 'method_private', + 'method_protected_static', + 'method_private_static', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'const', + 'function', + 'class', + ] + ], + 'ordered_interfaces' => [ + 'direction' => 'ascend', + 'order' => 'alpha', + ], + 'ordered_traits' => true, + 'ordered_types' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_case_static_method_calls' => [ + 'call_type' => 'this', + ], + 'phpdoc_add_missing_param_annotation' => false, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_order_by_value' => [ + 'annotations' => [ + 'covers', + 'dataProvider', + 'throws', + 'uses', + ], + ], + 'phpdoc_param_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_tag_casing' => true, + 'phpdoc_tag_type' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types' => ['groups' => ['simple', 'meta']], + 'phpdoc_types_order' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'pow_to_exponentiation' => true, + 'protected_to_private' => true, + 'return_assignment' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'self_accessor' => true, + 'self_static_accessor' => true, + 'semicolon_after_instruction' => true, + 'set_type_to_cast' => true, + 'short_scalar_cast' => true, + 'simple_to_complex_string_variable' => true, + 'simplified_null_return' => false, + 'single_blank_line_at_eof' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_line_comment_spacing' => true, + 'single_quote' => true, + 'single_space_around_construct' => true, + 'single_trait_insert_per_statement' => true, + 'space_after_semicolon' => true, + 'spaces_inside_parentheses' => [ + 'space' => 'none', + ], + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'statement_indentation' => true, + 'static_lambda' => true, + 'strict_param' => true, + 'string_length_to_empty'=> true, + 'string_line_ending' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'switch_continue_to_break' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_elvis_operator' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => [ + 'elements' => [ + 'arguments', + 'arrays', + 'match', + ] + ], + 'trim_array_spaces' => true, + 'type_declaration_spaces' => [ + 'elements' => [ + 'function', + ], + ], + 'types_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => [ + 'elements' => [ + 'const', + 'method', + 'property', + ], + ], + 'void_return' => true, + 'whitespace_after_comma_in_array' => true, + ]); + +$config->setCacheFile(__DIR__ . '/.php-cs-fixer.cache/' . json_decode((string) @file_get_contents('composer.json'), true)["extra"]["branch-alias"]["dev-main"] ?? 'unknown'); + +$config->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()); + +return $config; diff --git a/ChangeLog-12.3.md b/ChangeLog-12.3.md new file mode 100644 index 000000000..f076381c5 --- /dev/null +++ b/ChangeLog-12.3.md @@ -0,0 +1,31 @@ +# ChangeLog + +All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## [12.3.2] - 2025-07-29 + +### Changed + +* Add coverage and complexity columns to class and method complexity tables +* Add CRAP to graph tooltip + +### Fixed + +* [#1081](https://github.com/sebastianbergmann/php-code-coverage/issues/1081): Class complexity scatter chart tooltips show incorrect class + +## [12.3.1] - 2025-06-18 + +### Changed + +* Changed CSS for HTML report to not use common ligatures as this sometimes lead to hard-to-read code +* Updated Bootstrap to version 5.3.6 for HTML report + +## [12.3.0] - 2025-05-23 + +### Changed + +* [#1080](https://github.com/sebastianbergmann/php-code-coverage/pull/1080): Support for reporting code coverage information in OpenClover XML format; unlike the existing Clover XML reporter, which remains unchanged, the XML documents generated by this new reporter validate against the OpenClover project's XML schema definition, with one exception: we do not generate the `` element. This feature is experimental and the generated XML might change in order to improve compliance with the OpenClover project's XML schema definition further. Such changes will be made in bugfix and/or minor releases even if they break backward compatibility. + +[12.3.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.1...12.3.2 +[12.3.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.3.0...12.3.1 +[12.3.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.2.1...12.3.0 diff --git a/ChangeLog.markdown b/ChangeLog.markdown deleted file mode 100644 index c412ecc01..000000000 --- a/ChangeLog.markdown +++ /dev/null @@ -1,38 +0,0 @@ -PHP_CodeCoverage 1.0 -==================== - -This is the list of changes for the PHP_CodeCoverage 1.0 release series. - -PHP_CodeCoverage 1.0.4 ----------------------- - -* Fixed an issue where `mkdir()` was called with a wrong argument. -* Updated list of dependencies in `package.xml`. - -PHP_CodeCoverage 1.0.3 ----------------------- - -* Fixed GH-32: `//@codeCoverageIgnore*` (no leading space) no longer works. -* Fixed a bug in `PHP_CodeCoverage_Report_HTML_Node_File::getNumClasses()`. -* Abstract methods are now excluded from code coverage statistics. -* When the directory to which the Clover XML logfile is to be written does not exist it is created. -* Updated bundled RGraph library to version 2010-12-24. -* Updated bundled YUI library to version 2.8.2r1. - -PHP_CodeCoverage 1.0.2 ----------------------- - -* Fixed the `version_compare()` check for Xdebug 2.2. - -PHP_CodeCoverage 1.0.1 ----------------------- - -* Covered lines of uncovered files are now correctly marked as uncovered. -* Fixed the detection of uncovered files. -* A warning is now printed when Xdebug 2.2 (or later) is used and `xdebug.coverage_enable=0` is set. -* Various minor performance optimizations. - -PHP_CodeCoverage 1.0.0 ----------------------- - -* Initial release. diff --git a/LICENSE b/LICENSE index ecf094fd9..017eb48b1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,33 +1,29 @@ -PHP_CodeCoverage +BSD 3-Clause License -Copyright (c) 2009-2011, Sebastian Bergmann . +Copyright (c) 2009-2025, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: +modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. - * Neither the name of Sebastian Bergmann nor the names of his - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PHP/CodeCoverage.php b/PHP/CodeCoverage.php deleted file mode 100644 index 1be05cf58..000000000 --- a/PHP/CodeCoverage.php +++ /dev/null @@ -1,613 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage/Driver/Xdebug.php'; -require_once 'PHP/CodeCoverage/Filter.php'; -require_once 'PHP/CodeCoverage/Util.php'; - -/** - * Provides collection functionality for PHP code coverage information. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage -{ - /** - * @var PHP_CodeCoverage_Driver - */ - protected $driver; - - /** - * @var PHP_CodeCoverage_Filter - */ - protected $filter; - - /** - * @var boolean - */ - protected $forceCoversAnnotation = FALSE; - - /** - * @var boolean - */ - protected $mapTestClassNameToCoveredClassName = FALSE; - - /** - * @var boolean - */ - protected $processUncoveredFilesFromWhitelist = TRUE; - - /** - * @var mixed - */ - protected $currentId; - - /** - * List of covered files. - * - * @var array - */ - protected $coveredFiles = array(); - - /** - * Raw code coverage data. - * - * @var array - */ - protected $data = array(); - - /** - * Summarized code coverage data. - * - * @var array - */ - protected $summary = array(); - - /** - * Test data. - * - * @var array - */ - protected $tests = array(); - - /** - * @var boolean - */ - protected $isCodeCoverageTestSuite = FALSE; - - /** - * @var boolean - */ - protected $isFileIteratorTestSuite = FALSE; - - /** - * @var boolean - */ - protected $isTimerTestSuite = FALSE; - - /** - * @var boolean - */ - protected $isTokenStreamTestSuite = FALSE; - - /** - * Default PHP_CodeCoverage object. - * - * @var PHP_CodeCoverage - */ - protected static $instance; - - /** - * Constructor. - * - * @param PHP_CodeCoverage_Driver $driver - * @param PHP_CodeCoverage_Filter $filter - * @throws InvalidArgumentException - */ - public function __construct(PHP_CodeCoverage_Driver $driver = NULL, PHP_CodeCoverage_Filter $filter = NULL) - { - if ($driver === NULL) { - $driver = new PHP_CodeCoverage_Driver_Xdebug; - } - - if ($filter === NULL) { - $filter = PHP_CodeCoverage_Filter::getInstance(); - } - - $this->driver = $driver; - $this->filter = $filter; - - if (defined('PHP_CODECOVERAGE_TESTSUITE')) { - $this->isCodeCoverageTestSuite = TRUE; - } - - if (defined('FILE_ITERATOR_TESTSUITE')) { - $this->isFileIteratorTestSuite = TRUE; - } - - if (defined('PHP_TIMER_TESTSUITE')) { - $this->isTimerTestSuite = TRUE; - } - - if (defined('PHP_TOKENSTREAM_TESTSUITE')) { - $this->isTokenStreamTestSuite = TRUE; - } - } - - /** - * Returns the default instance. - * - * @return PHP_CodeCoverage - */ - public static function getInstance() - { - if (self::$instance === NULL) { - // @codeCoverageIgnoreStart - self::$instance = new PHP_CodeCoverage; - } - // @codeCoverageIgnoreEnd - return self::$instance; - } - - /** - * Start collection of code coverage information. - * - * @param mixed $id - * @param boolean $clear - * @throws InvalidArgumentException - */ - public function start($id, $clear = FALSE) - { - if (!is_bool($clear)) { - throw new InvalidArgumentException; - } - - if ($clear) { - $this->clear(); - } - - $this->currentId = $id; - - $this->driver->start(); - } - - /** - * Stop collection of code coverage information. - * - * @param boolean $append - * @return array - * @throws InvalidArgumentException - */ - public function stop($append = TRUE) - { - if (!is_bool($append)) { - throw new InvalidArgumentException; - } - - $data = $this->driver->stop(); - - if ($append) { - $this->append($data); - } - - $this->currentId = NULL; - - return $data; - } - - /** - * Appends code coverage data. - * - * @param array $data - * @param mixed $id - * @param array $filterGroups - */ - public function append(array $data, $id = NULL, array $filterGroups = array('DEFAULT')) - { - if ($id === NULL) { - $id = $this->currentId; - } - - if ($id === NULL) { - throw new InvalidArgumentException; - } - - $this->applySelfFilter($data); - $this->applyListsFilter($data, $filterGroups); - $raw = $data; - $this->applyCoversAnnotationFilter($data, $id); - - if (!empty($data)) { - if ($id instanceof PHPUnit_Framework_TestCase) { - $status = $id->getStatus(); - $id = get_class($id) . '::' . $id->getName(); - $this->tests[$id] = $status; - } - - else if ($id instanceof PHPUnit_Extensions_PhptTestCase) { - $id = $id->getName(); - } - - $this->coveredFiles = array_unique( - array_merge($this->coveredFiles, array_keys($data)) - ); - - $this->data[$id] = array('filtered' => $data, 'raw' => $raw); - $this->summary = array(); - } - } - - /** - * Merges the data from another instance of PHP_CodeCoverage. - * - * @param PHP_CodeCoverage $that - */ - public function merge(PHP_CodeCoverage $that) - { - foreach ($that->data as $id => $data) { - if (!isset($this->data[$id])) { - $this->data[$id] = $data; - } else { - foreach (array('filtered', 'raw') as $type) { - foreach ($data[$type] as $file => $lines) { - if (!isset($this->data[$id][$type][$file])) { - $this->data[$id][$type][$file] = $lines; - } else { - foreach ($lines as $line => $flag) { - if (!isset($this->data[$id][$type][$file][$line]) || - $flag > $this->data[$id][$type][$file][$line]) { - $this->data[$id][$type][$file][$line] = $flag; - } - } - } - } - } - } - } - - foreach ($that->tests as $id => $status) { - if (!isset($this->tests[$id]) || $status > $this->tests[$id]) { - $this->tests[$id] = $status; - } - } - - $this->coveredFiles = array_unique( - array_merge($this->coveredFiles, $that->coveredFiles) - ); - - $this->summary = array(); - } - - /** - * Returns summarized code coverage data. - * - * Format of the result array: - * - * - * array( - * "/tested/code.php" => array( - * linenumber => array(tests that executed the line) - * ) - * ) - * - * - * @return array - */ - public function getSummary() - { - if (empty($this->summary)) { - if ($this->processUncoveredFilesFromWhitelist) { - $this->processUncoveredFilesFromWhitelist(); - } - - foreach ($this->data as $test => $coverage) { - foreach ($coverage['filtered'] as $file => $lines) { - foreach ($lines as $line => $flag) { - if ($flag == 1) { - if (!isset($this->summary[$file][$line][0])) { - $this->summary[$file][$line] = array(); - } - - if (isset($this->tests[$test])) { - $status = $this->tests[$test]; - } else { - $status = NULL; - } - - $this->summary[$file][$line][] = array( - 'id' => $test, 'status' => $status - ); - } - } - } - - foreach ($coverage['raw'] as $file => $lines) { - foreach ($lines as $line => $flag) { - if ($flag != 1 && - !isset($this->summary[$file][$line][0])) { - $this->summary[$file][$line] = $flag; - } - } - } - } - - foreach ($this->summary as &$file) { - ksort($file); - } - - ksort($this->summary); - } - - return $this->summary; - } - - /** - * Clears collected code coverage data. - */ - public function clear() - { - $this->data = array(); - $this->coveredFiles = array(); - $this->summary = array(); - $this->currentId = NULL; - } - - /** - * Returns the PHP_CodeCoverage_Filter used. - * - * @return PHP_CodeCoverage_Filter - */ - public function filter() - { - return $this->filter; - } - - /** - * @param boolean $flag - * @throws InvalidArgumentException - */ - public function setForceCoversAnnotation($flag) - { - if (!is_bool($flag)) { - throw new InvalidArgumentException; - } - - $this->forceCoversAnnotation = $flag; - } - - /** - * @param boolean $flag - * @throws InvalidArgumentException - */ - public function setMapTestClassNameToCoveredClassName($flag) - { - if (!is_bool($flag)) { - throw new InvalidArgumentException; - } - - $this->mapTestClassNameToCoveredClassName = $flag; - } - - /** - * @param boolean $flag - * @throws InvalidArgumentException - */ - public function setProcessUncoveredFilesFromWhitelist($flag) - { - if (!is_bool($flag)) { - throw new InvalidArgumentException; - } - - $this->processUncoveredFilesFromWhitelist = $flag; - } - - /** - * Filters sourcecode files from PHP_CodeCoverage, PHP_TokenStream, - * Text_Template, and File_Iterator. - * - * @param array $data - */ - protected function applySelfFilter(&$data) - { - foreach (array_keys($data) as $filename) { - if (!$this->filter->isFile($filename)) { - unset($data[$filename]); - continue; - } - - if (!$this->isCodeCoverageTestSuite && - strpos($filename, dirname(__FILE__)) === 0) { - unset($data[$filename]); - continue; - } - - if (!$this->isFileIteratorTestSuite && - (substr($filename, -17) == 'File/Iterator.php' || - substr($filename, -25) == 'File/Iterator/Factory.php')) { - unset($data[$filename]); - continue; - } - - if (!$this->isTimerTestSuite && - (substr($filename, -13) == 'PHP/Timer.php')) { - unset($data[$filename]); - continue; - } - - if (!$this->isTokenStreamTestSuite && - (substr($filename, -13) == 'PHP/Token.php' || - substr($filename, -20) == 'PHP/Token/Stream.php' || - substr($filename, -35) == 'PHP/Token/Stream/CachingFactory.php')) { - unset($data[$filename]); - continue; - } - - if (substr($filename, -17) == 'Text/Template.php') { - unset($data[$filename]); - } - } - } - - /** - * Applies the blacklist/whitelist filtering. - * - * @param array $data - * @param array $filterGroups - */ - protected function applyListsFilter(&$data, $filterGroups) - { - foreach (array_keys($data) as $filename) { - if ($this->filter->isFiltered($filename, $filterGroups)) { - unset($data[$filename]); - } - } - } - - /** - * Applies the @covers annotation filtering. - * - * @param array $data - * @param mixed $id - */ - protected function applyCoversAnnotationFilter(&$data, $id) - { - if ($id instanceof PHPUnit_Framework_TestCase) { - $testClassName = get_class($id); - $linesToBeCovered = PHP_CodeCoverage_Util::getLinesToBeCovered( - $testClassName, $id->getName() - ); - - if ($this->mapTestClassNameToCoveredClassName && - empty($linesToBeCovered)) { - $testedClass = substr($testClassName, 0, -4); - - if (class_exists($testedClass)) { - $class = new ReflectionClass($testedClass); - - $linesToBeCovered = array( - $class->getFileName() => range( - $class->getStartLine(), $class->getEndLine() - ) - ); - } - } - } else { - $linesToBeCovered = array(); - } - - if (!empty($linesToBeCovered)) { - $data = array_intersect_key($data, $linesToBeCovered); - - foreach (array_keys($data) as $filename) { - $data[$filename] = array_intersect_key( - $data[$filename], array_flip($linesToBeCovered[$filename]) - ); - } - } - - else if ($this->forceCoversAnnotation) { - $data = array(); - } - } - - /** - * Processes whitelisted files that are not covered. - */ - protected function processUncoveredFilesFromWhitelist() - { - $data = array(); - $includedFiles = array_flip(get_included_files()); - $uncoveredFiles = array_diff( - $this->filter->getWhitelist(), $this->coveredFiles - ); - - foreach ($uncoveredFiles as $uncoveredFile) { - if (isset($includedFiles[$uncoveredFile])) { - foreach (array_keys($this->data) as $test) { - if (isset($this->data[$test]['raw'][$uncoveredFile])) { - $coverage = $this->data[$test]['raw'][$uncoveredFile]; - - foreach (array_keys($coverage) as $key) { - if ($coverage[$key] == 1) { - $coverage[$key] = -1; - } - } - - $data[$uncoveredFile] = $coverage; - - break; - } - } - } else { - $this->driver->start(); - include_once $uncoveredFile; - $coverage = $this->driver->stop(); - - foreach ($coverage as $file => $fileCoverage) { - if (!isset($data[$file]) && - in_array($file, $uncoveredFiles)) { - foreach (array_keys($fileCoverage) as $key) { - if ($fileCoverage[$key] == 1) { - $fileCoverage[$key] = -1; - } - } - - $data[$file] = $fileCoverage; - $includedFiles[$file] = TRUE; - } - } - } - } - - $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); - } -} diff --git a/PHP/CodeCoverage/Driver.php b/PHP/CodeCoverage/Driver.php deleted file mode 100644 index 12efe56bc..000000000 --- a/PHP/CodeCoverage/Driver.php +++ /dev/null @@ -1,71 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -/** - * Interface for code coverage drivers. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -interface PHP_CodeCoverage_Driver -{ - /** - * Start collection of code coverage information. - */ - public function start(); - - /** - * Stop collection of code coverage information. - * - * @return array - */ - public function stop(); -} diff --git a/PHP/CodeCoverage/Driver/Xdebug.php b/PHP/CodeCoverage/Driver/Xdebug.php deleted file mode 100644 index 4247a3c7e..000000000 --- a/PHP/CodeCoverage/Driver/Xdebug.php +++ /dev/null @@ -1,91 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage/Driver.php'; - -if (version_compare(phpversion('xdebug'), '2.2.0-dev', '>=') && - !ini_get('xdebug.coverage_enable')) { - die("You need to set xdebug.coverage_enable=On in your php.ini.\n"); -} - -/** - * Driver for Xdebug's code coverage functionality. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Driver_Xdebug implements PHP_CodeCoverage_Driver -{ - /** - * Start collection of code coverage information. - */ - public function start() - { - // @codeCoverageIgnoreStart - xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); - // @codeCoverageIgnoreEnd - } - - /** - * Stop collection of code coverage information. - * - * @return array - */ - public function stop() - { - // @codeCoverageIgnoreStart - $codeCoverage = xdebug_get_code_coverage(); - xdebug_stop_code_coverage(); - - return $codeCoverage; - // @codeCoverageIgnoreEnd - } -} diff --git a/PHP/CodeCoverage/Filter.php b/PHP/CodeCoverage/Filter.php deleted file mode 100644 index 8386559d0..000000000 --- a/PHP/CodeCoverage/Filter.php +++ /dev/null @@ -1,334 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'File/Iterator/Factory.php'; - -/** - * Filter for blacklisting and whitelisting of code coverage information. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Filter -{ - /** - * Source files that are blacklisted. - * - * @var array - */ - protected $blacklistedFiles = array( - 'DEFAULT' => array() - ); - - /** - * Source files that are whitelisted. - * - * @var array - */ - protected $whitelistedFiles = array(); - - /** - * Default PHP_CodeCoverage object. - * - * @var PHP_CodeCoverage - */ - protected static $instance; - - /** - * Returns the default instance. - * - * @return PHP_CodeCoverage_Filter - */ - public static function getInstance() - { - if (self::$instance === NULL) { - // @codeCoverageIgnoreStart - self::$instance = new PHP_CodeCoverage_Filter; - } - // @codeCoverageIgnoreEnd - - return self::$instance; - } - - /** - * Adds a directory to the blacklist (recursively). - * - * @param string $directory - * @param string $suffix - * @param string $prefix - * @param string $group - */ - public function addDirectoryToBlacklist($directory, $suffix = '.php', $prefix = '', $group = 'DEFAULT') - { - $files = File_Iterator_Factory::getFileIterator( - $directory, $suffix, $prefix - ); - - foreach ($files as $file) { - $this->addFileToBlacklist($file->getPathName(), $group, FALSE); - } - } - - /** - * Adds a file to the blacklist. - * - * @param string $filename - * @param string $group - */ - public function addFileToBlacklist($filename, $group = 'DEFAULT') - { - $this->blacklistedFiles[$group][realpath($filename)] = TRUE; - } - - /** - * Adds files to the blacklist. - * - * @param array $files - * @param string $group - */ - public function addFilesToBlacklist(array $files, $group = 'DEFAULT') - { - foreach ($files as $file) { - $this->addFileToBlacklist($file, $group); - } - } - - /** - * Removes a directory from the blacklist (recursively). - * - * @param string $directory - * @param string $suffix - * @param string $prefix - * @param string $group - */ - public function removeDirectoryFromBlacklist($directory, $suffix = '.php', $prefix = '', $group = 'DEFAULT') - { - $files = File_Iterator_Factory::getFileIterator( - $directory, $suffix, $prefix - ); - - foreach ($files as $file) { - $this->removeFileFromBlacklist($file->getPathName(), $group); - } - } - - /** - * Removes a file from the blacklist. - * - * @param string $filename - * @param string $group - */ - public function removeFileFromBlacklist($filename, $group = 'DEFAULT') - { - $filename = realpath($filename); - - if (isset($this->blacklistedFiles[$group][$filename])) { - unset($this->blacklistedFiles[$group][$filename]); - } - } - - /** - * Adds a directory to the whitelist (recursively). - * - * @param string $directory - * @param string $suffix - * @param string $prefix - */ - public function addDirectoryToWhitelist($directory, $suffix = '.php', $prefix = '') - { - $files = File_Iterator_Factory::getFileIterator( - $directory, $suffix, $prefix - ); - - foreach ($files as $file) { - $this->addFileToWhitelist($file->getPathName(), FALSE); - } - } - - /** - * Adds a file to the whitelist. - * - * When the whitelist is empty (default), blacklisting is used. - * When the whitelist is not empty, whitelisting is used. - * - * @param string $filename - */ - public function addFileToWhitelist($filename) - { - $this->whitelistedFiles[realpath($filename)] = TRUE; - } - - /** - * Adds files to the whitelist. - * - * @param array $files - */ - public function addFilesToWhitelist(array $files) - { - foreach ($files as $file) { - $this->addFileToWhitelist($file); - } - } - - /** - * Removes a directory from the whitelist (recursively). - * - * @param string $directory - * @param string $suffix - * @param string $prefix - */ - public function removeDirectoryFromWhitelist($directory, $suffix = '.php', $prefix = '') - { - $files = File_Iterator_Factory::getFileIterator( - $directory, $suffix, $prefix - ); - - foreach ($files as $file) { - $this->removeFileFromWhitelist($file->getPathName()); - } - } - - /** - * Removes a file from the whitelist. - * - * @param string $filename - */ - public function removeFileFromWhitelist($filename) - { - $filename = realpath($filename); - - if (isset($this->whitelistedFiles[$filename])) { - unset($this->whitelistedFiles[$filename]); - } - } - - /** - * Checks whether a filename is a real filename. - * - * @param string $filename - */ - public static function isFile($filename) - { - if ($filename == '-' || - strpos($filename, 'eval()\'d code') !== FALSE || - strpos($filename, 'runtime-created function') !== FALSE || - strpos($filename, 'assert code') !== FALSE || - strpos($filename, 'regexp code') !== FALSE) { - return FALSE; - } - - return TRUE; - } - - /** - * Checks whether or not a file is filtered. - * - * When the whitelist is empty (default), blacklisting is used. - * When the whitelist is not empty, whitelisting is used. - * - * @param string $filename - * @param array $groups - * @param boolean $ignoreWhitelist - * @return boolean - * @throws InvalidArgumentException - */ - public function isFiltered($filename, array $groups = array('DEFAULT'), $ignoreWhitelist = FALSE) - { - if (!is_bool($ignoreWhitelist)) { - throw new InvalidArgumentException; - } - - $filename = realpath($filename); - - if (!$ignoreWhitelist && !empty($this->whitelistedFiles)) { - return !isset($this->whitelistedFiles[$filename]); - } - - $blacklistedFiles = array(); - - foreach ($groups as $group) { - if (isset($this->blacklistedFiles[$group])) { - $blacklistedFiles = array_merge( - $blacklistedFiles, - $this->blacklistedFiles[$group] - ); - } - } - - return isset($blacklistedFiles[$filename]); - } - - /** - * Returns the list of blacklisted files. - * - * @return array - */ - public function getBlacklist() - { - $blacklistedFiles = array(); - - foreach ($this->blacklistedFiles as $group => $list) { - $blacklistedFiles[$group] = array_keys($list); - } - - return $blacklistedFiles; - } - - /** - * Returns the list of whitelisted files. - * - * @return array - */ - public function getWhitelist() - { - return array_keys($this->whitelistedFiles); - } -} diff --git a/PHP/CodeCoverage/Report/Clover.php b/PHP/CodeCoverage/Report/Clover.php deleted file mode 100644 index 49be68f48..000000000 --- a/PHP/CodeCoverage/Report/Clover.php +++ /dev/null @@ -1,467 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage.php'; -require_once 'PHP/Token/Stream/CachingFactory.php'; - -/** - * Generates a Clover XML logfile from an PHP_CodeCoverage object. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_Clover -{ - /** - * @param PHP_CodeCoverage $coverage - * @param string $target - * @param string $name - * @return string - */ - public function process(PHP_CodeCoverage $coverage, $target = NULL, $name = NULL) - { - $document = new DOMDocument('1.0', 'UTF-8'); - $document->formatOutput = TRUE; - - $root = $document->createElement('coverage'); - $root->setAttribute('generated', (int)$_SERVER['REQUEST_TIME']); - $document->appendChild($root); - - $project = $document->createElement('project'); - $project->setAttribute('timestamp', (int)$_SERVER['REQUEST_TIME']); - - if (is_string($name)) { - $project->setAttribute('name', $name); - } - - $root->appendChild($project); - - $files = $coverage->getSummary(); - $packages = array(); - - $projectStatistics = array( - 'files' => 0, - 'loc' => 0, - 'ncloc' => 0, - 'classes' => 0, - 'methods' => 0, - 'coveredMethods' => 0, - 'conditionals' => 0, - 'coveredConditionals' => 0, - 'statements' => 0, - 'coveredStatements' => 0 - ); - - foreach ($files as $filename => $data) { - $namespace = 'global'; - - if (file_exists($filename)) { - $fileStatistics = array( - 'classes' => 0, - 'methods' => 0, - 'coveredMethods' => 0, - 'conditionals' => 0, - 'coveredConditionals' => 0, - 'statements' => 0, - 'coveredStatements' => 0 - ); - - $file = $document->createElement('file'); - $file->setAttribute('name', $filename); - - $tokens = PHP_Token_Stream_CachingFactory::get($filename); - $classesInFile = $tokens->getClasses(); - $linesOfCode = $tokens->getLinesOfCode(); - unset($tokens); - - $ignoredLines = PHP_CodeCoverage_Util::getLinesToBeIgnored( - $filename - ); - - $lines = array(); - - foreach ($classesInFile as $className => $_class) { - $classStatistics = array( - 'methods' => 0, - 'coveredMethods' => 0, - 'conditionals' => 0, - 'coveredConditionals' => 0, - 'statements' => 0, - 'coveredStatements' => 0 - ); - - foreach ($_class['methods'] as $methodName => $method) { - $classStatistics['methods']++; - - $methodCount = 0; - $methodLines = 0; - $methodLinesCovered = 0; - - for ($i = $method['startLine']; - $i <= $method['endLine']; - $i++) { - if (isset($ignoredLines[$i])) { - continue; - } - - $add = TRUE; - $count = 0; - - if (isset($files[$filename][$i])) { - if ($files[$filename][$i] != -2) { - $classStatistics['statements']++; - $methodLines++; - } - - if (is_array($files[$filename][$i])) { - $classStatistics['coveredStatements']++; - $methodLinesCovered++; - $count = count($files[$filename][$i]); - } - - else if ($files[$filename][$i] == -2) { - $add = FALSE; - } - } else { - $add = FALSE; - } - - $methodCount = max($methodCount, $count); - - if ($add) { - $lines[$i] = array( - 'count' => $count, - 'type' => 'stmt' - ); - } - } - - if ($methodCount > 0) { - $classStatistics['coveredMethods']++; - } - - $lines[$method['startLine']] = array( - 'count' => $methodCount, - 'crap' => PHP_CodeCoverage_Util::crap( - $method['ccn'], - PHP_CodeCoverage_Util::percent( - $methodLinesCovered, - $methodLines - ) - ), - 'type' => 'method', - 'name' => $methodName - ); - } - - $package = PHP_CodeCoverage_Util::getPackageInformation( - $className, $_class['docblock'] - ); - - if (!empty($package['namespace'])) { - $namespace = $package['namespace']; - } - - $class = $document->createElement('class'); - $class->setAttribute('name', $className); - $class->setAttribute('namespace', $namespace); - - if (!empty($package['fullPackage'])) { - $class->setAttribute( - 'fullPackage', $package['fullPackage'] - ); - } - - if (!empty($package['category'])) { - $class->setAttribute( - 'category', $package['category'] - ); - } - - if (!empty($package['package'])) { - $class->setAttribute( - 'package', $package['package'] - ); - } - - if (!empty($package['subpackage'])) { - $class->setAttribute( - 'subpackage', $package['subpackage'] - ); - } - - $file->appendChild($class); - - $metrics = $document->createElement('metrics'); - - $metrics->setAttribute( - 'methods', $classStatistics['methods'] - ); - - $metrics->setAttribute( - 'coveredmethods', $classStatistics['coveredMethods'] - ); - - $metrics->setAttribute( - 'conditionals', $classStatistics['conditionals'] - ); - - $metrics->setAttribute( - 'coveredconditionals', - $classStatistics['coveredConditionals'] - ); - - $metrics->setAttribute( - 'statements', $classStatistics['statements'] - ); - - $metrics->setAttribute( - 'coveredstatements', - $classStatistics['coveredStatements'] - ); - - $metrics->setAttribute( - 'elements', - $classStatistics['conditionals'] + - $classStatistics['statements'] + - $classStatistics['methods'] - ); - - $metrics->setAttribute( - 'coveredelements', - $classStatistics['coveredConditionals'] + - $classStatistics['coveredStatements'] + - $classStatistics['coveredMethods'] - ); - - $class->appendChild($metrics); - - $fileStatistics['methods'] += $classStatistics['methods']; - $fileStatistics['coveredMethods'] += $classStatistics['coveredMethods']; - $fileStatistics['conditionals'] += $classStatistics['conditionals']; - $fileStatistics['coveredConditionals'] += $classStatistics['coveredConditionals']; - $fileStatistics['statements'] += $classStatistics['statements']; - $fileStatistics['coveredStatements'] += $classStatistics['coveredStatements']; - $fileStatistics['classes']++; - } - - foreach ($data as $_line => $_data) { - if (isset($lines[$_line]) || isset($ignoredLines[$_line])) { - continue; - } - - if ($_data != -2) { - $fileStatistics['statements']++; - - if (is_array($_data)) { - $count = count($_data); - $fileStatistics['coveredStatements']++; - } else { - $count = 0; - } - - $lines[$_line] = array( - 'count' => $count, - 'type' => 'stmt' - ); - } - } - - ksort($lines); - - foreach ($lines as $_line => $_data) { - if (isset($ignoredLines[$_line])) { - continue; - } - - $line = $document->createElement('line'); - $line->setAttribute('num', $_line); - $line->setAttribute('type', $_data['type']); - - if (isset($_data['name'])) { - $line->setAttribute('name', $_data['name']); - } - - if (isset($_data['crap'])) { - $line->setAttribute('crap', $_data['crap']); - } - - $line->setAttribute('count', $_data['count']); - - $file->appendChild($line); - } - - $metrics = $document->createElement('metrics'); - - $metrics->setAttribute('loc', $linesOfCode['loc']); - $metrics->setAttribute('ncloc', $linesOfCode['ncloc']); - $metrics->setAttribute('classes', $fileStatistics['classes']); - $metrics->setAttribute('methods', $fileStatistics['methods']); - - $metrics->setAttribute( - 'coveredmethods', $fileStatistics['coveredMethods'] - ); - - $metrics->setAttribute( - 'conditionals', $fileStatistics['conditionals'] - ); - - $metrics->setAttribute( - 'coveredconditionals', $fileStatistics['coveredConditionals'] - ); - - $metrics->setAttribute( - 'statements', $fileStatistics['statements'] - ); - - $metrics->setAttribute( - 'coveredstatements', $fileStatistics['coveredStatements'] - ); - - $metrics->setAttribute( - 'elements', - $fileStatistics['conditionals'] + - $fileStatistics['statements'] + - $fileStatistics['methods'] - ); - - $metrics->setAttribute( - 'coveredelements', - $fileStatistics['coveredConditionals'] + - $fileStatistics['coveredStatements'] + - $fileStatistics['coveredMethods'] - ); - - $file->appendChild($metrics); - - if ($namespace == 'global') { - $project->appendChild($file); - } else { - if (!isset($packages[$namespace])) { - $packages[$namespace] = $document->createElement( - 'package' - ); - - $packages[$namespace]->setAttribute('name', $namespace); - $project->appendChild($packages[$namespace]); - } - - $packages[$namespace]->appendChild($file); - } - - $projectStatistics['loc'] += $linesOfCode['loc']; - $projectStatistics['ncloc'] += $linesOfCode['ncloc']; - $projectStatistics['classes'] += $fileStatistics['classes']; - $projectStatistics['methods'] += $fileStatistics['methods']; - $projectStatistics['coveredMethods'] += $fileStatistics['coveredMethods']; - $projectStatistics['conditionals'] += $fileStatistics['conditionals']; - $projectStatistics['coveredConditionals'] += $fileStatistics['coveredConditionals']; - $projectStatistics['statements'] += $fileStatistics['statements']; - $projectStatistics['coveredStatements'] += $fileStatistics['coveredStatements']; - $projectStatistics['files']++; - } - } - - $metrics = $document->createElement('metrics'); - - $metrics->setAttribute('files', $projectStatistics['files']); - $metrics->setAttribute('loc', $projectStatistics['loc']); - $metrics->setAttribute('ncloc', $projectStatistics['ncloc']); - $metrics->setAttribute('classes', $projectStatistics['classes']); - $metrics->setAttribute('methods', $projectStatistics['methods']); - - $metrics->setAttribute( - 'coveredmethods', $projectStatistics['coveredMethods'] - ); - - $metrics->setAttribute( - 'conditionals', $projectStatistics['conditionals'] - ); - - $metrics->setAttribute( - 'coveredconditionals', $projectStatistics['coveredConditionals'] - ); - - $metrics->setAttribute( - 'statements', $projectStatistics['statements'] - ); - - $metrics->setAttribute( - 'coveredstatements', $projectStatistics['coveredStatements'] - ); - - $metrics->setAttribute( - 'elements', - $projectStatistics['conditionals'] + - $projectStatistics['statements'] + - $projectStatistics['methods'] - ); - - $metrics->setAttribute( - 'coveredelements', - $projectStatistics['coveredConditionals'] + - $projectStatistics['coveredStatements'] + - $projectStatistics['coveredMethods'] - ); - - $project->appendChild($metrics); - - if ($target !== NULL) { - if (!is_dir(dirname($target))) { - mkdir(dirname($target), 0777, TRUE); - } - - return $document->save($target); - } else { - return $document->saveXML(); - } - } -} diff --git a/PHP/CodeCoverage/Report/HTML.php b/PHP/CodeCoverage/Report/HTML.php deleted file mode 100644 index cf9b46ac7..000000000 --- a/PHP/CodeCoverage/Report/HTML.php +++ /dev/null @@ -1,419 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage.php'; -require_once 'PHP/CodeCoverage/Report/HTML/Node.php'; -require_once 'Text/Template.php'; - -/** - * Generates an HTML report from an PHP_CodeCoverage object. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_HTML -{ - /** - * @var string - */ - public static $templatePath; - - /** - * @var array - */ - protected $options; - - /** - * Constructor. - * - * @param array $options - */ - public function __construct(array $options = array()) - { - if (!isset($options['title'])) { - $options['title'] = ''; - } - - if (!isset($options['charset'])) { - $options['charset'] = 'UTF-8'; - } - - if (!isset($options['yui'])) { - $options['yui'] = TRUE; - } - - if (!isset($options['highlight'])) { - $options['highlight'] = FALSE; - } - - if (!isset($options['lowUpperBound'])) { - $options['lowUpperBound'] = 35; - } - - if (!isset($options['highLowerBound'])) { - $options['highLowerBound'] = 70; - } - - if (!isset($options['generator'])) { - $options['generator'] = ''; - } - - $this->options = $options; - - self::$templatePath = sprintf( - '%s%sHTML%sTemplate%s', - - dirname(__FILE__), - DIRECTORY_SEPARATOR, - DIRECTORY_SEPARATOR, - DIRECTORY_SEPARATOR - ); - } - - /** - * @param PHP_CodeCoverage $coverage - * @param string $target - */ - public function process(PHP_CodeCoverage $coverage, $target) - { - $target = PHP_CodeCoverage_Util::getDirectory($target); - $files = $coverage->getSummary(); - $commonPath = PHP_CodeCoverage_Util::reducePaths($files); - $items = PHP_CodeCoverage_Util::buildDirectoryStructure($files); - $root = new PHP_CodeCoverage_Report_HTML_Node_Directory( - $commonPath, NULL - ); - - $this->addItems($root, $items); - - $this->renderDashboard( - $root, $target . 'index.dashboard.html', $this->options['title'] - ); - - foreach ($root as $node) { - if ($node instanceof PHP_CodeCoverage_Report_HTML_Node_Directory) { - $this->renderDashboard( - $node, - $target . PHP_CodeCoverage_Util::getSafeFilename( - $node->getId() - ) . '.dashboard.html', - $node->getName(TRUE) - ); - } - } - - $root->render( - $target, - $this->options['title'], - $this->options['charset'], - $this->options['lowUpperBound'], - $this->options['highLowerBound'], - $this->options['generator'] - ); - - $this->copyFiles($target); - } - - /** - * @param PHP_CodeCoverage_Report_HTML_Node_Directory $root - * @param string $file - * @param string $title - */ - protected function renderDashboard(PHP_CodeCoverage_Report_HTML_Node_Directory $root, $file, $title) - { - $classes = $this->classes($root); - $template = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . 'dashboard.html' - ); - - $template->setVar( - array( - 'title' => $title, - 'charset' => $this->options['charset'], - 'date' => date( - 'D M j G:i:s T Y', - $_SERVER['REQUEST_TIME'] - ), - 'version' => '@package_version@', - 'php_version' => PHP_VERSION, - 'generator' => $this->options['generator'], - 'least_tested_methods' => $this->leastTestedMethods($classes), - 'top_project_risks' => $this->topProjectRisks($classes), - 'cc_values' => $this->classComplexity($classes), - 'ccd_values' => $this->classCoverageDistribution($classes), - 'backlink' => basename(str_replace('.dashboard', '', $file)) - ) - ); - - $template->renderTo($file); - } - - /** - * @param PHP_CodeCoverage_Report_HTML_Node_Directory $root - * @param array $items - */ - protected function addItems(PHP_CodeCoverage_Report_HTML_Node_Directory $root, array $items) - { - foreach ($items as $key => $value) { - if (substr($key, -2) == '/f') { - try { - $root->addFile( - substr($key, 0, -2), - $value, - $this->options['yui'], - $this->options['highlight'] - ); - } - - catch (RuntimeException $e) { - continue; - } - } else { - $child = $root->addDirectory($key); - $this->addItems($child, $value); - } - } - } - - /** - * Returns the classes. - * - * @param PHP_CodeCoverage_Report_HTML_Node_Directory $root - * @return array - */ - protected function classes(PHP_CodeCoverage_Report_HTML_Node_Directory $root) - { - $classes = array(); - - foreach ($root as $node) { - if ($node instanceof PHP_CodeCoverage_Report_HTML_Node_File) { - $classes = array_merge($classes, $node->getClasses()); - } - } - - if (isset($classes['*'])) { - unset($classes['*']); - } - - return $classes; - } - - /** - * Returns the data for the Class Complexity chart. - * - * @param array $classes - * @return string - */ - protected function classComplexity(array $classes) - { - $data = array(); - - foreach ($classes as $name => $class) { - $data[] = array($class['coverage'], $class['ccn'], 'blue', $name); - } - - return json_encode($data); - } - - /** - * Returns the data for the Class Coverage Distribution chart. - * - * @param array $classes - * @return string - */ - protected function classCoverageDistribution(array $classes) - { - $data = array( - '0%' => 0, - '0-10%' => 0, - '10-20%' => 0, - '20-30%' => 0, - '30-40%' => 0, - '40-50%' => 0, - '50-60%' => 0, - '60-70%' => 0, - '70-80%' => 0, - '80-90%' => 0, - '90-100%' => 0, - '100%' => 0 - ); - - foreach ($classes as $class) { - if ($class['coverage'] == 0) { - $data['0%']++; - } - - else if ($class['coverage'] == 100) { - $data['100%']++; - } - - else { - $key = floor($class['coverage']/10)*10; - $key = $key . '-' . ($key + 10) . '%'; - $data[$key]++; - } - } - - return json_encode(array_values($data)); - } - - /** - * @param string $target - */ - protected function copyFiles($target) - { - $files = array( - 'butter.png', - 'chameleon.png', - 'close12_1.gif', - 'container.css', - 'container-min.js', - 'directory.png', - 'excanvas.compressed.js', - 'file.png', - 'glass.png', - 'RGraph.bar.js', - 'RGraph.common.core.js', - 'RGraph.common.tooltips.js', - 'RGraph.scatter.js', - 'scarlet_red.png', - 'snow.png', - 'style.css', - 'yahoo-dom-event.js' - ); - - foreach ($files as $file) { - copy(self::$templatePath . $file, $target . $file); - } - } - - /** - * Returns the least tested methods. - * - * @param array $classes - * @param integer $max - * @return string - */ - protected function leastTestedMethods(array $classes, $max = 10) - { - $methods = array(); - - foreach ($classes as $className => $class) { - foreach ($class['methods'] as $methodName => $method) { - if ($method['coverage'] < 100) { - if ($className != '*') { - $key = $className . '::' . $methodName; - } else { - $key = $methodName; - } - - $methods[$key] = $method['coverage']; - } - } - } - - asort($methods); - - $methods = array_slice($methods, 0, min($max, count($methods))); - $buffer = ''; - - foreach ($methods as $name => $coverage) { - list($class, $method) = explode('::', $name); - - $buffer .= sprintf( - '
  • %s (%d%%)
  • ' . "\n", - $classes[$class]['methods'][$method]['file'], - $name, - $coverage - ); - } - - return $buffer; - } - - /** - * Returns the top project risks according to the CRAP index. - * - * @param array $classes - * @param integer $max - * @return string - */ - protected function topProjectRisks(array $classes, $max = 10) - { - $risks = array(); - - foreach ($classes as $className => $class) { - if ($class['coverage'] < 100 && - $class['ccn'] > count($class['methods'])) { - $risks[$className] = $class['crap']; - } - } - - asort($risks); - - $risks = array_reverse( - array_slice($risks, 0, min($max, count($risks))) - ); - - $buffer = ''; - - foreach ($risks as $name => $crap) { - $buffer .= sprintf( - '
  • %s (%d)
  • ' . "\n", - $classes[$name]['file'], - $name, - $crap - ); - } - - return $buffer; - } -} diff --git a/PHP/CodeCoverage/Report/HTML/Node.php b/PHP/CodeCoverage/Report/HTML/Node.php deleted file mode 100644 index 5cefdf8da..000000000 --- a/PHP/CodeCoverage/Report/HTML/Node.php +++ /dev/null @@ -1,512 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -/** - * Base class for nodes in the code coverage information tree. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -abstract class PHP_CodeCoverage_Report_HTML_Node -{ - /** - * @var array - */ - protected $cache = array(); - - /** - * @var string - */ - protected $name; - - /** - * @var PHP_CodeCoverage_Report_HTML_Node - */ - protected $parent; - - /** - * Constructor. - * - * @param string $name - * @param PHP_CodeCoverage_Report_HTML_Node $parent - */ - public function __construct($name, PHP_CodeCoverage_Report_HTML_Node $parent = NULL) - { - $this->name = $name; - $this->parent = $parent; - } - - /** - * Returns the percentage of classes that has been tested. - * - * @return integer - */ - public function getTestedClassesPercent() - { - return PHP_CodeCoverage_Util::percent( - $this->getNumTestedClasses(), - $this->getNumClasses(), - TRUE - ); - } - - /** - * Returns the percentage of methods that has been tested. - * - * @return integer - */ - public function getTestedMethodsPercent() - { - return PHP_CodeCoverage_Util::percent( - $this->getNumTestedMethods(), - $this->getNumMethods(), - TRUE - ); - } - - /** - * Returns the percentage of executed lines. - * - * @return integer - */ - public function getLineExecutedPercent() - { - return PHP_CodeCoverage_Util::percent( - $this->getNumExecutedLines(), - $this->getNumExecutableLines(), - TRUE - ); - } - - /** - * Returns this node's ID. - * - * @return string - */ - public function getId() - { - if (!isset($this->cache['id'])) { - if ($this->parent === NULL) { - $this->cache['id'] = 'index'; - } else { - $parentId = $this->parent->getId(); - - if ($parentId == 'index') { - $this->cache['id'] = $this->getName(); - } else { - $this->cache['id'] = $parentId . '_' . $this->getName(); - } - } - } - - return $this->cache['id']; - } - - /** - * Returns this node's name. - * - * @param boolean $includeParent - * @return string - */ - public function getName($includeParent = FALSE, $includeCommonPath = FALSE) - { - if ($includeParent && $this->parent !== NULL) { - if (!isset($this->cache['nameIncludingParent'])) { - $parent = $this->parent->getName(TRUE); - - if (!empty($parent)) { - $this->cache['nameIncludingParent'] = $parent . '/' . - $this->name; - } else { - $this->cache['nameIncludingParent'] = $this->name; - } - } - - return $this->cache['nameIncludingParent']; - } else { - if ($this->parent !== NULL) { - return $this->name; - } else { - return $includeCommonPath ? $this->name : ''; - } - } - } - - /** - * Returns the link to this node. - * - * @param boolean $full - * @return string - */ - public function getLink($full) - { - if (substr($this->name, -1) == DIRECTORY_SEPARATOR) { - $name = substr($this->name, 0, -1); - } else { - $name = $this->name; - } - - $cleanId = PHP_CodeCoverage_Util::getSafeFilename($this->getId()); - - if ($full) { - if ($this->parent !== NULL) { - $parent = $this->parent->getLink(TRUE) . DIRECTORY_SEPARATOR; - } else { - $parent = ''; - } - - return sprintf( - '%s%s', - $parent, - $cleanId, - $name - ); - } else { - return sprintf( - '%s', - $cleanId, - $name - ); - } - } - - /** - * Returns this node's path. - * - * @return string - */ - public function getPath() - { - if (!isset($this->cache['path'])) { - if ($this->parent === NULL) { - $this->cache['path'] = $this->getName(FALSE, TRUE); - } else { - $parentPath = $this->parent->getPath(); - - if (substr($parentPath, -1) == DIRECTORY_SEPARATOR) { - $this->cache['path'] = $parentPath . - $this->getName(FALSE, TRUE); - } else { - $this->cache['path'] = $parentPath . - DIRECTORY_SEPARATOR . - $this->getName(FALSE, TRUE); - - if ($parentPath === '' && - realpath($this->cache['path']) === FALSE && - realpath($this->getName(FALSE, TRUE)) !== FALSE) { - $this->cache['path'] = $this->getName(FALSE, TRUE); - } - } - } - } - - return $this->cache['path']; - } - - protected function doRenderItemObject(PHP_CodeCoverage_Report_HTML_Node $item, $lowUpperBound, $highLowerBound, $link = NULL, $itemClass = 'coverItem') - { - return $this->doRenderItem( - array( - 'name' => $link != NULL ? $link : $item->getLink( - FALSE - ), - 'itemClass' => $itemClass, - 'numClasses' => $item->getNumClasses(), - 'numTestedClasses' => $item->getNumTestedClasses(), - 'testedClassesPercent' => $item->getTestedClassesPercent(), - 'numMethods' => $item->getNumMethods(), - 'numTestedMethods' => $item->getNumTestedMethods(), - 'testedMethodsPercent' => $item->getTestedMethodsPercent(), - 'numExecutableLines' => $item->getNumExecutableLines(), - 'numExecutedLines' => $item->getNumExecutedLines(), - 'executedLinesPercent' => $item->getLineExecutedPercent(), - 'crap' => $link == 'Total' ? 'CRAP' : '' - ), - $lowUpperBound, - $highLowerBound - ); - } - - protected function doRenderItem(array $data, $lowUpperBound, $highLowerBound, $template = NULL) - { - if ($template === NULL) { - if ($this instanceof PHP_CodeCoverage_Report_HTML_Node_Directory) { - $template = 'directory_item.html'; - } else { - $template = 'file_item.html'; - } - } - - $itemTemplate = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . $template - ); - - if ($data['numClasses'] > 0) { - list($classesColor, $classesLevel) = $this->getColorLevel( - $data['testedClassesPercent'], $lowUpperBound, $highLowerBound - ); - - $classesNumber = $data['numTestedClasses'] . ' / ' . - $data['numClasses']; - } else { - $classesColor = 'snow'; - $classesLevel = 'None'; - $classesNumber = ' '; - } - - if ($data['numMethods'] > 0) { - list($methodsColor, $methodsLevel) = $this->getColorLevel( - $data['testedMethodsPercent'], $lowUpperBound, $highLowerBound - ); - - $methodsNumber = $data['numTestedMethods'] . ' / ' . - $data['numMethods']; - } else { - $methodsColor = 'snow'; - $methodsLevel = 'None'; - $methodsNumber = ' '; - } - - list($linesColor, $linesLevel) = $this->getColorLevel( - $data['executedLinesPercent'], $lowUpperBound, $highLowerBound - ); - - if ($data['name'] == '*') { - $functions = TRUE; - } else { - $functions = FALSE; - } - - $icon = ''; - - if (isset($data['itemClass'])) { - if ($data['itemClass'] == 'coverDirectory') { - $icon = 'directory '; - } - - else if ($data['itemClass'] == 'coverFile') { - $icon = 'file '; - } - } - - $itemTemplate->setVar( - array( - 'name' => $functions ? 'Functions' : $data['name'], - 'icon' => $icon, - 'itemClass' => isset($data['itemClass']) ? $data['itemClass'] : 'coverItem', - 'classes_color' => $classesColor, - 'classes_level' => $functions ? 'None' : $classesLevel, - 'classes_tested_width' => floor($data['testedClassesPercent']), - 'classes_tested_percent' => !$functions && $data['numClasses'] > 0 ? $data['testedClassesPercent'] . '%' : ' ', - 'classes_not_tested_width' => 100 - floor($data['testedClassesPercent']), - 'classes_number' => $functions ? ' ' : $classesNumber, - 'methods_color' => $methodsColor, - 'methods_level' => $methodsLevel, - 'methods_tested_width' => floor($data['testedMethodsPercent']), - 'methods_tested_percent' => $data['numMethods'] > 0 ? $data['testedMethodsPercent'] . '%' : ' ', - 'methods_not_tested_width' => 100 - floor($data['testedMethodsPercent']), - 'methods_number' => $methodsNumber, - 'lines_color' => $linesColor, - 'lines_level' => $linesLevel, - 'lines_executed_width' => floor($data['executedLinesPercent']), - 'lines_executed_percent' => $data['executedLinesPercent'] . '%', - 'lines_not_executed_width' => 100 - floor($data['executedLinesPercent']), - 'num_executable_lines' => $data['numExecutableLines'], - 'num_executed_lines' => $data['numExecutedLines'], - 'crap' => isset($data['crap']) ? $data['crap'] : '' - ) - ); - - return $itemTemplate->render(); - } - - protected function getColorLevel($percent, $lowUpperBound, $highLowerBound) - { - $floorPercent = floor($percent); - - if ($floorPercent < $lowUpperBound) { - $color = 'scarlet_red'; - $level = 'Lo'; - } - - else if ($floorPercent >= $lowUpperBound && - $floorPercent < $highLowerBound) { - $color = 'butter'; - $level = 'Med'; - } - - else { - $color = 'chameleon'; - $level = 'Hi'; - } - - return array($color, $level); - } - - protected function renderTotalItem($lowUpperBound, $highLowerBound, $directory = TRUE) - { - if ($directory && - empty($this->directories) && - count($this->files) == 1) { - return ''; - } - - return $this->doRenderItemObject( - $this, $lowUpperBound, $highLowerBound, 'Total' - ) . - " \n" . - '  ' . - "\n \n"; - } - - /** - * @param Text_Template $template - * @param string $title - * @param string $charset - * @param string $generator - */ - protected function setTemplateVars(Text_Template $template, $title, $charset, $generator) - { - $dashboard = ''; - - if ($this instanceof PHP_CodeCoverage_Report_HTML_Node_Directory) { - $dashboard = sprintf( - 'dashboard', - PHP_CodeCoverage_Util::getSafeFilename( - $this->getId() - ) . '.dashboard.html' - ); - } - - $template->setVar( - array( - 'title' => $title, - 'charset' => $charset, - 'link' => $this->getLink(TRUE), - 'dashboard_link' => $dashboard, - 'num_executable_lines' => $this->getNumExecutableLines(), - 'num_executed_lines' => $this->getNumExecutedLines(), - 'lines_executed_percent' => $this->getLineExecutedPercent(), - 'date' => date( - 'D M j G:i:s T Y', - $_SERVER['REQUEST_TIME'] - ), - 'version' => '@package_version@', - 'php_version' => PHP_VERSION, - 'generator' => $generator - ) - ); - } - - /** - * Returns the classes of this node. - * - * @return array - */ - abstract public function getClasses(); - - /** - * Returns the number of executable lines. - * - * @return integer - */ - abstract public function getNumExecutableLines(); - - /** - * Returns the number of executed lines. - * - * @return integer - */ - abstract public function getNumExecutedLines(); - - /** - * Returns the number of classes. - * - * @return integer - */ - abstract public function getNumClasses(); - - /** - * Returns the number of tested classes. - * - * @return integer - */ - abstract public function getNumTestedClasses(); - - /** - * Returns the number of methods. - * - * @return integer - */ - abstract public function getNumMethods(); - - /** - * Returns the number of tested methods. - * - * @return integer - */ - abstract public function getNumTestedMethods(); - - /** - * Renders this node. - * - * @param string $target - * @param string $title - * @param string $charset - * @param integer $lowUpperBound - * @param integer $highLowerBound - * @param string $generator - */ - abstract public function render($target, $title, $charset = 'UTF-8', $lowUpperBound = 35, $highLowerBound = 70, $generator = ''); -} - -require_once 'PHP/CodeCoverage/Report/HTML/Node/Directory.php'; -require_once 'PHP/CodeCoverage/Report/HTML/Node/File.php'; diff --git a/PHP/CodeCoverage/Report/HTML/Node/Directory.php b/PHP/CodeCoverage/Report/HTML/Node/Directory.php deleted file mode 100644 index 4df4f947f..000000000 --- a/PHP/CodeCoverage/Report/HTML/Node/Directory.php +++ /dev/null @@ -1,430 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage/Report/HTML/Node/Iterator.php'; - -/** - * Represents a directory in the code coverage information tree. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_HTML_Node_Directory extends PHP_CodeCoverage_Report_HTML_Node implements IteratorAggregate -{ - /** - * @var PHP_CodeCoverage_Report_HTML_Node[] - */ - protected $children = array(); - - /** - * @var PHP_CodeCoverage_Report_HTML_Node_Directory[] - */ - protected $directories = array(); - - /** - * @var PHP_CodeCoverage_Report_HTML_Node_File[] - */ - protected $files = array(); - - /** - * @var array - */ - protected $classes; - - /** - * @var integer - */ - protected $numExecutableLines = -1; - - /** - * @var integer - */ - protected $numExecutedLines = -1; - - /** - * @var integer - */ - protected $numClasses = -1; - - /** - * @var integer - */ - protected $numTestedClasses = -1; - - /** - * @var integer - */ - protected $numMethods = -1; - - /** - * @var integer - */ - protected $numTestedMethods = -1; - - /** - * Returns an iterator for this node. - * - * @return RecursiveIteratorIterator - */ - public function getIterator() - { - return new RecursiveIteratorIterator( - new PHP_CodeCoverage_Report_HTML_Node_Iterator($this), - RecursiveIteratorIterator::SELF_FIRST - ); - } - - /** - * Adds a new directory. - * - * @return PHP_CodeCoverage_Report_HTML_Node_Directory - */ - public function addDirectory($name) - { - $directory = new PHP_CodeCoverage_Report_HTML_Node_Directory( - $name, $this - ); - - $this->children[] = $directory; - $this->directories[] = &$this->children[count($this->children) - 1]; - - return $directory; - } - - /** - * Adds a new file. - * - * @param string $name - * @param array $lines - * @param boolean $yui - * @param boolean $highlight - * @return PHP_CodeCoverage_Report_HTML_Node_File - * @throws RuntimeException - */ - public function addFile($name, array $lines, $yui, $highlight) - { - $file = new PHP_CodeCoverage_Report_HTML_Node_File( - $name, $this, $lines, $yui, $highlight - ); - - $this->children[] = $file; - $this->files[] = &$this->children[count($this->children) - 1]; - - $this->numExecutableLines = -1; - $this->numExecutedLines = -1; - - return $file; - } - - /** - * Returns the directories in this directory. - * - * @return array - */ - public function getDirectories() - { - return $this->directories; - } - - /** - * Returns the files in this directory. - * - * @return array - */ - public function getFiles() - { - return $this->files; - } - - /** - * Returns the child nodes of this node. - * - * @return array - */ - public function getChildNodes() - { - return $this->children; - } - - /** - * Returns the classes of this node. - * - * @return array - */ - public function getClasses() - { - if ($this->classes === NULL) { - $this->classes = array(); - - foreach ($this->children as $child) { - $this->classes = array_merge( - $this->classes, $child->getClasses() - ); - } - } - - return $this->classes; - } - - /** - * Returns the number of executable lines. - * - * @return integer - */ - public function getNumExecutableLines() - { - if ($this->numExecutableLines == -1) { - $this->numExecutableLines = 0; - - foreach ($this->children as $child) { - $this->numExecutableLines += $child->getNumExecutableLines(); - } - } - - return $this->numExecutableLines; - } - - /** - * Returns the number of executed lines. - * - * @return integer - */ - public function getNumExecutedLines() - { - if ($this->numExecutedLines == -1) { - $this->numExecutedLines = 0; - - foreach ($this->children as $child) { - $this->numExecutedLines += $child->getNumExecutedLines(); - } - } - - return $this->numExecutedLines; - } - - /** - * Returns the number of classes. - * - * @return integer - */ - public function getNumClasses() - { - if ($this->numClasses == -1) { - $this->numClasses = 0; - - foreach ($this->children as $child) { - $this->numClasses += $child->getNumClasses(); - } - } - - return $this->numClasses; - } - - /** - * Returns the number of tested classes. - * - * @return integer - */ - public function getNumTestedClasses() - { - if ($this->numTestedClasses == -1) { - $this->numTestedClasses = 0; - - foreach ($this->children as $child) { - $this->numTestedClasses += $child->getNumTestedClasses(); - } - } - - return $this->numTestedClasses; - } - - /** - * Returns the number of methods. - * - * @return integer - */ - public function getNumMethods() - { - if ($this->numMethods == -1) { - $this->numMethods = 0; - - foreach ($this->children as $child) { - $this->numMethods += $child->getNumMethods(); - } - } - - return $this->numMethods; - } - - /** - * Returns the number of tested methods. - * - * @return integer - */ - public function getNumTestedMethods() - { - if ($this->numTestedMethods == -1) { - $this->numTestedMethods = 0; - - foreach ($this->children as $child) { - $this->numTestedMethods += $child->getNumTestedMethods(); - } - } - - return $this->numTestedMethods; - } - - /** - * Renders this node. - * - * @param string $target - * @param string $title - * @param string $charset - * @param integer $lowUpperBound - * @param integer $highLowerBound - * @param string $generator - */ - public function render($target, $title, $charset = 'UTF-8', $lowUpperBound = 35, $highLowerBound = 70, $generator = '') - { - $this->doRender( - $target, $title, $charset, $lowUpperBound, $highLowerBound, $generator - ); - - foreach ($this->children as $child) { - $child->render( - $target, - $title, - $charset, - $lowUpperBound, - $highLowerBound, - $generator - ); - } - - $this->children = array(); - } - - /** - * @param string $target - * @param string $title - * @param string $charset - * @param integer $lowUpperBound - * @param integer $highLowerBound - * @param string $generator - */ - protected function doRender($target, $title, $charset, $lowUpperBound, $highLowerBound, $generator) - { - $cleanId = PHP_CodeCoverage_Util::getSafeFilename($this->getId()); - $file = $target . $cleanId . '.html'; - - $template = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . 'directory.html' - ); - - $this->setTemplateVars($template, $title, $charset, $generator); - - $template->setVar( - array( - 'total_item' => $this->renderTotalItem( - $lowUpperBound, $highLowerBound - ), - 'items' => $this->renderItems( - $lowUpperBound, $highLowerBound - ), - 'low_upper_bound' => $lowUpperBound, - 'high_lower_bound' => $highLowerBound - ) - ); - - $template->renderTo($file); - - $this->directories = array(); - $this->files = array(); - } - - /** - * @param float $lowUpperBound - * @param float $highLowerBound - * @return string - */ - protected function renderItems($lowUpperBound, $highLowerBound) - { - $items = $this->doRenderItems( - $this->directories, $lowUpperBound, $highLowerBound, 'coverDirectory' - ); - - $items .= $this->doRenderItems( - $this->files, $lowUpperBound, $highLowerBound, 'coverFile' - ); - - return $items; - } - - /** - * @param array $items - * @param float $lowUpperBound - * @param float $highLowerBound - * @param string $itemClass - * @return string - */ - protected function doRenderItems(array $items, $lowUpperBound, $highLowerBound, $itemClass) - { - $result = ''; - - foreach ($items as $item) { - $result .= $this->doRenderItemObject( - $item, $lowUpperBound, $highLowerBound, NULL, $itemClass - ); - } - - return $result; - } -} diff --git a/PHP/CodeCoverage/Report/HTML/Node/File.php b/PHP/CodeCoverage/Report/HTML/Node/File.php deleted file mode 100644 index 7643e940a..000000000 --- a/PHP/CodeCoverage/Report/HTML/Node/File.php +++ /dev/null @@ -1,916 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -if (!defined('T_NAMESPACE')) { - define('T_NAMESPACE', 377); -} - -require_once 'PHP/Token/Stream/CachingFactory.php'; - -/** - * Represents a file in the code coverage information tree. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_HTML_Node_File extends PHP_CodeCoverage_Report_HTML_Node -{ - /** - * @var array - */ - protected $codeLines; - - /** - * @var array - */ - protected $codeLinesFillup = array(); - - /** - * @var array - */ - protected $executedLines; - - /** - * @var boolean - */ - protected $yui = TRUE; - - /** - * @var boolean - */ - protected $highlight = FALSE; - - /** - * @var integer - */ - protected $numExecutableLines = 0; - - /** - * @var integer - */ - protected $numExecutedLines = 0; - - /** - * @var array - */ - protected $classes = array(); - - /** - * @var integer - */ - protected $numTestedClasses = 0; - - /** - * @var integer - */ - protected $numClasses = NULL; - - /** - * @var integer - */ - protected $numMethods = NULL; - - /** - * @var integer - */ - protected $numTestedMethods = NULL; - - /** - * @var string - */ - protected $yuiPanelJS = ''; - - /** - * @var array - */ - protected $startLines = array(); - - /** - * @var array - */ - protected $endLines = array(); - - /** - * Constructor. - * - * @param string $name - * @param PHP_CodeCoverage_Report_HTML_Node $parent - * @param array $executedLines - * @param boolean $yui - * @param boolean $highlight - * @throws RuntimeException - */ - public function __construct($name, PHP_CodeCoverage_Report_HTML_Node $parent, array $executedLines, $yui = TRUE, $highlight = FALSE) - { - parent::__construct($name, $parent); - - $path = $this->getPath(); - - if (!file_exists($path)) { - throw new RuntimeException( - sprintf('Path "%s" does not exist.', $path) - ); - } - - $this->executedLines = $executedLines; - $this->highlight = $highlight; - $this->yui = $yui; - $this->codeLines = $this->loadFile($path); - $this->ignoredLines = PHP_CodeCoverage_Util::getLinesToBeIgnored( - $path - ); - - $this->calculateStatistics(); - } - - /** - * Returns the classes of this node. - * - * @return array - */ - public function getClasses() - { - return $this->classes; - } - - /** - * Returns the number of executable lines. - * - * @return integer - */ - public function getNumExecutableLines() - { - return $this->numExecutableLines; - } - - /** - * Returns the number of executed lines. - * - * @return integer - */ - public function getNumExecutedLines() - { - return $this->numExecutedLines; - } - - /** - * Returns the number of classes. - * - * @return integer - */ - public function getNumClasses() - { - if ($this->numClasses === NULL) { - $this->numClasses = count($this->classes); - - if (isset($this->classes['*'])) { - $this->numClasses--; - } - } - - return $this->numClasses; - } - - /** - * Returns the number of tested classes. - * - * @return integer - */ - public function getNumTestedClasses() - { - return $this->numTestedClasses; - } - - /** - * Returns the number of methods. - * - * @return integer - */ - public function getNumMethods() - { - if ($this->numMethods === NULL) { - $this->numMethods = 0; - - foreach ($this->classes as $class) { - foreach ($class['methods'] as $method) { - if ($method['executableLines'] > 0) { - $this->numMethods++; - } - } - } - } - - return $this->numMethods; - } - - /** - * Returns the number of tested methods. - * - * @return integer - */ - public function getNumTestedMethods() - { - if ($this->numTestedMethods === NULL) { - $this->numTestedMethods = 0; - - foreach ($this->classes as $class) { - foreach ($class['methods'] as $method) { - if ($method['executableLines'] > 0 && - $method['coverage'] == 100) { - $this->numTestedMethods++; - } - } - } - } - - return $this->numTestedMethods; - } - - /** - * Renders this node. - * - * @param string $target - * @param string $title - * @param string $charset - * @param integer $lowUpperBound - * @param integer $highLowerBound - * @param string $generator - */ - public function render($target, $title, $charset = 'UTF-8', $lowUpperBound = 35, $highLowerBound = 70, $generator = '') - { - if ($this->yui) { - $template = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . 'file.html' - ); - - $yuiTemplate = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . 'yui_item.js' - ); - } else { - $template = new Text_Template( - PHP_CodeCoverage_Report_HTML::$templatePath . 'file_no_yui.html' - ); - } - - $i = 1; - $lines = ''; - - foreach ($this->codeLines as $line) { - $css = ''; - - if (!isset($this->ignoredLines[$i]) && - isset($this->executedLines[$i])) { - $count = ''; - - // Array: Line is executable and was executed. - // count(Array) = Number of tests that hit this line. - if (is_array($this->executedLines[$i])) { - $color = 'lineCov'; - $numTests = count($this->executedLines[$i]); - $count = sprintf('%8d', $numTests); - - if ($this->yui) { - $buffer = ''; - $testCSS = ''; - - foreach ($this->executedLines[$i] as $test) { - switch ($test['status']) { - case 0: { - $testCSS = ' class=\"testPassed\"'; - } - break; - - case 1: - case 2: { - $testCSS = ' class=\"testIncomplete\"'; - } - break; - - case 3: { - $testCSS = ' class=\"testFailure\"'; - } - break; - - case 4: { - $testCSS = ' class=\"testError\"'; - } - break; - - default: { - $testCSS = ''; - } - } - - $buffer .= sprintf( - '%s', - - $testCSS, - addslashes(htmlspecialchars($test['id'])) - ); - } - - if ($numTests > 1) { - $header = $numTests . ' tests cover'; - } else { - $header = '1 test covers'; - } - - $header .= ' line ' . $i; - - $yuiTemplate->setVar( - array( - 'line' => $i, - 'header' => $header, - 'tests' => $buffer - ), - FALSE - ); - - $this->yuiPanelJS .= $yuiTemplate->render(); - } - } - - // -1: Line is executable and was not executed. - else if ($this->executedLines[$i] == -1) { - $color = 'lineNoCov'; - $count = sprintf('%8d', 0); - } - - // -2: Line is dead code. - else { - $color = 'lineDeadCode'; - $count = ' '; - } - - $css = sprintf( - ' %s : ', - - $color, - $count - ); - } - - $fillup = array_shift($this->codeLinesFillup); - - if ($fillup > 0) { - $line .= str_repeat(' ', $fillup); - } - - $lines .= sprintf( - ''. - '%8d %s%s%s' . "\n", - - $i, - $i, - $i, - $i, - $i, - !empty($css) ? $css : ' : ', - !$this->highlight ? htmlspecialchars($line) : $line, - !empty($css) ? '' : '' - ); - - $i++; - } - - $items = ''; - - foreach ($this->classes as $className => $classData) { - if ($classData['executedLines'] == $classData['executableLines']) { - $numTestedClasses = 1; - $testedClassesPercent = 100; - } else { - $numTestedClasses = 0; - $testedClassesPercent = 0; - } - - $numMethods = 0; - $numTestedMethods = 0; - - foreach ($classData['methods'] as $method) { - if ($method['executableLines'] > 0) { - $numMethods++; - - if ($method['executedLines'] == $method['executableLines']) { - $numTestedMethods++; - } - } - } - - $items .= $this->doRenderItem( - array( - 'name' => sprintf( - '%s', - - $classData['startLine'], - $className - ), - 'numClasses' => 1, - 'numTestedClasses' => $numTestedClasses, - 'testedClassesPercent' => sprintf( - '%01.2f', $testedClassesPercent - ), - 'numMethods' => $numMethods, - 'numTestedMethods' => $numTestedMethods, - 'testedMethodsPercent' => PHP_CodeCoverage_Util::percent( - $numTestedMethods, $numMethods, TRUE - ), - 'numExecutableLines' => $classData['executableLines'], - 'numExecutedLines' => $classData['executedLines'], - 'executedLinesPercent' => PHP_CodeCoverage_Util::percent( - $classData['executedLines'], - $classData['executableLines'], - TRUE - ) - ), - $lowUpperBound, - $highLowerBound - ); - - foreach ($classData['methods'] as $methodData) { - if ($methodData['executableLines'] > 0) { - if ($methodData['executedLines'] == $methodData['executableLines']) { - $numTestedMethods = 1; - $testedMethodsPercent = 100; - } else { - $numTestedMethods = 0; - $testedMethodsPercent = 0; - } - - $items .= $this->doRenderItem( - array( - 'name' => sprintf( - ' %s', - - $methodData['startLine'], - htmlspecialchars($methodData['signature']) - ), - 'numClasses' => '', - 'numTestedClasses' => '', - 'testedClassesPercent' => '', - 'numMethods' => 1, - 'numTestedMethods' => $numTestedMethods, - 'testedMethodsPercent' => sprintf( - '%01.2f', $testedMethodsPercent - ), - 'numExecutableLines' => $methodData['executableLines'], - 'numExecutedLines' => $methodData['executedLines'], - 'executedLinesPercent' => PHP_CodeCoverage_Util::percent( - $methodData['executedLines'], - $methodData['executableLines'], - TRUE - ), - 'crap' => PHP_CodeCoverage_Util::crap( - $methodData['ccn'], - PHP_CodeCoverage_Util::percent( - $methodData['executedLines'], - $methodData['executableLines'] - ) - ) - ), - $lowUpperBound, - $highLowerBound, - 'method_item.html' - ); - } - } - } - - $this->setTemplateVars($template, $title, $charset, $generator); - - $template->setVar( - array( - 'lines' => $lines, - 'total_item' => $this->renderTotalItem( - $lowUpperBound, $highLowerBound, FALSE - ), - 'items' => $items, - 'yuiPanelJS' => $this->yuiPanelJS - ) - ); - - $cleanId = PHP_CodeCoverage_Util::getSafeFilename($this->getId()); - $template->renderTo($target . $cleanId . '.html'); - - $this->yuiPanelJS = ''; - $this->executedLines = array(); - } - - /** - * Calculates coverage statistics for the file. - * - */ - protected function calculateStatistics() - { - $this->processClasses(); - $this->processFunctions(); - - $max = count($this->codeLines); - - for ($lineNumber = 1; $lineNumber <= $max; $lineNumber++) { - if (isset($this->startLines[$lineNumber])) { - // Start line of a class. - if (isset($this->startLines[$lineNumber]['methods'])) { - $currentClass = &$this->startLines[$lineNumber]; - } - - // Start line of a method. - else { - $currentMethod = &$this->startLines[$lineNumber]; - } - } - - if (isset($this->executedLines[$lineNumber])) { - // Array: Line is executable and was executed. - if (is_array($this->executedLines[$lineNumber])) { - if (isset($currentClass)) { - $currentClass['executableLines']++; - $currentClass['executedLines']++; - } - - if (isset($currentMethod)) { - $currentMethod['executableLines']++; - $currentMethod['executedLines']++; - } - - $this->numExecutableLines++; - $this->numExecutedLines++; - } - - // -1: Line is executable and was not executed. - else if ($this->executedLines[$lineNumber] == -1) { - if (isset($currentClass)) { - $currentClass['executableLines']++; - } - - if (isset($currentMethod)) { - $currentMethod['executableLines']++; - } - - $this->numExecutableLines++; - - if (isset($this->ignoredLines[$lineNumber])) { - if (isset($currentClass)) { - $currentClass['executedLines']++; - } - - if (isset($currentMethod)) { - $currentMethod['executedLines']++; - } - - $this->numExecutedLines++; - } - } - } - - if (isset($this->endLines[$lineNumber])) { - // End line of a class. - if (isset($this->endLines[$lineNumber]['methods'])) { - unset($currentClass); - } - - // End line of a method. - else { - unset($currentMethod); - } - } - } - - foreach ($this->classes as $className => &$class) { - foreach ($class['methods'] as &$method) { - if ($method['executableLines'] > 0) { - $method['coverage'] = ($method['executedLines'] / - $method['executableLines']) * 100; - } else { - $method['coverage'] = 100; - } - - $method['crap'] = PHP_CodeCoverage_Util::crap( - $method['ccn'], $method['coverage'] - ); - - $class['ccn'] += $method['ccn']; - } - - if ($className != '*') { - if ($class['executableLines'] > 0) { - $class['coverage'] = ($class['executedLines'] / - $class['executableLines']) * 100; - } else { - $class['coverage'] = 100; - } - - if ($class['coverage'] == 100) { - $this->numTestedClasses++; - } - - $class['crap'] = PHP_CodeCoverage_Util::crap( - $class['ccn'], $class['coverage'] - ); - } - } - } - - /** - * @param string $file - * @return array - * @author Aidan Lister - */ - protected function loadFile($file) - { - $buffer = file_get_contents($file); - $lines = explode("\n", str_replace("\t", ' ', $buffer)); - $result = array(); - - if (count($lines) == 0) { - return $result; - } - - $lines = array_map('rtrim', $lines); - $linesLength = array_map('strlen', $lines); - $width = max($linesLength); - - foreach ($linesLength as $line => $length) { - $this->codeLinesFillup[$line] = $width - $length; - } - - if (!$this->highlight) { - unset($lines[count($lines)-1]); - return $lines; - } - - $tokens = token_get_all($buffer); - $stringFlag = FALSE; - $i = 0; - $result[$i] = ''; - - foreach ($tokens as $j => $token) { - if (is_string($token)) { - if ($token === '"' && $tokens[$j - 1] !== '\\') { - $result[$i] .= sprintf( - '%s', - - htmlspecialchars($token) - ); - - $stringFlag = !$stringFlag; - } else { - $result[$i] .= sprintf( - '%s', - - htmlspecialchars($token) - ); - } - - continue; - } - - list ($token, $value) = $token; - - $value = str_replace( - array("\t", ' '), - array('    ', ' '), - htmlspecialchars($value) - ); - - if ($value === "\n") { - $result[++$i] = ''; - } else { - $lines = explode("\n", $value); - - foreach ($lines as $jj => $line) { - $line = trim($line); - - if ($line !== '') { - if ($stringFlag) { - $colour = 'string'; - } else { - switch ($token) { - case T_INLINE_HTML: { - $colour = 'html'; - } - break; - - case T_COMMENT: - case T_DOC_COMMENT: { - $colour = 'comment'; - } - break; - - case T_ABSTRACT: - case T_ARRAY: - case T_ARRAY_CAST: - case T_AS: - case T_BOOLEAN_AND: - case T_BOOLEAN_OR: - case T_BOOL_CAST: - case T_BREAK: - case T_CASE: - case T_CATCH: - case T_CLASS: - case T_CLONE: - case T_CONCAT_EQUAL: - case T_CONTINUE: - case T_DEFAULT: - case T_DOUBLE_ARROW: - case T_DOUBLE_CAST: - case T_ECHO: - case T_ELSE: - case T_ELSEIF: - case T_EMPTY: - case T_ENDDECLARE: - case T_ENDFOR: - case T_ENDFOREACH: - case T_ENDIF: - case T_ENDSWITCH: - case T_ENDWHILE: - case T_END_HEREDOC: - case T_EXIT: - case T_EXTENDS: - case T_FINAL: - case T_FOREACH: - case T_FUNCTION: - case T_GLOBAL: - case T_IF: - case T_INC: - case T_INCLUDE: - case T_INCLUDE_ONCE: - case T_INSTANCEOF: - case T_INT_CAST: - case T_ISSET: - case T_IS_EQUAL: - case T_IS_IDENTICAL: - case T_IS_NOT_IDENTICAL: - case T_IS_SMALLER_OR_EQUAL: - case T_NAMESPACE: - case T_NEW: - case T_OBJECT_CAST: - case T_OBJECT_OPERATOR: - case T_PAAMAYIM_NEKUDOTAYIM: - case T_PRIVATE: - case T_PROTECTED: - case T_PUBLIC: - case T_REQUIRE: - case T_REQUIRE_ONCE: - case T_RETURN: - case T_SL: - case T_SL_EQUAL: - case T_SR: - case T_SR_EQUAL: - case T_START_HEREDOC: - case T_STATIC: - case T_STRING_CAST: - case T_THROW: - case T_TRY: - case T_UNSET_CAST: - case T_USE: - case T_VAR: - case T_WHILE: { - $colour = 'keyword'; - } - break; - - default: { - $colour = 'default'; - } - } - } - - $result[$i] .= sprintf( - '%s', - - $colour, - $line - ); - } - - if (isset($lines[$jj + 1])) { - $result[++$i] = ''; - } - } - } - } - - unset($result[count($result)-1]); - - return $result; - } - - protected function processClasses() - { - $file = $this->getId() . '.html#'; - $tokens = PHP_Token_Stream_CachingFactory::get($this->getPath()); - $classes = $tokens->getClasses(); - unset($tokens); - - foreach ($classes as $className => $class) { - $this->classes[$className] = array( - 'methods' => array(), - 'startLine' => $class['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => 0, - 'coverage' => 0, - 'crap' => 0, - 'file' => $file . $class['startLine'] - ); - - $this->startLines[$class['startLine']] = &$this->classes[$className]; - $this->endLines[$class['endLine']] = &$this->classes[$className]; - - foreach ($class['methods'] as $methodName => $method) { - $this->classes[$className]['methods'][$methodName] = array( - 'signature' => $method['signature'], - 'startLine' => $method['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => $method['ccn'], - 'coverage' => 0, - 'crap' => 0, - 'file' => $file . $method['startLine'] - ); - - $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName]; - $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName]; - } - } - } - - protected function processFunctions() - { - $tokens = PHP_Token_Stream_CachingFactory::get($this->getPath()); - $functions = $tokens->getFunctions(); - unset($tokens); - - if (count($functions) > 0 && !isset($this->classes['*'])) { - $this->classes['*'] = array( - 'methods' => array(), - 'startLine' => 0, - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => 0 - ); - } - - foreach ($functions as $functionName => $function) { - $this->classes['*']['methods'][$functionName] = array( - 'signature' => $function['signature'], - 'startLine' => $function['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => $function['ccn'] - ); - - $this->startLines[$function['startLine']] = &$this->classes['*']['methods'][$functionName]; - $this->endLines[$function['endLine']] = &$this->classes['*']['methods'][$functionName]; - } - } -} diff --git a/PHP/CodeCoverage/Report/HTML/Node/Iterator.php b/PHP/CodeCoverage/Report/HTML/Node/Iterator.php deleted file mode 100644 index c7be952ba..000000000 --- a/PHP/CodeCoverage/Report/HTML/Node/Iterator.php +++ /dev/null @@ -1,149 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -/** - * Recursive iterator for PHP_CodeCoverage_Report_HTML_Node object graphs. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_HTML_Node_Iterator implements RecursiveIterator -{ - /** - * @var integer - */ - protected $position; - - /** - * @var PHP_CodeCoverage_Report_HTML_Node[] - */ - protected $nodes; - - /** - * Constructor. - * - * @param PHP_CodeCoverage_Report_HTML_Node_Directory $node - */ - public function __construct(PHP_CodeCoverage_Report_HTML_Node_Directory $node) - { - $this->nodes = $node->getChildNodes(); - } - - /** - * Rewinds the Iterator to the first element. - * - */ - public function rewind() - { - $this->position = 0; - } - - /** - * Checks if there is a current element after calls to rewind() or next(). - * - * @return boolean - */ - public function valid() - { - return $this->position < count($this->nodes); - } - - /** - * Returns the key of the current element. - * - * @return integer - */ - public function key() - { - return $this->position; - } - - /** - * Returns the current element. - * - * @return PHPUnit_Framework_Test - */ - public function current() - { - return $this->valid() ? $this->nodes[$this->position] : NULL; - } - - /** - * Moves forward to next element. - * - */ - public function next() - { - $this->position++; - } - - /** - * Returns the sub iterator for the current element. - * - * @return PHP_CodeCoverage_Report_HTML_Node_Iterator - */ - public function getChildren() - { - return new PHP_CodeCoverage_Report_HTML_Node_Iterator( - $this->nodes[$this->position] - ); - } - - /** - * Checks whether the current element has children. - * - * @return boolean - */ - public function hasChildren() - { - return $this->nodes[$this->position] instanceof PHP_CodeCoverage_Report_HTML_Node_Directory; - } -} diff --git a/PHP/CodeCoverage/Report/HTML/Template/RGraph.bar.js b/PHP/CodeCoverage/Report/HTML/Template/RGraph.bar.js deleted file mode 100644 index 777ec2e4e..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/RGraph.bar.js +++ /dev/null @@ -1,1653 +0,0 @@ - /** - * o------------------------------------------------------------------------------o - * | This file is part of the RGraph package - you can learn more at: | - * | | - * | http://www.rgraph.net | - * | | - * | This package is licensed under the RGraph license. For all kinds of business | - * | purposes there is a small one-time licensing fee to pay and for non | - * | commercial purposes it is free to use. You can read the full license here: | - * | | - * | http://www.rgraph.net/LICENSE.txt | - * o------------------------------------------------------------------------------o - */ - - if (typeof(RGraph) == 'undefined') RGraph = {}; - - /** - * The bar chart constructor - * - * @param object canvas The canvas object - * @param array data The chart data - */ - RGraph.Bar = function (id, data) - { - // Get the canvas and context objects - this.id = id; - this.canvas = document.getElementById(id); - this.context = this.canvas.getContext ? this.canvas.getContext("2d") : null; - this.canvas.__object__ = this; - this.type = 'bar'; - this.max = 0; - this.stackedOrGrouped = false; - this.isRGraph = true; - - /** - * Compatibility with older browsers - */ - RGraph.OldBrowserCompat(this.context); - - - // Various config type stuff - this.properties = { - 'chart.background.barcolor1': 'rgba(0,0,0,0)', - 'chart.background.barcolor2': 'rgba(0,0,0,0)', - 'chart.background.grid': true, - 'chart.background.grid.color': '#ddd', - 'chart.background.grid.width': 1, - 'chart.background.grid.hsize': 20, - 'chart.background.grid.vsize': 20, - 'chart.background.grid.vlines': true, - 'chart.background.grid.hlines': true, - 'chart.background.grid.border': true, - 'chart.background.grid.autofit':false, - 'chart.background.grid.autofit.numhlines': 7, - 'chart.background.grid.autofit.numvlines': 20, - 'chart.ytickgap': 20, - 'chart.smallyticks': 3, - 'chart.largeyticks': 5, - 'chart.numyticks': 10, - 'chart.hmargin': 5, - 'chart.strokecolor': '#666', - 'chart.axis.color': 'black', - 'chart.gutter': 25, - 'chart.labels': null, - 'chart.labels.ingraph': null, - 'chart.labels.above': false, - 'chart.labels.above.decimals': 0, - 'chart.labels.above.size': null, - 'chart.ylabels': true, - 'chart.ylabels.count': 5, - 'chart.ylabels.inside': false, - 'chart.xlabels.offset': 0, - 'chart.xaxispos': 'bottom', - 'chart.yaxispos': 'left', - 'chart.text.color': 'black', - 'chart.text.size': 10, - 'chart.text.angle': 0, - 'chart.text.font': 'Verdana', - 'chart.ymax': null, - 'chart.title': '', - 'chart.title.background': null, - 'chart.title.hpos': null, - 'chart.title.vpos': null, - 'chart.title.xaxis': '', - 'chart.title.yaxis': '', - 'chart.title.xaxis.pos': 0.25, - 'chart.title.yaxis.pos': 0.25, - 'chart.colors': ['rgb(0,0,255)', '#0f0', '#00f', '#ff0', '#0ff', '#0f0'], - 'chart.grouping': 'grouped', - 'chart.variant': 'bar', - 'chart.shadow': false, - 'chart.shadow.color': '#666', - 'chart.shadow.offsetx': 3, - 'chart.shadow.offsety': 3, - 'chart.shadow.blur': 3, - 'chart.tooltips': null, - 'chart.tooltips.effect': 'fade', - 'chart.tooltips.css.class': 'RGraph_tooltip', - 'chart.tooltips.event': 'onclick', - 'chart.tooltips.coords.adjust': [0,0], - 'chart.tooltips.highlight': true, - 'chart.background.hbars': null, - - 'chart.key': [], - 'chart.key.background': 'white', - 'chart.key.position': 'graph', - 'chart.key.shadow': false, - 'chart.key.shadow.color': '#666', - 'chart.key.shadow.blur': 3, - 'chart.key.shadow.offsetx': 2, - 'chart.key.shadow.offsety': 2, - 'chart.key.position.gutter.boxed': true, - 'chart.key.position.x': null, - 'chart.key.position.y': null, - 'chart.key.color.shape': 'square', - 'chart.key.rounded': true, - 'chart.key.text.size': 10, - - 'chart.contextmenu': null, - 'chart.line': null, - 'chart.units.pre': '', - 'chart.units.post': '', - 'chart.scale.decimals': 0, - 'chart.scale.point': '.', - 'chart.scale.thousand': ',', - 'chart.crosshairs': false, - 'chart.crosshairs.color': '#333', - 'chart.linewidth': 1, - 'chart.annotatable': false, - 'chart.annotate.color': 'black', - 'chart.zoom.factor': 1.5, - 'chart.zoom.fade.in': true, - 'chart.zoom.fade.out': true, - 'chart.zoom.hdir': 'right', - 'chart.zoom.vdir': 'down', - 'chart.zoom.frames': 10, - 'chart.zoom.delay': 50, - 'chart.zoom.shadow': true, - 'chart.zoom.mode': 'canvas', - 'chart.zoom.thumbnail.width': 75, - 'chart.zoom.thumbnail.height': 75, - 'chart.zoom.background': true, - 'chart.resizable': false, - 'chart.adjustable': false - } - - // Check for support - if (!this.canvas) { - alert('[BAR] No canvas support'); - return; - } - - // Check the common library has been included - if (typeof(RGraph) == 'undefined') { - alert('[BAR] Fatal error: The common library does not appear to have been included'); - } - - /** - * Determine whether the chart will contain stacked or grouped bars - */ - for (i=0; i 0) { - - alert('[BAR] (' + this.id + ') Sorry, tooltips are not supported with dot or pyramid charts'); - } - - /** - * Stop the coords array from growing uncontrollably - */ - this.coords = []; - - /** - * Work out a few things. They need to be here because they depend on things you can change before you - * call Draw() but after you instantiate the object - */ - this.max = 0; - this.grapharea = this.canvas.height - ( (2 * this.gutter)); - this.halfgrapharea = this.grapharea / 2; - this.halfTextHeight = this.Get('chart.text.size') / 2; - - // Progressively Draw the chart - RGraph.background.Draw(this); - - - //If it's a sketch chart variant, draw the axes first - if (this.Get('chart.variant') == 'sketch') { - this.DrawAxes(); - this.Drawbars(); - } else { - this.Drawbars(); - this.DrawAxes(); - } - - this.DrawLabels(); - - - // Draw the key if necessary - if (this.Get('chart.key').length) { - RGraph.DrawKey(this, this.Get('chart.key'), this.Get('chart.colors')); - } - - - /** - * Setup the context menu if required - */ - if (this.Get('chart.contextmenu')) { - RGraph.ShowContext(this); - } - - - /** - * Is a line is defined, draw it - */ - var line = this.Get('chart.line'); - - if (line) { - - // Check the length of the data(s) - if (line.original_data[0].length != this.data.length) { - alert("[BAR] You're adding a line with a differing amount of data points to the bar chart - this is not permitted"); - } - - // Check the X axis positions - if (this.Get('chart.xaxispos') != line.Get('chart.xaxispos')) { - alert("[BAR] Using different X axis positions when combining the Bar and Line is not advised"); - } - - line.Set('chart.gutter', this.Get('chart.gutter')); - line.Set('chart.noaxes', true); - line.Set('chart.background.barcolor1', 'rgba(0,0,0,0)'); - line.Set('chart.background.barcolor2', 'rgba(0,0,0,0)'); - line.Set('chart.background.grid', false); - line.Set('chart.ylabels', false); - line.Set('chart.hmargin', (this.canvas.width - (2 * this.gutter)) / (line.original_data[0].length * 2)); - - // If a custom yMax is set, use that - if (this.Get('chart.ymax')) { - line.Set('chart.ymax', this.Get('chart.ymax')); - } - - line.Draw(); - } - - - /** - * Draw "in graph" labels - */ - if (this.Get('chart.labels.ingraph')) { - RGraph.DrawInGraphLabels(this); - } - - /** - * Draw crosschairs - */ - if (this.Get('chart.crosshairs')) { - RGraph.DrawCrosshairs(this); - } - - /** - * If the canvas is annotatable, do install the event handlers - */ - if (this.Get('chart.annotatable')) { - RGraph.Annotate(this); - } - - /** - * This bit shows the mini zoom window if requested - */ - if (this.Get('chart.zoom.mode') == 'thumbnail' || this.Get('chart.zoom.mode') == 'area') { - RGraph.ShowZoomWindow(this); - } - - - /** - * This function enables resizing - */ - if (this.Get('chart.resizable')) { - RGraph.AllowResizing(this); - } - - - /** - * This function enables adjusting - */ - if (this.Get('chart.adjustable')) { - RGraph.AllowAdjusting(this); - } - - /** - * Fire the RGraph ondraw event - */ - RGraph.FireCustomEvent(this, 'ondraw'); - } - - - /** - * Draws the charts axes - */ - RGraph.Bar.prototype.DrawAxes = function () - { - var gutter = this.gutter; - var xaxispos = this.Get('chart.xaxispos'); - var yaxispos = this.Get('chart.yaxispos'); - - this.context.beginPath(); - this.context.strokeStyle = this.Get('chart.axis.color'); - this.context.lineWidth = 1; - - // Draw the Y axis - if (yaxispos == 'right') { - this.context.moveTo(this.canvas.width - gutter, gutter); - this.context.lineTo(this.canvas.width - gutter, this.canvas.height - gutter); - } else { - this.context.moveTo(gutter, gutter); - this.context.lineTo(gutter, this.canvas.height - gutter); - } - - // Draw the X axis - this.context.moveTo(gutter, (xaxispos == 'center' ? this.canvas.height / 2 : this.canvas.height - gutter)); - this.context.lineTo(this.canvas.width - gutter, xaxispos == 'center' ? this.canvas.height / 2 : this.canvas.height - gutter); - - var numYTicks = this.Get('chart.numyticks'); - - // Draw the Y tickmarks - var yTickGap = (this.canvas.height - (2 * gutter)) / numYTicks; - var xpos = yaxispos == 'left' ? gutter : this.canvas.width - gutter; - - for (y=gutter; - xaxispos == 'center' ? y <= (this.canvas.height - gutter) : y < (this.canvas.height - gutter); - y += yTickGap) { - - if (xaxispos == 'center' && y == (this.canvas.height / 2)) continue; - - this.context.moveTo(xpos, y); - this.context.lineTo(xpos + (yaxispos == 'left' ? -3 : 3), y); - } - - // Draw the X tickmarks - xTickGap = (this.canvas.width - (2 * gutter) ) / this.data.length; - yStart = this.canvas.height - gutter; - yEnd = (this.canvas.height - gutter) + 3; - - //////////////// X TICKS //////////////// - - // Now move the Y start end positions down if the axis is set to center - if (xaxispos == 'center') { - yStart = (this.canvas.height / 2) + 3; - yEnd = (this.canvas.height / 2) - 3; - } - - for (x=gutter + (yaxispos == 'left' ? xTickGap : 0); x 0) { - RGraph.DrawBars(this); - } - - var variant = this.Get('chart.variant'); - - /** - * Draw the 3D axes is necessary - */ - if (variant == '3d') { - RGraph.Draw3DAxes(this); - } - - /** - * Get the variant once, and draw the bars, be they regular, stacked or grouped - */ - - // Get these variables outside of the loop - var xaxispos = this.Get('chart.xaxispos'); - var width = (this.canvas.width - (2 * gutter) ) / this.data.length; - var orig_height = height; - var hmargin = this.Get('chart.hmargin'); - var shadow = this.Get('chart.shadow'); - var shadowColor = this.Get('chart.shadow.color'); - var shadowBlur = this.Get('chart.shadow.blur'); - var shadowOffsetX = this.Get('chart.shadow.offsetx'); - var shadowOffsetY = this.Get('chart.shadow.offsety'); - var strokeStyle = this.Get('chart.strokecolor'); - var colors = this.Get('chart.colors'); - - for (i=0; i 0.4 ? -1 : 3) - (r * width),y - 1); - this.context.lineTo(x + hmargin + width - (r > 0.4 ? 1 : -1) - (r * width), y + height + (r == 0.2 ? 1 : -2)); - } - - this.context.stroke(); - - // Regular bar - } else if (variant == 'bar' || variant == '3d' || variant == 'glass') { - - if (document.all && shadow) { - this.DrawIEShadow([x + hmargin, y, barWidth, height]); - } - - if (variant == 'glass') { - RGraph.filledCurvyRect(this.context, x + hmargin, y, barWidth, height, 3, this.data[i] > 0, this.data[i] > 0, this.data[i] < 0, this.data[i] < 0); - RGraph.strokedCurvyRect(this.context, x + hmargin, y, barWidth, height, 3, this.data[i] > 0, this.data[i] > 0, this.data[i] < 0, this.data[i] < 0); - } else { - this.context.strokeRect(x + hmargin, y, barWidth, height); - this.context.fillRect(x + hmargin, y, barWidth, height); - } - - - // This bit draws the text labels that appear above the bars if requested - if (this.Get('chart.labels.above')) { - - // Turn off any shadow - if (shadow) { - RGraph.NoShadow(this); - } - - var yPos = y - 3; - - // Account for negative bars - if (this.data[i] < 0) { - yPos += height + 6 + (this.Get('chart.text.size') - 4); - } - - this.context.fillStyle = this.Get('chart.text.color'); - RGraph.Text(this.context, this.Get('chart.text.font'), typeof(this.Get('chart.labels.above.size')) == 'number' ? this.Get('chart.labels.above.size') : this.Get('chart.text.size') - 3, x + hmargin + (barWidth / 2), yPos, RGraph.number_format(this, Number(this.data[i]).toFixed(this.Get('chart.labels.above.decimals')),this.Get('chart.units.pre'), this.Get('chart.units.post')), null, 'center'); - } - - // 3D effect - if (variant == '3d') { - - var prevStrokeStyle = this.context.strokeStyle; - var prevFillStyle = this.context.fillStyle; - - // Draw the top - this.context.beginPath(); - this.context.moveTo(x + hmargin, y); - this.context.lineTo(x + hmargin + 10, y - 5); - this.context.lineTo(x + hmargin + 10 + barWidth, y - 5); - this.context.lineTo(x + hmargin + barWidth, y); - this.context.closePath(); - - this.context.stroke(); - this.context.fill(); - - // Draw the right hand side - this.context.beginPath(); - this.context.moveTo(x + hmargin + barWidth, y); - this.context.lineTo(x + hmargin + barWidth + 10, y - 5); - this.context.lineTo(x + hmargin + barWidth + 10, y + height - 5); - this.context.lineTo(x + hmargin + barWidth, y + height); - this.context.closePath(); - - this.context.stroke(); - this.context.fill(); - - // Draw the darker top section - this.context.beginPath(); - this.context.fillStyle = 'rgba(255,255,255,0.3)'; - this.context.moveTo(x + hmargin, y); - this.context.lineTo(x + hmargin + 10, y - 5); - this.context.lineTo(x + hmargin + 10 + barWidth, y - 5); - this.context.lineTo(x + hmargin + barWidth, y); - this.context.lineTo(x + hmargin, y); - this.context.closePath(); - - this.context.stroke(); - this.context.fill(); - - // Draw the darker right side section - this.context.beginPath(); - this.context.fillStyle = 'rgba(0,0,0,0.4)'; - this.context.moveTo(x + hmargin + barWidth, y); - this.context.lineTo(x + hmargin + barWidth + 10, y - 5); - this.context.lineTo(x + hmargin + barWidth + 10, y - 5 + height); - this.context.lineTo(x + hmargin + barWidth, y + height); - this.context.lineTo(x + hmargin + barWidth, y); - this.context.closePath(); - - this.context.stroke(); - this.context.fill(); - - this.context.strokeStyle = prevStrokeStyle; - this.context.fillStyle = prevFillStyle; - - // Glass variant - } else if (variant == 'glass') { - - var grad = this.context.createLinearGradient( - x + hmargin, - y, - x + hmargin + (barWidth / 2), - y - ); - grad.addColorStop(0, 'rgba(255,255,255,0.9)'); - grad.addColorStop(1, 'rgba(255,255,255,0.5)'); - - this.context.beginPath(); - this.context.fillStyle = grad; - this.context.fillRect(x + hmargin + 2,y + (this.data[i] > 0 ? 2 : 0),(barWidth / 2) - 2,height - 2); - this.context.fill(); - } - - // Dot chart - } else if (variant == 'dot') { - - this.context.beginPath(); - this.context.moveTo(x + (width / 2), y); - this.context.lineTo(x + (width / 2), y + height); - this.context.stroke(); - - this.context.beginPath(); - this.context.fillStyle = this.Get('chart.colors')[i]; - this.context.arc(x + (width / 2), y + (this.data[i] > 0 ? 0 : height), 2, 0, 6.28, 0); - - // Set the colour for the dots - this.context.fillStyle = this.Get('chart.colors')[0]; - - this.context.stroke(); - this.context.fill(); - - // Pyramid chart - } else if (variant == 'pyramid') { - - this.context.beginPath(); - var startY = (this.Get('chart.xaxispos') == 'center' ? (this.canvas.height / 2) : (this.canvas.height - this.Get('chart.gutter'))); - - this.context.moveTo(x + hmargin, startY); - this.context.lineTo( - x + hmargin + (barWidth / 2), - y + (this.Get('chart.xaxispos') == 'center' && (this.data[i] < 0) ? height : 0) - ); - this.context.lineTo(x + hmargin + barWidth, startY); - - this.context.closePath(); - - this.context.stroke(); - this.context.fill(); - - // Arrow chart - } else if (variant == 'arrow') { - var startY = (this.Get('chart.xaxispos') == 'center' ? (this.canvas.height / 2) : (this.canvas.height - this.gutter)); - - this.context.lineWidth = this.Get('chart.linewidth') ? this.Get('chart.linewidth') : 1; - this.context.lineCap = 'round'; - - this.context.beginPath(); - - this.context.moveTo(x + hmargin + (barWidth / 2), startY); - this.context.lineTo(x + hmargin + (barWidth / 2), y + (this.Get('chart.xaxispos') == 'center' && (this.data[i] < 0) ? height : 0)); - this.context.arc(x + hmargin + (barWidth / 2), - y + (this.Get('chart.xaxispos') == 'center' && (this.data[i] < 0) ? height : 0), - 5, - this.data[i] > 0 ? 0.78 : 5.6, - this.data[i] > 0 ? 0.79 : 5.48, - this.data[i] < 0); - - this.context.moveTo(x + hmargin + (barWidth / 2), y + (this.Get('chart.xaxispos') == 'center' && (this.data[i] < 0) ? height : 0)); - this.context.arc(x + hmargin + (barWidth / 2), - y + (this.Get('chart.xaxispos') == 'center' && (this.data[i] < 0) ? height : 0), - 5, - this.data[i] > 0 ? 2.355 : 4, - this.data[i] > 0 ? 2.4 : 3.925, - this.data[i] < 0); - - this.context.stroke(); - - this.context.lineWidth = 1; - - // Unknown variant type - } else { - alert('[BAR] Warning! Unknown chart.variant: ' + variant); - } - - this.coords.push([x + hmargin, y, width - (2 * hmargin), height]); - - - /** - * Stacked bar - */ - } else if (typeof(this.data[i]) == 'object' && this.Get('chart.grouping') == 'stacked') { - - var barWidth = width - (2 * hmargin); - var redrawCoords = [];// Necessary to draw if the shadow is enabled - var startY = 0; - - for (j=0; j 0) { - - /** - * Get the tooltip text - */ - if (typeof(obj.Get('chart.tooltips')) == 'function') { - var text = String(obj.Get('chart.tooltips')(barCoords[5])); - - } else if (typeof(obj.Get('chart.tooltips')) == 'object' && typeof(obj.Get('chart.tooltips')[barCoords[5]]) == 'function') { - var text = String(obj.Get('chart.tooltips')[barCoords[5]](barCoords[5])); - - } else if (typeof(obj.Get('chart.tooltips')) == 'object' && (typeof(obj.Get('chart.tooltips')[barCoords[5]]) == 'string' || typeof(obj.Get('chart.tooltips')[barCoords[5]]) == 'number')) { - var text = String(obj.Get('chart.tooltips')[barCoords[5]]); - - } else { - var text = null; - } - - if (text) { - canvas.style.cursor = 'pointer'; - } else { - canvas.style.cursor = 'default'; - } - - /** - * Hide the currently displayed tooltip if the index is the same - */ - if ( RGraph.Registry.Get('chart.tooltip') - && RGraph.Registry.Get('chart.tooltip').__canvas__.id != obj.id - && obj.Get('chart.tooltips.event') == 'onmousemove') { - - RGraph.Redraw(); - RGraph.HideTooltip(); - } - - /** - * This facilitates the tooltips using the onmousemove event - */ - - if ( obj.Get('chart.tooltips.event') == 'onmousemove' - && ( - (RGraph.Registry.Get('chart.tooltip') && RGraph.Registry.Get('chart.tooltip').__index__ != barCoords[5]) - || !RGraph.Registry.Get('chart.tooltip') - ) - && text) { - /** - * Show a tooltip if it's defined - */ - RGraph.Redraw(obj); - - obj.context.beginPath(); - obj.context.strokeStyle = 'black'; - obj.context.fillStyle = 'rgba(255,255,255,0.5)'; - obj.context.strokeRect(barCoords[1], barCoords[2], barCoords[3], barCoords[4]); - obj.context.fillRect(barCoords[1], barCoords[2], barCoords[3], barCoords[4]); - - obj.context.stroke(); - obj.context.fill(); - - RGraph.Tooltip(canvas, text, e.pageX, e.pageY, barCoords[5]); - } - } else { - canvas.style.cursor = 'default'; - } - } - RGraph.AddEventListener(this.id, 'mousemove', canvas_onmousemove); - this.canvas.addEventListener('mousemove', canvas_onmousemove, false); - - - /** - * Install the onclick event handler for the tooltips - */ - if (this.Get('chart.tooltips.event') == 'onclick') { - - canvas_onclick = function (e) - { - var e = RGraph.FixEventObject(e); - - // If the button pressed isn't the left, we're not interested - if (e.button != 0) return; - - e = RGraph.FixEventObject(e); - - var canvas = document.getElementById(this.id); - var obj = canvas.__object__; - var barCoords = obj.getBar(e); - - /** - * Redraw the graph first, in effect resetting the graph to as it was when it was first drawn - * This "deselects" any already selected bar - */ - RGraph.Redraw(); - - /** - * Loop through the bars determining if the mouse is over a bar - */ - if (barCoords) { - - /** - * Get the tooltip text - */ - if (typeof(obj.Get('chart.tooltips')) == 'function') { - var text = String(obj.Get('chart.tooltips')(barCoords[5])); - - } else if (typeof(obj.Get('chart.tooltips')) == 'object' && typeof(obj.Get('chart.tooltips')[barCoords[5]]) == 'function') { - var text = String(obj.Get('chart.tooltips')[barCoords[5]](barCoords[5])); - - } else if (typeof(obj.Get('chart.tooltips')) == 'object') { - var text = String(obj.Get('chart.tooltips')[barCoords[5]]); - - } else { - var text = null; - } - - /** - * Show a tooltip if it's defined - */ - if (text && text != 'undefined') { - - // [TODO] Allow customisation of the highlight colors - obj.context.beginPath(); - obj.context.strokeStyle = 'black'; - obj.context.fillStyle = 'rgba(255,255,255,0.5)'; - obj.context.strokeRect(barCoords[1], barCoords[2], barCoords[3], barCoords[4]); - obj.context.fillRect(barCoords[1], barCoords[2], barCoords[3], barCoords[4]); - - obj.context.stroke(); - obj.context.fill(); - - RGraph.Tooltip(canvas, text, e.pageX, e.pageY, barCoords[5]); - } - } - - /** - * Stop the event bubbling - */ - e.stopPropagation(); - } - RGraph.AddEventListener(this.id, 'click', canvas_onclick); - this.canvas.addEventListener('click', canvas_onclick, false); - } - - - // This resets the bar graph - // 8th August 2010 : Is this redundant - //if (typeof(obj) != 'undefined' && obj == RGraph.Registry.Get('chart.tooltip')) { - // obj.style.display = 'none'; - // RGraph.Registry.Set('chart.tooltip', null) - //} - } - } - - /** - * Draws the labels for the graph - */ - RGraph.Bar.prototype.DrawLabels = function () - { - var context = this.context; - var gutter = this.gutter; - var text_angle = this.Get('chart.text.angle'); - var text_size = this.Get('chart.text.size'); - var labels = this.Get('chart.labels'); - - - // Draw the Y axis labels: - if (this.Get('chart.ylabels')) { - this.Drawlabels_center(); - this.Drawlabels_bottom(); - } - - /** - * The X axis labels - */ - if (typeof(labels) == 'object' && labels) { - - var yOffset = 13 + Number(this.Get('chart.xlabels.offset')); - - /** - * Text angle - */ - var angle = 0; - var halign = 'center'; - - if (text_angle > 0) { - angle = -1 * text_angle; - halign = 'right'; - yOffset -= 5; - } - - // Draw the X axis labels - context.fillStyle = this.Get('chart.text.color'); - - // How wide is each bar - var barWidth = (this.canvas.width - (2 * gutter) ) / labels.length; - - // Reset the xTickGap - xTickGap = (this.canvas.width - (2 * gutter)) / labels.length - - // Draw the X tickmarks - var i=0; - var font = this.Get('chart.text.font'); - - for (x=gutter + (xTickGap / 2); x<=this.canvas.width - gutter; x+=xTickGap) { - RGraph.Text(context, font, - text_size, - x + (this.Get('chart.text.angle') == 90 ? 0: 0), - (this.canvas.height - gutter) + yOffset, - String(labels[i++]), - (this.Get('chart.text.angle') == 90 ? 'center' : null), - halign, - null, - angle); - } - } - } - - /** - * Draws the X axis in the middle - */ - RGraph.Bar.prototype.Drawlabels_center = function () - { - var font = this.Get('chart.text.font'); - var numYLabels = this.Get('chart.ylabels.count'); - - this.context.fillStyle = this.Get('chart.text.color'); - - if (this.Get('chart.xaxispos') == 'center') { - - /** - * Draw the top labels - */ - var interval = (this.grapharea * (1/10) ); - var text_size = this.Get('chart.text.size'); - var gutter = this.gutter; - var units_pre = this.Get('chart.units.pre'); - var units_post = this.Get('chart.units.post'); - var context = this.context; - var align = ''; - var xpos = 0; - var boxed = false; - - this.context.fillStyle = this.Get('chart.text.color'); - this.context.strokeStyle = 'black'; - - if (this.Get('chart.ylabels.inside') == true) { - var xpos = this.Get('chart.yaxispos') == 'left' ? gutter + 5 : this.canvas.width - gutter - 5; - var align = this.Get('chart.yaxispos') == 'left' ? 'left' : 'right'; - var boxed = true; - } else { - var xpos = this.Get('chart.yaxispos') == 'left' ? gutter - 5 : this.canvas.width - gutter + 5; - var align = this.Get('chart.yaxispos') == 'left' ? 'right' : 'left'; - var boxed = false; - } - - - - - - - - - - - - - /** - * Draw specific Y labels here so that the local variables can be reused - */ - if (typeof(this.Get('chart.ylabels.specific')) == 'object') { - - var labels = this.Get('chart.ylabels.specific'); - var grapharea = this.canvas.height - (2 * gutter); - - // Draw the top halves labels - for (var i=0; i=0; --i) { - var y = gutter + (grapharea * ( (i+1) / (labels.length * 2) )) + (grapharea / 2); - - RGraph.Text(context, font, text_size, xpos, y, labels[labels.length - i - 1], 'center', align, boxed); - } - - return; - } - - - - - - - - - - - - - if (numYLabels == 3 || numYLabels == 5) { - RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight, RGraph.number_format(this, this.scale[4], units_pre, units_post), null, align, boxed); - - if (numYLabels == 5) { - RGraph.Text(context, font, text_size, xpos, (1*interval) + gutter + this.halfTextHeight, RGraph.number_format(this, this.scale[3], units_pre, units_post), null, align, boxed); - RGraph.Text(context, font, text_size, xpos, (3*interval) + gutter + this.halfTextHeight, RGraph.number_format(this, this.scale[1], units_pre, units_post), null, align, boxed); - } - - if (numYLabels == 3 || numYLabels == 5) { - RGraph.Text(context, font, text_size, xpos, (4*interval) + gutter + this.halfTextHeight, RGraph.number_format(this, this.scale[0], units_pre, units_post), null, align, boxed); - RGraph.Text(context, font, text_size, xpos, (2*interval) + gutter + this.halfTextHeight, RGraph.number_format(this, this.scale[2], units_pre, units_post), null, align, boxed); - } - } else if (numYLabels == 10) { - // 10Y labels - interval = (this.grapharea / numYLabels) / 2; - - for (var i=0; i= (left + obj.Get('chart.tooltips.coords.adjust')[0]) - && mouseX <= (left + width+ obj.Get('chart.tooltips.coords.adjust')[0]) - && mouseY >= (top + obj.Get('chart.tooltips.coords.adjust')[1]) - && mouseY <= (top + height + obj.Get('chart.tooltips.coords.adjust')[1]) ) { - - return [obj, left, top, width, height, i]; - } - } - - return null; - } diff --git a/PHP/CodeCoverage/Report/HTML/Template/RGraph.common.core.js b/PHP/CodeCoverage/Report/HTML/Template/RGraph.common.core.js deleted file mode 100644 index b3b26b73e..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/RGraph.common.core.js +++ /dev/null @@ -1,2454 +0,0 @@ - /** - * o------------------------------------------------------------------------------o - * | This file is part of the RGraph package - you can learn more at: | - * | | - * | http://www.rgraph.net | - * | | - * | This package is licensed under the RGraph license. For all kinds of business | - * | purposes there is a small one-time licensing fee to pay and for non | - * | commercial purposes it is free to use. You can read the full license here: | - * | | - * | http://www.rgraph.net/LICENSE.txt | - * o------------------------------------------------------------------------------o - */ - - /** - * Initialise the various objects - */ - if (typeof(RGraph) == 'undefined') RGraph = {isRGraph:true,type:'common'}; - - - RGraph.Registry = {}; - RGraph.Registry.store = []; - RGraph.Registry.store['chart.event.handlers'] = []; - RGraph.background = {}; - RGraph.objects = []; - RGraph.Resizing = {}; - RGraph.events = []; - - - - /** - * Returns five values which are used as a nice scale - * - * @param max int The maximum value of the graph - * @param obj object The graph object - * @return array An appropriate scale - */ - RGraph.getScale = function (max, obj) - { - /** - * Special case for 0 - */ - if (max == 0) { - return ['0.2', '0.4', '0.6', '0.8', '1.0']; - } - - var original_max = max; - - /** - * Manually do decimals - */ - if (max <= 1) { - if (max > 0.5) { - return [0.2,0.4,0.6,0.8, Number(1).toFixed(1)]; - - } else if (max >= 0.1) { - return obj.Get('chart.scale.round') ? [0.2,0.4,0.6,0.8,1] : [0.1,0.2,0.3,0.4,0.5]; - - } else { - - var tmp = max; - var exp = 0; - - while (tmp < 1.01) { - exp += 1; - tmp *= 10; - } - - var ret = ['2e-' + exp, '4e-' + exp, '6e-' + exp, '8e-' + exp, '10e-' + exp]; - - - if (max <= ('5e-' + exp)) { - ret = ['1e-' + exp, '2e-' + exp, '3e-' + exp, '4e-' + exp, '5e-' + exp]; - } - - return ret; - } - } - - // Take off any decimals - if (String(max).indexOf('.') > 0) { - max = String(max).replace(/\.\d+$/, ''); - } - - var interval = Math.pow(10, Number(String(Number(max)).length - 1)); - var topValue = interval; - - while (topValue < max) { - topValue += (interval / 2); - } - - // Handles cases where the max is (for example) 50.5 - if (Number(original_max) > Number(topValue)) { - topValue += (interval / 2); - } - - // Custom if the max is greater than 5 and less than 10 - if (max < 10) { - topValue = (Number(original_max) <= 5 ? 5 : 10); - } - - /** - * Added 02/11/2010 to create "nicer" scales - */ - if (obj && typeof(obj.Get('chart.scale.round')) == 'boolean' && obj.Get('chart.scale.round')) { - topValue = 10 * interval; - } - - return [topValue * 0.2, topValue * 0.4, topValue * 0.6, topValue * 0.8, topValue]; - } - - - /** - * Returns the maximum value which is in an array - * - * @param array arr The array - * @param int Whether to ignore signs (ie negative/positive) - * @return int The maximum value in the array - */ - RGraph.array_max = function (arr) - { - var max = null; - - for (var i=0; i ' + RGraph.pr(obj[i], true, indent + ' ') + '\n'; - } - - var str = str + indent + ')'; - break; - - case 'function': - str += obj; - break; - - case 'boolean': - str += 'Boolean: ' + (obj ? 'true' : 'false'); - break; - } - - /** - * Finished, now either return if we're in a recursed call, or alert() - * if we're not. - */ - if (arguments[1]) { - return str; - } else { - alert(str); - } - } - - - /** - * The RGraph registry Set() function - * - * @param string name The name of the key - * @param mixed value The value to set - * @return mixed Returns the same value as you pass it - */ - RGraph.Registry.Set = function (name, value) - { - // Store the setting - RGraph.Registry.store[name] = value; - - // Don't really need to do this, but ho-hum - return value; - } - - - /** - * The RGraph registry Get() function - * - * @param string name The name of the particular setting to fetch - * @return mixed The value if exists, null otherwise - */ - RGraph.Registry.Get = function (name) - { - //return RGraph.Registry.store[name] == null ? null : RGraph.Registry.store[name]; - return RGraph.Registry.store[name]; - } - - - /** - * This function draws the background for the bar chart, line chart and scatter chart. - * - * @param object obj The graph object - */ - RGraph.background.Draw = function (obj) - { - var canvas = obj.canvas; - var context = obj.context; - var height = 0; - var gutter = obj.Get('chart.gutter'); - var variant = obj.Get('chart.variant'); - - context.fillStyle = obj.Get('chart.text.color'); - - // If it's a bar and 3D variant, translate - if (variant == '3d') { - context.save(); - context.translate(10, -5); - } - - // X axis title - if (typeof(obj.Get('chart.title.xaxis')) == 'string' && obj.Get('chart.title.xaxis').length) { - - var size = obj.Get('chart.text.size'); - var font = obj.Get('chart.text.font'); - - context.beginPath(); - RGraph.Text(context, font, size + 2, obj.canvas.width / 2, canvas.height - (gutter * obj.Get('chart.title.xaxis.pos')), obj.Get('chart.title.xaxis'), 'center', 'center', false, false, false, true); - context.fill(); - } - - // Y axis title - if (typeof(obj.Get('chart.title.yaxis')) == 'string' && obj.Get('chart.title.yaxis').length) { - - var size = obj.Get('chart.text.size'); - var font = obj.Get('chart.text.font'); - - context.beginPath(); - RGraph.Text(context, font, size + 2, gutter * obj.Get('chart.title.yaxis.pos'), canvas.height / 2, obj.Get('chart.title.yaxis'), 'center', 'center', false, 270, false, true); - context.fill(); - } - - obj.context.beginPath(); - - // Draw the horizontal bars - context.fillStyle = obj.Get('chart.background.barcolor1'); - height = (obj.canvas.height - obj.Get('chart.gutter')); - - for (var i=gutter; i < height ; i+=80) { - obj.context.fillRect(gutter, i, obj.canvas.width - (gutter * 2), Math.min(40, obj.canvas.height - gutter - i) ); - } - - context.fillStyle = obj.Get('chart.background.barcolor2'); - height = (obj.canvas.height - gutter); - - for (var i= (40 + gutter); i < height; i+=80) { - obj.context.fillRect(gutter, i, obj.canvas.width - (gutter * 2), i + 40 > (obj.canvas.height - gutter) ? obj.canvas.height - (gutter + i) : 40); - } - - context.stroke(); - - - // Draw the background grid - if (obj.Get('chart.background.grid')) { - - // If autofit is specified, use the .numhlines and .numvlines along with the width to work - // out the hsize and vsize - if (obj.Get('chart.background.grid.autofit')) { - var vsize = (canvas.width - (2 * obj.Get('chart.gutter')) - (obj.type == 'gantt' ? 2 * obj.Get('chart.gutter') : 0)) / obj.Get('chart.background.grid.autofit.numvlines'); - var hsize = (canvas.height - (2 * obj.Get('chart.gutter'))) / obj.Get('chart.background.grid.autofit.numhlines'); - - obj.Set('chart.background.grid.vsize', vsize); - obj.Set('chart.background.grid.hsize', hsize); - } - - context.beginPath(); - context.lineWidth = obj.Get('chart.background.grid.width') ? obj.Get('chart.background.grid.width') : 1; - context.strokeStyle = obj.Get('chart.background.grid.color'); - - // Draw the horizontal lines - if (obj.Get('chart.background.grid.hlines')) { - height = (canvas.height - gutter) - for (y=gutter; y < height; y+=obj.Get('chart.background.grid.hsize')) { - context.moveTo(gutter, y); - context.lineTo(canvas.width - gutter, y); - } - } - - if (obj.Get('chart.background.grid.vlines')) { - // Draw the vertical lines - var width = (canvas.width - gutter) - for (x=gutter + (obj.type == 'gantt' ? (2 * gutter) : 0); x<=width; x+=obj.Get('chart.background.grid.vsize')) { - context.moveTo(x, gutter); - context.lineTo(x, obj.canvas.height - gutter); - } - } - - if (obj.Get('chart.background.grid.border')) { - // Make sure a rectangle, the same colour as the grid goes around the graph - context.strokeStyle = obj.Get('chart.background.grid.color'); - context.strokeRect(gutter, gutter, canvas.width - (2 * gutter), canvas.height - (2 * gutter)); - } - } - - context.stroke(); - - // If it's a bar and 3D variant, translate - if (variant == '3d') { - context.restore(); - } - - // Draw the title if one is set - if ( typeof(obj.Get('chart.title')) == 'string') { - - if (obj.type == 'gantt') { - gutter /= 2; - } - - RGraph.DrawTitle(canvas, obj.Get('chart.title'), gutter, null, obj.Get('chart.text.size') + 2); - } - - context.stroke(); - } - - - /** - * Returns the day number for a particular date. Eg 1st February would be 32 - * - * @param object obj A date object - * @return int The day number of the given date - */ - RGraph.GetDays = function (obj) - { - var year = obj.getFullYear(); - var days = obj.getDate(); - var month = obj.getMonth(); - - if (month == 0) return days; - if (month >= 1) days += 31; - if (month >= 2) days += 28; - - // Leap years. Crude, but if this code is still being used - // when it stops working, then you have my permission to shoot - // me. Oh, you won't be able to - I'll be dead... - if (year >= 2008 && year % 4 == 0) days += 1; - - if (month >= 3) days += 31; - if (month >= 4) days += 30; - if (month >= 5) days += 31; - if (month >= 6) days += 30; - if (month >= 7) days += 31; - if (month >= 8) days += 31; - if (month >= 9) days += 30; - if (month >= 10) days += 31; - if (month >= 11) days += 30; - - return days; - } - - - - - - - - - - - - - - - - /** - * Draws the graph key (used by various graphs) - * - * @param object obj The graph object - * @param array key An array of the texts to be listed in the key - * @param colors An array of the colors to be used - */ - RGraph.DrawKey = function (obj, key, colors) - { - var canvas = obj.canvas; - var context = obj.context; - context.lineWidth = 1; - - context.beginPath(); - - /** - * Key positioned in the gutter - */ - var keypos = obj.Get('chart.key.position'); - var textsize = obj.Get('chart.text.size'); - var gutter = obj.Get('chart.gutter'); - - /** - * Change the older chart.key.vpos to chart.key.position.y - */ - if (typeof(obj.Get('chart.key.vpos')) == 'number') { - obj.Set('chart.key.position.y', obj.Get('chart.key.vpos') * gutter); - } - - if (keypos && keypos == 'gutter') { - - RGraph.DrawKey_gutter(obj, key, colors); - - - /** - * In-graph style key - */ - } else if (keypos && keypos == 'graph') { - - RGraph.DrawKey_graph(obj, key, colors); - - } else { - alert('[COMMON] (' + obj.id + ') Unknown key position: ' + keypos); - } - } - - - - - - /** - * This does the actual drawing of the key when it's in the graph - * - * @param object obj The graph object - * @param array key The key items to draw - * @param array colors An aray of colors that the key will use - */ - RGraph.DrawKey_graph = function (obj, key, colors) - { - var canvas = obj.canvas; - var context = obj.context; - var text_size = typeof(obj.Get('chart.key.text.size')) == 'number' ? obj.Get('chart.key.text.size') : obj.Get('chart.text.size'); - var text_font = obj.Get('chart.text.font'); - var gutter = obj.Get('chart.gutter'); - var hpos = obj.Get('chart.yaxispos') == 'right' ? gutter + 10 : canvas.width - gutter - 10; - var vpos = gutter + 10; - var title = obj.Get('chart.title'); - var blob_size = text_size; // The blob of color - var hmargin = 8; // This is the size of the gaps between the blob of color and the text - var vmargin = 4; // This is the vertical margin of the key - var fillstyle = obj.Get('chart.key.background'); - var strokestyle = 'black'; - var height = 0; - var width = 0; - - - // Need to set this so that measuring the text works out OK - context.font = text_size + 'pt ' + obj.Get('chart.text.font'); - - // Work out the longest bit of text - for (i=0; i=0; i--) { - var j = Number(i) + 1; - - // Draw the blob of color - if (obj.Get('chart.key.color.shape') == 'circle') { - context.beginPath(); - context.strokeStyle = 'rgba(0,0,0,0)'; - context.fillStyle = colors[i]; - context.arc(hpos + 5 + (blob_size / 2), vpos + (5 * j) + (text_size * j) - text_size + (blob_size / 2), blob_size / 2, 0, 6.26, 0); - context.fill(); - - } else if (obj.Get('chart.key.color.shape') == 'line') { - context.beginPath(); - context.strokeStyle = colors[i]; - context.moveTo(hpos + 5, vpos + (5 * j) + (text_size * j) - text_size + (blob_size / 2)); - context.lineTo(hpos + blob_size + 5, vpos + (5 * j) + (text_size * j) - text_size + (blob_size / 2)); - context.stroke(); - - } else { - context.fillStyle = colors[i]; - context.fillRect(hpos + 5, vpos + (5 * j) + (text_size * j) - text_size, text_size, text_size + 1); - } - - context.beginPath(); - - context.fillStyle = 'black'; - - RGraph.Text(context, - text_font, - text_size, - hpos + blob_size + 5 + 5, - vpos + (5 * j) + (text_size * j), - key[i]); - } - context.fill(); - } - - - - - - - /** - * This does the actual drawing of the key when it's in the gutter - * - * @param object obj The graph object - * @param array key The key items to draw - * @param array colors An aray of colors that the key will use - */ - RGraph.DrawKey_gutter = function (obj, key, colors) - { - var canvas = obj.canvas; - var context = obj.context; - var text_size = typeof(obj.Get('chart.key.text.size')) == 'number' ? obj.Get('chart.key.text.size') : obj.Get('chart.text.size'); - var text_font = obj.Get('chart.text.font'); - var gutter = obj.Get('chart.gutter'); - var hpos = canvas.width / 2; - var vpos = (gutter / 2) - 5; - var title = obj.Get('chart.title'); - var blob_size = text_size; // The blob of color - var hmargin = 8; // This is the size of the gaps between the blob of color and the text - var vmargin = 4; // This is the vertical margin of the key - var fillstyle = obj.Get('chart.key.background'); - var strokestyle = 'black'; - var length = 0; - - - - // Need to work out the length of the key first - context.font = text_size + 'pt ' + text_font; - for (i=0; i=0; i--) { - newarr.push(arr[i]); - } - - return newarr; - } - - - /** - * Formats a number with thousand seperators so it's easier to read - * - * @param integer num The number to format - * @param string The (optional) string to prepend to the string - * @param string The (optional) string to ap - * pend to the string - * @return string The formatted number - */ - RGraph.number_format = function (obj, num) - { - var i; - var prepend = arguments[2] ? String(arguments[2]) : ''; - var append = arguments[3] ? String(arguments[3]) : ''; - var output = ''; - var decimal = ''; - var decimal_seperator = obj.Get('chart.scale.point') ? obj.Get('chart.scale.point') : '.'; - var thousand_seperator = obj.Get('chart.scale.thousand') ? obj.Get('chart.scale.thousand') : ','; - RegExp.$1 = ''; - var i,j; - - // Ignore the preformatted version of "1e-2" - if (String(num).indexOf('e') > 0) { - return String(prepend + String(num) + append); - } - - // We need then number as a string - num = String(num); - - // Take off the decimal part - we re-append it later - if (num.indexOf('.') > 0) { - num = num.replace(/\.(.*)/, ''); - decimal = RegExp.$1; - } - - // Thousand seperator - //var seperator = arguments[1] ? String(arguments[1]) : ','; - var seperator = thousand_seperator; - - /** - * Work backwards adding the thousand seperators - */ - var foundPoint; - for (i=(num.length - 1),j=0; i>=0; j++,i--) { - var character = num.charAt(i); - - if ( j % 3 == 0 && j != 0) { - output += seperator; - } - - /** - * Build the output - */ - output += character; - } - - /** - * Now need to reverse the string - */ - var rev = output; - output = ''; - for (i=(rev.length - 1); i>=0; i--) { - output += rev.charAt(i); - } - - // Tidy up - output = output.replace(/^-,/, '-'); - - // Reappend the decimal - if (decimal.length) { - output = output + decimal_seperator + decimal; - decimal = ''; - RegExp.$1 = ''; - } - - // Minor bugette - if (output.charAt(0) == '-') { - output *= -1; - prepend = '-' + prepend; - } - - return prepend + output + append; - } - - - /** - * Draws horizontal coloured bars on something like the bar, line or scatter - */ - RGraph.DrawBars = function (obj) - { - var hbars = obj.Get('chart.background.hbars'); - - /** - * Draws a horizontal bar - */ - obj.context.beginPath(); - - for (i=0; i obj.max) { - hbars[i][1] = obj.max - hbars[i][0]; - } - - - // If height is negative, and the abs() value is greater than .max, use a negative max instead - if (Math.abs(hbars[i][1]) > obj.max) { - hbars[i][1] = -1 * obj.max; - } - - - // If start point is greater than max, change it to max - if (Math.abs(hbars[i][0]) > obj.max) { - hbars[i][0] = obj.max; - } - - // If start point plus height is less than negative max, use the negative max plus the start point - if (hbars[i][0] + hbars[i][1] < (-1 * obj.max) ) { - hbars[i][1] = -1 * (obj.max + hbars[i][0]); - } - - // If the X axis is at the bottom, and a negative max is given, warn the user - if (obj.Get('chart.xaxispos') == 'bottom' && (hbars[i][0] < 0 || (hbars[i][1] + hbars[i][1] < 0)) ) { - alert('[' + obj.type.toUpperCase() + ' (ID: ' + obj.id + ') BACKGROUND HBARS] You have a negative value in one of your background hbars values, whilst the X axis is in the center'); - } - - var ystart = (obj.grapharea - ((hbars[i][0] / obj.max) * obj.grapharea)); - var height = (Math.min(hbars[i][1], obj.max - hbars[i][0]) / obj.max) * obj.grapharea; - - // Account for the X axis being in the center - if (obj.Get('chart.xaxispos') == 'center') { - ystart /= 2; - height /= 2; - } - - ystart += obj.Get('chart.gutter') - - var x = obj.Get('chart.gutter'); - var y = ystart - height; - var w = obj.canvas.width - (2 * obj.Get('chart.gutter')); - var h = height; - - // Accommodate Opera :-/ - if (navigator.userAgent.indexOf('Opera') != -1 && obj.Get('chart.xaxispos') == 'center' && h < 0) { - h *= -1; - y = y - h; - } - - obj.context.fillStyle = hbars[i][2]; - obj.context.fillRect(x, y, w, h); - } - - obj.context.fill(); - } - - - /** - * Draws in-graph labels. - * - * @param object obj The graph object - */ - RGraph.DrawInGraphLabels = function (obj) - { - var canvas = obj.canvas; - var context = obj.context; - var labels = obj.Get('chart.labels.ingraph'); - var labels_processed = []; - - // Defaults - var fgcolor = 'black'; - var bgcolor = 'white'; - var direction = 1; - - if (!labels) { - return; - } - - /** - * Preprocess the labels array. Numbers are expanded - */ - for (var i=0; i 0) { - - for (var i=0; i 0) { - var x = (obj.type == 'bar' ? coords[0] + (coords[2] / 2) : coords[0]); - var y = (obj.type == 'bar' ? coords[1] + (coords[3] / 2) : coords[1]); - var length = typeof(labels_processed[i][4]) == 'number' ? labels_processed[i][4] : 25; - - context.beginPath(); - context.fillStyle = 'black'; - context.strokeStyle = 'black'; - - - if (obj.type == 'bar') { - - if (obj.Get('chart.variant') == 'dot') { - context.moveTo(x, obj.coords[i][1] - 5); - context.lineTo(x, obj.coords[i][1] - 5 - length); - - var text_x = x; - var text_y = obj.coords[i][1] - 5 - length; - - } else if (obj.Get('chart.variant') == 'arrow') { - context.moveTo(x, obj.coords[i][1] - 5); - context.lineTo(x, obj.coords[i][1] - 5 - length); - - var text_x = x; - var text_y = obj.coords[i][1] - 5 - length; - - } else { - - context.arc(x, y, 2.5, 0, 6.28, 0); - context.moveTo(x, y); - context.lineTo(x, y - length); - - var text_x = x; - var text_y = y - length; - } - - context.stroke(); - context.fill(); - - - } else if (obj.type == 'line') { - - if ( - typeof(labels_processed[i]) == 'object' && - typeof(labels_processed[i][3]) == 'number' && - labels_processed[i][3] == -1 - ) { - - context.moveTo(x, y + 5); - context.lineTo(x, y + 5 + length); - - context.stroke(); - context.beginPath(); - - // This draws the arrow - context.moveTo(x, y + 5); - context.lineTo(x - 3, y + 10); - context.lineTo(x + 3, y + 10); - context.closePath(); - - var text_x = x; - var text_y = y + 5 + length; - - } else { - - var text_x = x; - var text_y = y - 5 - length; - - context.moveTo(x, y - 5); - context.lineTo(x, y - 5 - length); - - context.stroke(); - context.beginPath(); - - // This draws the arrow - context.moveTo(x, y - 5); - context.lineTo(x - 3, y - 10); - context.lineTo(x + 3, y - 10); - context.closePath(); - } - - context.fill(); - } - - - // Taken out on the 10th Nov 2010 - unnecessary - //var width = context.measureText(labels[i]).width; - - context.beginPath(); - - // Fore ground color - context.fillStyle = (typeof(labels_processed[i]) == 'object' && typeof(labels_processed[i][1]) == 'string') ? labels_processed[i][1] : 'black'; - - RGraph.Text(context, - obj.Get('chart.text.font'), - obj.Get('chart.text.size'), - text_x, - text_y, - (typeof(labels_processed[i]) == 'object' && typeof(labels_processed[i][0]) == 'string') ? labels_processed[i][0] : labels_processed[i], - 'bottom', - 'center', - true, - null, - (typeof(labels_processed[i]) == 'object' && typeof(labels_processed[i][2]) == 'string') ? labels_processed[i][2] : 'white'); - context.fill(); - } - } - } - } - } - - - /** - * This function "fills in" key missing properties that various implementations lack - * - * @param object e The event object - */ - RGraph.FixEventObject = function (e) - { - if (RGraph.isIE8()) { - - var e = event; - - e.pageX = (event.clientX + document.body.scrollLeft); - e.pageY = (event.clientY + document.body.scrollTop); - e.target = event.srcElement; - - if (!document.body.scrollTop && document.documentElement.scrollTop) { - e.pageX += parseInt(document.documentElement.scrollLeft); - e.pageY += parseInt(document.documentElement.scrollTop); - } - } - - // This is mainly for FF which doesn't provide offsetX - if (typeof(e.offsetX) == 'undefined' && typeof(e.offsetY) == 'undefined') { - var coords = RGraph.getMouseXY(e); - e.offsetX = coords[0]; - e.offsetY = coords[1]; - } - - // Any browser that doesn't implement stopPropagation() (MSIE) - if (!e.stopPropagation) { - e.stopPropagation = function () {window.event.cancelBubble = true;} - } - - return e; - } - - - /** - * Draw crosshairs if enabled - * - * @param object obj The graph object (from which we can get the context and canvas as required) - */ - RGraph.DrawCrosshairs = function (obj) - { - if (obj.Get('chart.crosshairs')) { - var canvas = obj.canvas; - var context = obj.context; - - // 5th November 2010 - removed now that tooltips are DOM2 based. - //if (obj.Get('chart.tooltips') && obj.Get('chart.tooltips').length > 0) { - //alert('[' + obj.type.toUpperCase() + '] Sorry - you cannot have crosshairs enabled with tooltips! Turning off crosshairs...'); - //obj.Set('chart.crosshairs', false); - //return; - //} - - canvas.onmousemove = function (e) - { - var e = RGraph.FixEventObject(e); - var canvas = obj.canvas; - var context = obj.context; - var gutter = obj.Get('chart.gutter'); - var width = canvas.width; - var height = canvas.height; - var adjustments = obj.Get('chart.tooltips.coords.adjust'); - - var mouseCoords = RGraph.getMouseXY(e); - var x = mouseCoords[0]; - var y = mouseCoords[1]; - - if (typeof(adjustments) == 'object' && adjustments[0] && adjustments[1]) { - x = x - adjustments[0]; - y = y - adjustments[1]; - } - - RGraph.Clear(canvas); - obj.Draw(); - - if ( x >= gutter - && y >= gutter - && x <= (width - gutter) - && y <= (height - gutter) - ) { - - var linewidth = obj.Get('chart.crosshairs.linewidth'); - context.lineWidth = linewidth ? linewidth : 1; - - context.beginPath(); - context.strokeStyle = obj.Get('chart.crosshairs.color'); - - // Draw a top vertical line - context.moveTo(x, gutter); - context.lineTo(x, height - gutter); - - // Draw a horizontal line - context.moveTo(gutter, y); - context.lineTo(width - gutter, y); - - context.stroke(); - - /** - * Need to show the coords? - */ - if (obj.Get('chart.crosshairs.coords')) { - if (obj.type == 'scatter') { - - var xCoord = (((x - obj.Get('chart.gutter')) / (obj.canvas.width - (2 * obj.Get('chart.gutter')))) * (obj.Get('chart.xmax') - obj.Get('chart.xmin'))) + obj.Get('chart.xmin'); - xCoord = xCoord.toFixed(obj.Get('chart.scale.decimals')); - var yCoord = obj.max - (((y - obj.Get('chart.gutter')) / (obj.canvas.height - (2 * obj.Get('chart.gutter')))) * obj.max); - - if (obj.type == 'scatter' && obj.Get('chart.xaxispos') == 'center') { - yCoord = (yCoord - (obj.max / 2)) * 2; - } - - yCoord = yCoord.toFixed(obj.Get('chart.scale.decimals')); - var div = RGraph.Registry.Get('chart.coordinates.coords.div'); - var mouseCoords = RGraph.getMouseXY(e); - var canvasXY = RGraph.getCanvasXY(canvas); - - if (!div) { - - div = document.createElement('DIV'); - div.__object__ = obj; - div.style.position = 'absolute'; - div.style.backgroundColor = 'white'; - div.style.border = '1px solid black'; - div.style.fontFamily = 'Arial, Verdana, sans-serif'; - div.style.fontSize = '10pt' - div.style.padding = '2px'; - div.style.opacity = 1; - div.style.WebkitBorderRadius = '3px'; - div.style.borderRadius = '3px'; - div.style.MozBorderRadius = '3px'; - document.body.appendChild(div); - - RGraph.Registry.Set('chart.coordinates.coords.div', div); - } - - // Convert the X/Y pixel coords to correspond to the scale - - div.style.opacity = 1; - div.style.display = 'inline'; - - if (!obj.Get('chart.crosshairs.coords.fixed')) { - div.style.left = Math.max(2, (e.pageX - div.offsetWidth - 3)) + 'px'; - div.style.top = Math.max(2, (e.pageY - div.offsetHeight - 3)) + 'px'; - } else { - div.style.left = canvasXY[0] + obj.Get('chart.gutter') + 3 + 'px'; - div.style.top = canvasXY[1] + obj.Get('chart.gutter') + 3 + 'px'; - } - - div.innerHTML = '' + obj.Get('chart.crosshairs.coords.labels.x') + ': ' + xCoord + '
    ' + obj.Get('chart.crosshairs.coords.labels.y') + ': ' + yCoord; - - canvas.addEventListener('mouseout', RGraph.HideCrosshairCoords, false); - - } else { - alert('[RGRAPH] Showing crosshair coordinates is only supported on the Scatter chart'); - } - } - } else { - RGraph.HideCrosshairCoords(); - } - } - } - } - - /** - * Thisz function hides the crosshairs coordinates - */ - RGraph.HideCrosshairCoords = function () - { - var div = RGraph.Registry.Get('chart.coordinates.coords.div'); - - if ( div - && div.style.opacity == 1 - && div.__object__.Get('chart.crosshairs.coords.fadeout') - ) { - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.9;}, 50); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.8;}, 100); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.7;}, 150); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.6;}, 200); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.5;}, 250); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.4;}, 300); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.3;}, 350); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.2;}, 400); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0.1;}, 450); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.opacity = 0;}, 500); - setTimeout(function() {RGraph.Registry.Get('chart.coordinates.coords.div').style.display = 'none';}, 550); - } - } - - - /** - * Trims the right hand side of a string. Removes SPACE, TAB - * CR and LF. - * - * @param string str The string to trim - */ - RGraph.rtrim = function (str) - { - return str.replace(/( |\n|\r|\t)+$/, ''); - } - - - /** - * Draws the3D axes/background - */ - RGraph.Draw3DAxes = function (obj) - { - var gutter = obj.Get('chart.gutter'); - var context = obj.context; - var canvas = obj.canvas; - - context.strokeStyle = '#aaa'; - context.fillStyle = '#ddd'; - - // Draw the vertical left side - context.beginPath(); - context.moveTo(gutter, gutter); - context.lineTo(gutter + 10, gutter - 5); - context.lineTo(gutter + 10, canvas.height - gutter - 5); - context.lineTo(gutter, canvas.height - gutter); - context.closePath(); - - context.stroke(); - context.fill(); - - // Draw the bottom floor - context.beginPath(); - context.moveTo(gutter, canvas.height - gutter); - context.lineTo(gutter + 10, canvas.height - gutter - 5); - context.lineTo(canvas.width - gutter + 10, canvas.height - gutter - 5); - context.lineTo(canvas.width - gutter, canvas.height - gutter); - context.closePath(); - - context.stroke(); - context.fill(); - } - - /** - * Turns off any shadow - * - * @param object obj The graph object - */ - RGraph.NoShadow = function (obj) - { - obj.context.shadowColor = 'rgba(0,0,0,0)'; - obj.context.shadowBlur = 0; - obj.context.shadowOffsetX = 0; - obj.context.shadowOffsetY = 0; - } - - - /** - * Sets the four shadow properties - a shortcut function - * - * @param object obj Your graph object - * @param string color The shadow color - * @param number offsetx The shadows X offset - * @param number offsety The shadows Y offset - * @param number blur The blurring effect applied to the shadow - */ - RGraph.SetShadow = function (obj, color, offsetx, offsety, blur) - { - obj.context.shadowColor = color; - obj.context.shadowOffsetX = offsetx; - obj.context.shadowOffsetY = offsety; - obj.context.shadowBlur = blur; - } - - - /** - * This function attempts to "fill in" missing functions from the canvas - * context object. Only two at the moment - measureText() nd fillText(). - * - * @param object context The canvas 2D context - */ - RGraph.OldBrowserCompat = function (context) - { - if (!context.measureText) { - - // This emulates the measureText() function - context.measureText = function (text) - { - var textObj = document.createElement('DIV'); - textObj.innerHTML = text; - textObj.style.backgroundColor = 'white'; - textObj.style.position = 'absolute'; - textObj.style.top = -100 - textObj.style.left = 0; - document.body.appendChild(textObj); - - var width = {width: textObj.offsetWidth}; - - textObj.style.display = 'none'; - - return width; - } - } - - if (!context.fillText) { - // This emulates the fillText() method - context.fillText = function (text, targetX, targetY) - { - return false; - } - } - - // If IE8, add addEventListener() - if (!context.canvas.addEventListener) { - window.addEventListener = function (ev, func, bubble) - { - return this.attachEvent('on' + ev, func); - } - - context.canvas.addEventListener = function (ev, func, bubble) - { - return this.attachEvent('on' + ev, func); - } - } - } - - - /** - * This function is for use with circular graph types, eg the Pie or Radar. Pass it your event object - * and it will pass you back the corresponding segment details as an array: - * - * [x, y, r, startAngle, endAngle] - * - * Angles are measured in degrees, and are measured from the "east" axis (just like the canvas). - * - * @param object e Your event object - */ - RGraph.getSegment = function (e) - { - RGraph.FixEventObject(e); - - // The optional arg provides a way of allowing some accuracy (pixels) - var accuracy = arguments[1] ? arguments[1] : 0; - - var obj = e.target.__object__; - var canvas = obj.canvas; - var context = obj.context; - var mouseCoords = RGraph.getMouseXY(e); - var x = mouseCoords[0] - obj.centerx; - var y = mouseCoords[1] - obj.centery; - var r = obj.radius; - var theta = Math.atan(y / x); // RADIANS - var hyp = y / Math.sin(theta); - var angles = obj.angles; - var ret = []; - var hyp = (hyp < 0) ? hyp + accuracy : hyp - accuracy; - - - // Put theta in DEGREES - theta *= 57.3 - - // hyp should not be greater than radius if it's a Rose chart - if (obj.type == 'rose') { - if ( (isNaN(hyp) && Math.abs(mouseCoords[0]) < (obj.centerx - r) ) - || (isNaN(hyp) && Math.abs(mouseCoords[0]) > (obj.centerx + r)) - || (!isNaN(hyp) && Math.abs(hyp) > r)) { - return; - } - } - - /** - * Account for the correct quadrant - */ - if (x < 0 && y >= 0) { - theta += 180; - } else if (x < 0 && y < 0) { - theta += 180; - } else if (x > 0 && y < 0) { - theta += 360; - } - - /** - * Account for the rose chart - */ - if (obj.type == 'rose') { - theta += 90; - } - - if (theta > 360) { - theta -= 360; - } - - for (var i=0; i= angles[i][0] && theta < angles[i][1]) { - - hyp = Math.abs(hyp); - - if (obj.type == 'rose' && hyp > angles[i][2]) { - return null; - } - - if (!hyp || (obj.type == 'pie' && obj.radius && hyp > obj.radius) ) { - return null; - } - - if (obj.type == 'pie' && obj.Get('chart.variant') == 'donut' && (hyp > obj.radius || hyp < (obj.radius / 2) ) ) { - return null; - } - - ret[0] = obj.centerx; - ret[1] = obj.centery; - ret[2] = (obj.type == 'rose') ? angles[i][2] : obj.radius; - ret[3] = angles[i][0]; - ret[4] = angles[i][1]; - ret[5] = i; - - if (obj.type == 'rose') { - - ret[3] -= 90; - ret[4] -= 90; - - if (x > 0 && y < 0) { - ret[3] += 360; - ret[4] += 360; - } - } - - if (ret[3] < 0) ret[3] += 360; - if (ret[4] > 360) ret[4] -= 360; - - return ret; - } - } - - return null; - } - - - /** - * This is a function that can be used to run code asynchronously, which can - * be used to speed up the loading of you pages. - * - * @param string func This is the code to run. It can also be a function pointer. - * The front page graphs show this function in action. Basically - * each graphs code is made in a function, and that function is - * passed to this function to run asychronously. - */ - RGraph.Async = function (func) - { - return setTimeout(func, arguments[1] ? arguments[1] : 1); - } - - - /** - * A custom random number function - * - * @param number min The minimum that the number should be - * @param number max The maximum that the number should be - * @param number How many decimal places there should be. Default for this is 0 - */ - RGraph.random = function (min, max) - { - var dp = arguments[2] ? arguments[2] : 0; - var r = Math.random(); - - return Number((((max - min) * r) + min).toFixed(dp)); - } - - - /** - * Draws a rectangle with curvy corners - * - * @param context object The context - * @param x number The X coordinate (top left of the square) - * @param y number The Y coordinate (top left of the square) - * @param w number The width of the rectangle - * @param h number The height of the rectangle - * @param number The radius of the curved corners - * @param boolean Whether the top left corner is curvy - * @param boolean Whether the top right corner is curvy - * @param boolean Whether the bottom right corner is curvy - * @param boolean Whether the bottom left corner is curvy - */ - RGraph.strokedCurvyRect = function (context, x, y, w, h) - { - // The corner radius - var r = arguments[5] ? arguments[5] : 3; - - // The corners - var corner_tl = (arguments[6] || arguments[6] == null) ? true : false; - var corner_tr = (arguments[7] || arguments[7] == null) ? true : false; - var corner_br = (arguments[8] || arguments[8] == null) ? true : false; - var corner_bl = (arguments[9] || arguments[9] == null) ? true : false; - - context.beginPath(); - - // Top left side - context.moveTo(x + (corner_tl ? r : 0), y); - context.lineTo(x + w - (corner_tr ? r : 0), y); - - // Top right corner - if (corner_tr) { - context.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2, false); - } - - // Top right side - context.lineTo(x + w, y + h - (corner_br ? r : 0) ); - - // Bottom right corner - if (corner_br) { - context.arc(x + w - r, y - r + h, r, Math.PI * 2, Math.PI * 0.5, false); - } - - // Bottom right side - context.lineTo(x + (corner_bl ? r : 0), y + h); - - // Bottom left corner - if (corner_bl) { - context.arc(x + r, y - r + h, r, Math.PI * 0.5, Math.PI, false); - } - - // Bottom left side - context.lineTo(x, y + (corner_tl ? r : 0) ); - - // Top left corner - if (corner_tl) { - context.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5, false); - } - - context.stroke(); - } - - - /** - * Draws a filled rectangle with curvy corners - * - * @param context object The context - * @param x number The X coordinate (top left of the square) - * @param y number The Y coordinate (top left of the square) - * @param w number The width of the rectangle - * @param h number The height of the rectangle - * @param number The radius of the curved corners - * @param boolean Whether the top left corner is curvy - * @param boolean Whether the top right corner is curvy - * @param boolean Whether the bottom right corner is curvy - * @param boolean Whether the bottom left corner is curvy - */ - RGraph.filledCurvyRect = function (context, x, y, w, h) - { - // The corner radius - var r = arguments[5] ? arguments[5] : 3; - - // The corners - var corner_tl = (arguments[6] || arguments[6] == null) ? true : false; - var corner_tr = (arguments[7] || arguments[7] == null) ? true : false; - var corner_br = (arguments[8] || arguments[8] == null) ? true : false; - var corner_bl = (arguments[9] || arguments[9] == null) ? true : false; - - context.beginPath(); - - // First draw the corners - - // Top left corner - if (corner_tl) { - context.moveTo(x + r, y + r); - context.arc(x + r, y + r, r, Math.PI, 1.5 * Math.PI, false); - } else { - context.fillRect(x, y, r, r); - } - - // Top right corner - if (corner_tr) { - context.moveTo(x + w - r, y + r); - context.arc(x + w - r, y + r, r, 1.5 * Math.PI, 0, false); - } else { - context.moveTo(x + w - r, y); - context.fillRect(x + w - r, y, r, r); - } - - - // Bottom right corner - if (corner_br) { - context.moveTo(x + w - r, y + h - r); - context.arc(x + w - r, y - r + h, r, 0, Math.PI / 2, false); - } else { - context.moveTo(x + w - r, y + h - r); - context.fillRect(x + w - r, y + h - r, r, r); - } - - // Bottom left corner - if (corner_bl) { - context.moveTo(x + r, y + h - r); - context.arc(x + r, y - r + h, r, Math.PI / 2, Math.PI, false); - } else { - context.moveTo(x, y + h - r); - context.fillRect(x, y + h - r, r, r); - } - - // Now fill it in - context.fillRect(x + r, y, w - r - r, h); - context.fillRect(x, y + r, r + 1, h - r - r); - context.fillRect(x + w - r - 1, y + r, r + 1, h - r - r); - - context.fill(); - } - - - /** - * A crude timing function - * - * @param string label The label to use for the time - */ - RGraph.Timer = function (label) - { - var d = new Date(); - - // This uses the Firebug console - console.log(label + ': ' + d.getSeconds() + '.' + d.getMilliseconds()); - } - - - /** - * Hides the palette if it's visible - */ - RGraph.HidePalette = function () - { - var div = RGraph.Registry.Get('palette'); - - if (typeof(div) == 'object' && div) { - div.style.visibility = 'hidden'; - div.style.display = 'none'; - RGraph.Registry.Set('palette', null); - } - } - - - /** - * Hides the zoomed canvas - */ - RGraph.HideZoomedCanvas = function () - { - if (typeof(__zoomedimage__) == 'object') { - obj = __zoomedimage__.obj; - } else { - return; - } - - if (obj.Get('chart.zoom.fade.out')) { - for (var i=10,j=1; i>=0; --i, ++j) { - if (typeof(__zoomedimage__) == 'object') { - setTimeout("__zoomedimage__.style.opacity = " + String(i / 10), j * 30); - } - } - - if (typeof(__zoomedbackground__) == 'object') { - setTimeout("__zoomedbackground__.style.opacity = " + String(i / 10), j * 30); - } - } - - if (typeof(__zoomedimage__) == 'object') { - setTimeout("__zoomedimage__.style.display = 'none'", obj.Get('chart.zoom.fade.out') ? 310 : 0); - } - - if (typeof(__zoomedbackground__) == 'object') { - setTimeout("__zoomedbackground__.style.display = 'none'", obj.Get('chart.zoom.fade.out') ? 310 : 0); - } - } - - - /** - * Adds an event handler - * - * @param object obj The graph object - * @param string event The name of the event, eg ontooltip - * @param object func The callback function - */ - RGraph.AddCustomEventListener = function (obj, name, func) - { - if (typeof(RGraph.events[obj.id]) == 'undefined') { - RGraph.events[obj.id] = []; - } - - RGraph.events[obj.id].push([obj, name, func]); - } - - - /** - * Used to fire one of the RGraph custom events - * - * @param object obj The graph object that fires the event - * @param string event The name of the event to fire - */ - RGraph.FireCustomEvent = function (obj, name) - { - for (i in RGraph.events) { - if (typeof(i) == 'string' && i == obj.id && RGraph.events[i].length > 0) { - for(var j=0; j 0; - } - - - /** - * Checks the browser for traces of MSIE9 - */ - RGraph.isIE9 = function () - { - return navigator.userAgent.indexOf('MSIE 9') > 0; - } - - - /** - * Checks the browser for traces of MSIE9 - */ - RGraph.isIE9up = function () - { - navigator.userAgent.match(/MSIE (\d+)/); - - return Number(RegExp.$1) >= 9; - } - - - /** - * This clears a canvases event handlers. - * - * @param string id The ID of the canvas whose event handlers will be cleared - */ - RGraph.ClearEventListeners = function (id) - { - for (var i=0; i str.length ? obj.Get('chart.labels')[i] : str); - } - } - - obj.context.font = obj.Get('chart.text.size') + 'pt ' + obj.Get('chart.text.font'); - - len = obj.context.measureText(str).width + 5; - - return (obj.type == 'hbar' ? len / 3 : len); - } - - - /** - * A basic Array shift gunction - * - * @param object The numerical array to work on - * @return The new array - */ - RGraph.array_shift = function (arr) - { - var ret = []; - - for (var i=1; i document.body.offsetWidth ? RGraph.Registry.Get('chart.tooltip').offsetWidth : 0); - var diffy = y - currenty - RGraph.Registry.Get('chart.tooltip').offsetHeight; - - // Position the tooltip - setTimeout('RGraph.Registry.Get("chart.tooltip").style.left = "' + (currentx + (diffx * 0.2)) + 'px"', 25); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.left = "' + (currentx + (diffx * 0.4)) + 'px"', 50); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.left = "' + (currentx + (diffx * 0.6)) + 'px"', 75); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.left = "' + (currentx + (diffx * 0.8)) + 'px"', 100); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.left = "' + (currentx + (diffx * 1.0)) + 'px"', 125); - - setTimeout('RGraph.Registry.Get("chart.tooltip").style.top = "' + (currenty + (diffy * 0.2)) + 'px"', 25); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.top = "' + (currenty + (diffy * 0.4)) + 'px"', 50); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.top = "' + (currenty + (diffy * 0.6)) + 'px"', 75); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.top = "' + (currenty + (diffy * 0.8)) + 'px"', 100); - setTimeout('RGraph.Registry.Get("chart.tooltip").style.top = "' + (currenty + (diffy * 1.0)) + 'px"', 125); - - } else { - - alert('[TOOLTIPS] The "snap" effect is only supported on the Line, Rscatter, Scatter and Tradar charts'); - } - - /** - * Fire the tooltip event - */ - RGraph.FireCustomEvent(canvas.__object__, 'ontooltip'); - - return; - } - - /** - * Hide any currently shown tooltip - */ - RGraph.HideTooltip(); - - - /** - * Show a tool tip - */ - var tooltipObj = document.createElement('DIV'); - tooltipObj.className = canvas.__object__.Get('chart.tooltips.css.class'); - tooltipObj.style.display = 'none'; - tooltipObj.style.position = 'absolute'; - tooltipObj.style.left = 0; - tooltipObj.style.top = 0; - tooltipObj.style.backgroundColor = '#ffe'; - tooltipObj.style.color = 'black'; - if (!document.all) tooltipObj.style.border = '1px solid rgba(0,0,0,0)'; - tooltipObj.style.visibility = 'visible'; - tooltipObj.style.paddingLeft = RGraph.tooltips.padding; - tooltipObj.style.paddingRight = RGraph.tooltips.padding; - tooltipObj.style.fontFamily = RGraph.tooltips.font_face; - tooltipObj.style.fontSize = RGraph.tooltips.font_size; - tooltipObj.style.zIndex = 3; - tooltipObj.style.borderRadius = '5px'; - tooltipObj.style.MozBorderRadius = '5px'; - tooltipObj.style.WebkitBorderRadius = '5px'; - tooltipObj.style.WebkitBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; - tooltipObj.style.MozBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; - tooltipObj.style.boxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; - tooltipObj.style.filter = 'progid:DXImageTransform.Microsoft.Shadow(color=#666666,direction=135)'; - tooltipObj.style.opacity = 0; - tooltipObj.style.overflow = 'hidden'; - tooltipObj.innerHTML = text; - tooltipObj.__text__ = text; // This is set because the innerHTML can change when it's set - tooltipObj.__canvas__ = canvas; - tooltipObj.style.display = 'inline'; - - if (typeof(idx) == 'number') { - tooltipObj.__index__ = idx; - } - - document.body.appendChild(tooltipObj); - - var width = tooltipObj.offsetWidth; - var height = tooltipObj.offsetHeight - (RGraph.isIE9up() ? 7 : 0); - - if ((y - height - 2) > 0) { - y = y - height - 2; - } else { - y = y + 2; - } - - /** - * Set the width on the tooltip so it doesn't resize if the window is resized - */ - tooltipObj.style.width = width + 'px'; - //tooltipObj.style.height = 0; // Initially set the tooltip height to nothing - - /** - * If the mouse is towards the right of the browser window and the tooltip would go outside of the window, - * move it left - */ - if ( (x + width) > document.body.offsetWidth ) { - x = x - width - 7; - var placementLeft = true; - - if (canvas.__object__.Get('chart.tooltips.effect') == 'none') { - x = x - 3; - } - - tooltipObj.style.left = x + 'px'; - tooltipObj.style.top = y + 'px'; - - } else { - x += 5; - - tooltipObj.style.left = x + 'px'; - tooltipObj.style.top = y + 'px'; - } - - - if (effect == 'expand') { - - tooltipObj.style.left = (x + (width / 2)) + 'px'; - tooltipObj.style.top = (y + (height / 2)) + 'px'; - leftDelta = (width / 2) / 10; - topDelta = (height / 2) / 10; - - tooltipObj.style.width = 0; - tooltipObj.style.height = 0; - tooltipObj.style.boxShadow = ''; - tooltipObj.style.MozBoxShadow = ''; - tooltipObj.style.WebkitBoxShadow = ''; - tooltipObj.style.borderRadius = 0; - tooltipObj.style.MozBorderRadius = 0; - tooltipObj.style.WebkitBorderRadius = 0; - tooltipObj.style.opacity = 1; - - // Progressively move the tooltip to where it should be (the x position) - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) - leftDelta) + 'px' }", 250)); - - // Progressively move the tooltip to where it should be (the Y position) - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) - topDelta) + 'px' }", 250)); - - // Progressively grow the tooltip width - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.1) + "px'; }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.2) + "px'; }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.3) + "px'; }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.4) + "px'; }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.5) + "px'; }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.6) + "px'; }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.7) + "px'; }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.8) + "px'; }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 0.9) + "px'; }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + width + "px'; }", 250)); - - // Progressively grow the tooltip height - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.1) + "px'; }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.2) + "px'; }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.3) + "px'; }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.4) + "px'; }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.5) + "px'; }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.6) + "px'; }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.7) + "px'; }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.8) + "px'; }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 0.9) + "px'; }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + height + "px'; }", 250)); - - // When the animation is finished, set the tooltip HTML - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').innerHTML = RGraph.Registry.Get('chart.tooltip').__text__; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.boxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.MozBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.WebkitBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.borderRadius = '5px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.MozBorderRadius = '5px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.WebkitBorderRadius = '5px'; }", 250)); - - } else if (effect == 'contract') { - - tooltipObj.style.left = (x - width) + 'px'; - tooltipObj.style.top = (y - (height * 2)) + 'px'; - tooltipObj.style.cursor = 'pointer'; - - leftDelta = width / 10; - topDelta = height / 10; - - tooltipObj.style.width = (width * 5) + 'px'; - tooltipObj.style.height = (height * 5) + 'px'; - - tooltipObj.style.opacity = 0.2; - - // Progressively move the tooltip to where it should be (the x position) - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.left = (parseInt(RGraph.Registry.Get('chart.tooltip').style.left) + leftDelta) + 'px' }", 250)); - - // Progressively move the tooltip to where it should be (the Y position) - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.top = (parseInt(RGraph.Registry.Get('chart.tooltip').style.top) + (topDelta*2)) + 'px' }", 250)); - - // Progressively shrink the tooltip width - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 5.5) + "px'; }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 5.0) + "px'; }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 4.5) + "px'; }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 4.0) + "px'; }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 3.5) + "px'; }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 3.0) + "px'; }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 2.5) + "px'; }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 2.0) + "px'; }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + (width * 1.5) + "px'; }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.width = '" + width + "px'; }", 250)); - - // Progressively shrink the tooltip height - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 5.5) + "px'; }", 25)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 5.0) + "px'; }", 50)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 4.5) + "px'; }", 75)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 4.0) + "px'; }", 100)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 3.5) + "px'; }", 125)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 3.0) + "px'; }", 150)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 2.5) + "px'; }", 175)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 2.0) + "px'; }", 200)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + (height * 1.5) + "px'; }", 225)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.height = '" + height + "px'; }", 250)); - - // When the animation is finished, set the tooltip HTML - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').innerHTML = RGraph.Registry.Get('chart.tooltip').__text__; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.boxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.MozBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.WebkitBoxShadow = 'rgba(96,96,96,0.5) 3px 3px 3px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.borderRadius = '5px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.MozBorderRadius = '5px'; }", 250)); - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.WebkitBorderRadius = '5px'; }", 250)); - - /** - * This resets the pointer - */ - RGraph.Registry.Get('chart.tooltip.timers').push(setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.cursor = 'default'; }", 275)); - - - - } else if (effect != 'fade' && effect != 'expand' && effect != 'none' && effect != 'snap' && effect != 'contract') { - alert('[COMMON] Unknown tooltip effect: ' + effect); - } - - if (effect != 'none') { - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.1; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 25); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.2; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 50); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.3; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 75); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.4; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 100); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.5; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 125); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.6; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.2)'; }", 150); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.7; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.4)'; }", 175); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.8; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.6)'; }", 200); - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 0.9; RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgba(96,96,96,0.8)'; }", 225); - } - - setTimeout("if (RGraph.Registry.Get('chart.tooltip')) { RGraph.Registry.Get('chart.tooltip').style.opacity = 1;RGraph.Registry.Get('chart.tooltip').style.border = '1px solid rgb(96,96,96)';}", effect == 'none' ? 50 : 250); - - /** - * If the tooltip it self is clicked, cancel it - */ - tooltipObj.onmousedown = function (e) - { - e = RGraph.FixEventObject(e) - e.stopPropagation(); - } - - tooltipObj.onclick = function (e) - { - if (e.button == 0) { - e = RGraph.FixEventObject(e); - e.stopPropagation(); - } - } - - /** - * Install the function for hiding the tooltip. - */ - document.body.onmousedown = function (event) - { - var tooltip = RGraph.Registry.Get('chart.tooltip'); - - if (tooltip) { - RGraph.HideTooltip(); - - // Redraw if highlighting is enabled - if (tooltip.__canvas__.__object__.Get('chart.tooltips.highlight')) { - RGraph.Redraw(); - } - } - } - - /** - * If the window is resized, hide the tooltip - */ - window.onresize = function () - { - var tooltip = RGraph.Registry.Get('chart.tooltip'); - - if (tooltip) { - tooltip.parentNode.removeChild(tooltip); - tooltip.style.display = 'none'; - tooltip.style.visibility = 'hidden'; - RGraph.Registry.Set('chart.tooltip', null); - - // Redraw the graph if necessary - if (canvas.__object__.Get('chart.tooltips.highlight')) { - RGraph.Clear(canvas); - canvas.__object__.Draw(); - } - } - } - - /** - * Keep a reference to the tooltip - */ - RGraph.Registry.Set('chart.tooltip', tooltipObj); - - /** - * Fire the tooltip event - */ - RGraph.FireCustomEvent(canvas.__object__, 'ontooltip'); - } - - - /** - * - */ - RGraph.getTooltipText = function (text) - { - var result = /^id:(.*)/.exec(text); - - if (result) { - text = document.getElementById(result[1]).innerHTML; - } - - return text; - } - - - /** - * - */ - RGraph.getTooltipWidth = function (text, obj) - { - var div = document.createElement('DIV'); - div.className = obj.Get('chart.tooltips.css.class'); - div.style.paddingLeft = RGraph.tooltips.padding; - div.style.paddingRight = RGraph.tooltips.padding; - div.style.fontFamily = RGraph.tooltips.font_face; - div.style.fontSize = RGraph.tooltips.font_size; - div.style.visibility = 'hidden'; - div.style.position = 'absolute'; - div.style.top = '300px'; - div.style.left = 0; - div.style.display = 'inline'; - div.innerHTML = RGraph.getTooltipText(text); - document.body.appendChild(div); - - return div.offsetWidth; - } - - - /** - * Hides the currently shown tooltip - */ - RGraph.HideTooltip = function () - { - var tooltip = RGraph.Registry.Get('chart.tooltip'); - - if (tooltip) { - tooltip.parentNode.removeChild(tooltip); - tooltip.style.display = 'none'; - tooltip.style.visibility = 'hidden'; - RGraph.Registry.Set('chart.tooltip', null); - } - } \ No newline at end of file diff --git a/PHP/CodeCoverage/Report/HTML/Template/RGraph.scatter.js b/PHP/CodeCoverage/Report/HTML/Template/RGraph.scatter.js deleted file mode 100644 index e87202cc0..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/RGraph.scatter.js +++ /dev/null @@ -1,1168 +0,0 @@ - /** - * o------------------------------------------------------------------------------o - * | This file is part of the RGraph package - you can learn more at: | - * | | - * | http://www.rgraph.net | - * | | - * | This package is licensed under the RGraph license. For all kinds of business | - * | purposes there is a small one-time licensing fee to pay and for non | - * | commercial purposes it is free to use. You can read the full license here: | - * | | - * | http://www.rgraph.net/LICENSE.txt | - * o------------------------------------------------------------------------------o - */ - - if (typeof(RGraph) == 'undefined') RGraph = {}; - - /** - * The scatter graph constructor - * - * @param object canvas The cxanvas object - * @param array data The chart data - */ - RGraph.Scatter = function (id, data) - { - // Get the canvas and context objects - this.id = id; - this.canvas = document.getElementById(id); - this.canvas.__object__ = this; - this.context = this.canvas.getContext ? this.canvas.getContext("2d") : null; - this.max = 0; - this.coords = []; - this.data = []; - this.type = 'scatter'; - this.isRGraph = true; - - - /** - * Compatibility with older browsers - */ - RGraph.OldBrowserCompat(this.context); - - - // Various config properties - this.properties = { - 'chart.background.barcolor1': 'white', - 'chart.background.barcolor2': 'white', - 'chart.background.grid': true, - 'chart.background.grid.width': 1, - 'chart.background.grid.color': '#ddd', - 'chart.background.grid.hsize': 20, - 'chart.background.grid.vsize': 20, - 'chart.background.hbars': null, - 'chart.background.vbars': null, - 'chart.background.grid.vlines': true, - 'chart.background.grid.hlines': true, - 'chart.background.grid.border': true, - 'chart.background.grid.autofit':false, - 'chart.background.grid.autofit.numhlines': 7, - 'chart.background.grid.autofit.numvlines': 20, - 'chart.text.size': 10, - 'chart.text.angle': 0, - 'chart.text.color': 'black', - 'chart.text.font': 'Verdana', - 'chart.tooltips.effect': 'fade', - 'chart.tooltips.hotspot': 3, - 'chart.tooltips.css.class': 'RGraph_tooltip', - 'chart.tooltips.highlight': true, - 'chart.tooltips.coords.adjust': [0,0], - 'chart.units.pre': '', - 'chart.units.post': '', - 'chart.tickmarks': 'cross', - 'chart.ticksize': 5, - 'chart.xticks': true, - 'chart.xaxis': true, - 'chart.gutter': 25, - 'chart.xmax': 0, - 'chart.ymax': null, - 'chart.ymin': null, - 'chart.scale.decimals': null, - 'chart.scale.point': '.', - 'chart.scale.thousand': ',', - 'chart.title': '', - 'chart.title.background': null, - 'chart.title.hpos': null, - 'chart.title.vpos': null, - 'chart.title.xaxis': '', - 'chart.title.yaxis': '', - 'chart.title.xaxis.pos': 0.25, - 'chart.title.yaxis.pos': 0.25, - 'chart.labels': [], - 'chart.ylabels': true, - 'chart.ylabels.count': 5, - 'chart.contextmenu': null, - 'chart.defaultcolor': 'black', - 'chart.xaxispos': 'bottom', - 'chart.yaxispos': 'left', - 'chart.noendxtick': false, - 'chart.crosshairs': false, - 'chart.crosshairs.color': '#333', - 'chart.crosshairs.linewidth': 1, - 'chart.crosshairs.coords': false, - 'chart.crosshairs.coords.fixed':true, - 'chart.crosshairs.coords.fadeout':false, - 'chart.crosshairs.coords.labels.x': 'X', - 'chart.crosshairs.coords.labels.y': 'Y', - 'chart.annotatable': false, - 'chart.annotate.color': 'black', - 'chart.line': false, - 'chart.line.linewidth': 1, - 'chart.line.colors': ['green', 'red'], - 'chart.line.shadow.color': 'rgba(0,0,0,0)', - 'chart.line.shadow.blur': 2, - 'chart.line.shadow.offsetx': 3, - 'chart.line.shadow.offsety': 3, - 'chart.line.stepped': false, - 'chart.noaxes': false, - 'chart.key': [], - 'chart.key.background': 'white', - 'chart.key.position': 'graph', - 'chart.key.shadow': false, - 'chart.key.shadow.color': '#666', - 'chart.key.shadow.blur': 3, - 'chart.key.shadow.offsetx': 2, - 'chart.key.shadow.offsety': 2, - 'chart.key.position.gutter.boxed': true, - 'chart.key.position.x': null, - 'chart.key.position.y': null, - 'chart.key.color.shape': 'square', - 'chart.key.rounded': true, - 'chart.axis.color': 'black', - 'chart.zoom.factor': 1.5, - 'chart.zoom.fade.in': true, - 'chart.zoom.fade.out': true, - 'chart.zoom.hdir': 'right', - 'chart.zoom.vdir': 'down', - 'chart.zoom.frames': 10, - 'chart.zoom.delay': 50, - 'chart.zoom.shadow': true, - 'chart.zoom.mode': 'canvas', - 'chart.zoom.thumbnail.width': 75, - 'chart.zoom.thumbnail.height': 75, - 'chart.zoom.background': true, - 'chart.zoom.action': 'zoom', - 'chart.boxplot.width': 8, - 'chart.resizable': false, - 'chart.xmin': 0 - } - - // Handle multiple datasets being given as one argument - if (arguments[1][0] && arguments[1][0][0] && typeof(arguments[1][0][0][0]) == 'number') { - // Store the data set(s) - for (var i=0; i 0) { - - this.scale = []; - this.max = this.Get('chart.ymax'); - this.min = this.Get('chart.ymin') ? this.Get('chart.ymin') : 0; - - this.scale[0] = ((this.max - this.min) * (1/5)) + this.min; - this.scale[1] = ((this.max - this.min) * (2/5)) + this.min; - this.scale[2] = ((this.max - this.min) * (3/5)) + this.min; - this.scale[3] = ((this.max - this.min) * (4/5)) + this.min; - this.scale[4] = ((this.max - this.min) * (5/5)) + this.min; - - var decimals = this.Get('chart.scale.decimals'); - - this.scale = [ - Number(this.scale[0]).toFixed(decimals), - Number(this.scale[1]).toFixed(decimals), - Number(this.scale[2]).toFixed(decimals), - Number(this.scale[3]).toFixed(decimals), - Number(this.scale[4]).toFixed(decimals) - ]; - - } else { - - var i = 0; - var j = 0; - - for (i=0; i= (xCoord - offset) && - mouseCoords[1] <= (yCoord + offset) && - mouseCoords[1] >= (yCoord - offset) && - tooltip && - tooltip.length > 0) { - - overHotspot = true; - canvas.style.cursor = 'pointer'; - - if ( - !RGraph.Registry.Get('chart.tooltip') || - RGraph.Registry.Get('chart.tooltip').__text__ != tooltip || - RGraph.Registry.Get('chart.tooltip').__index__ != i || - RGraph.Registry.Get('chart.tooltip').__dataset__ != set - ) { - - if (obj.Get('chart.tooltips.highlight')) { - RGraph.Redraw(); - } - - /** - * Get the tooltip text - */ - if (typeof(tooltip) == 'function') { - var text = String(tooltip(i)); - - } else { - var text = String(tooltip); - } - - RGraph.Tooltip(canvas, text, e.pageX, e.pageY, i); - RGraph.Registry.Get('chart.tooltip').__dataset__ = set; - - /** - * Draw a circle around the mark - */ - if (obj.Get('chart.tooltips.highlight')) { - context.beginPath(); - context.fillStyle = 'rgba(255,255,255,0.5)'; - context.arc(xCoord, yCoord, 3, 0, 6.28, 0); - context.fill(); - } - } - } - } - } - - /** - * Reset the pointer - */ - if (!overHotspot) { - canvas.style.cursor = 'default'; - } - } - this.canvas.addEventListener('mousemove', canvas_onmousemove_func, false); - RGraph.AddEventListener(this.id, 'mousemove', canvas_onmousemove_func); - } - - - /** - * Draw the key if necessary - */ - if (this.Get('chart.key') && this.Get('chart.key').length) { - RGraph.DrawKey(this, this.Get('chart.key'), this.Get('chart.line.colors')); - } - - - /** - * Draw crosschairs - */ - RGraph.DrawCrosshairs(this); - - /** - * If the canvas is annotatable, do install the event handlers - */ - if (this.Get('chart.annotatable')) { - RGraph.Annotate(this); - } - - /** - * This bit shows the mini zoom window if requested - */ - if (this.Get('chart.zoom.mode') == 'thumbnail' || this.Get('chart.zoom.mode') == 'area') { - RGraph.ShowZoomWindow(this); - } - - - /** - * This function enables resizing - */ - if (this.Get('chart.resizable')) { - RGraph.AllowResizing(this); - } - - /** - * Fire the RGraph ondraw event - */ - RGraph.FireCustomEvent(this, 'ondraw'); - } - - - /** - * Draws the axes of the scatter graph - */ - RGraph.Scatter.prototype.DrawAxes = function () - { - var canvas = this.canvas; - var context = this.context; - var graphHeight = this.canvas.height - (this.Get('chart.gutter') * 2); - var gutter = this.Get('chart.gutter'); - - context.beginPath(); - context.strokeStyle = this.Get('chart.axis.color'); - context.lineWidth = 1; - - // Draw the Y axis - if (this.Get('chart.yaxispos') == 'left') { - context.moveTo(gutter, gutter); - context.lineTo(gutter, this.canvas.height - gutter); - } else { - context.moveTo(canvas.width - gutter, gutter); - context.lineTo(canvas.width - gutter, canvas.height - gutter); - } - - - // Draw the X axis - if (this.Get('chart.xaxis')) { - if (this.Get('chart.xaxispos') == 'center') { - context.moveTo(gutter, canvas.height / 2); - context.lineTo(canvas.width - gutter, canvas.height / 2); - } else { - context.moveTo(gutter, canvas.height - gutter); - context.lineTo(canvas.width - gutter, canvas.height - gutter); - } - } - - /** - * Draw the Y tickmarks - */ - for (y=gutter; y < canvas.height - gutter + (this.Get('chart.xaxispos') == 'center' ? 1 : 0) ; y+=(graphHeight / 5) / 2) { - - // This is here to accomodate the X axis being at the center - if (y == (canvas.height / 2) ) continue; - - if (this.Get('chart.yaxispos') == 'left') { - context.moveTo(gutter, y); - context.lineTo(gutter - 3, y); - } else { - context.moveTo(canvas.width - gutter +3, y); - context.lineTo(canvas.width - gutter, y); - } - } - - - /** - * Draw the X tickmarks - */ - if (this.Get('chart.xticks') && this.Get('chart.xaxis')) { - - var x = 0; - var y = (this.Get('chart.xaxispos') == 'center') ? (this.canvas.height / 2) : (this.canvas.height - gutter); - this.xTickGap = (this.canvas.width - (2 * gutter) ) / this.Get('chart.labels').length; - - for (x = (gutter + (this.Get('chart.yaxispos') == 'left' ? this.xTickGap / 2 : 0) ); x<=(canvas.width - gutter - (this.Get('chart.yaxispos') == 'left' ? 0 : 1)); x += this.xTickGap / 2) { - - if (this.Get('chart.yaxispos') == 'left' && this.Get('chart.noendxtick') == true && x == (canvas.width - gutter) ) { - continue; - - } else if (this.Get('chart.yaxispos') == 'right' && this.Get('chart.noendxtick') == true && x == gutter) { - continue; - } - - context.moveTo(x, y - (this.Get('chart.xaxispos') == 'center' ? 3 : 0)); - context.lineTo(x, y + 3); - } - } - - context.stroke(); - } - - - /** - * Draws the labels on the scatter graph - */ - RGraph.Scatter.prototype.DrawLabels = function () - { - this.context.fillStyle = this.Get('chart.text.color'); - var font = this.Get('chart.text.font'); - var xMin = this.Get('chart.xmin'); - var xMax = this.Get('chart.xmax'); - var yMax = this.scale[4]; - var gutter = this.Get('chart.gutter'); - var text_size = this.Get('chart.text.size'); - var units_pre = this.Get('chart.units.pre'); - var units_post = this.Get('chart.units.post'); - var numYLabels = this.Get('chart.ylabels.count'); - var context = this.context; - var canvas = this.canvas; - - this.halfTextHeight = text_size / 2; - - - this.halfGraphHeight = (this.canvas.height - (2 * this.Get('chart.gutter'))) / 2; - - /** - * Draw the Y yaxis labels, be it at the top or center - */ - if (this.Get('chart.ylabels')) { - - var xPos = this.Get('chart.yaxispos') == 'left' ? gutter - 5 : canvas.width - gutter + 5; - var align = this.Get('chart.yaxispos') == 'right' ? 'left' : 'right'; - - if (this.Get('chart.xaxispos') == 'center') { - - - /** - * Specific Y labels - */ - if (typeof(this.Get('chart.ylabels.specific')) == 'object') { - - var labels = this.Get('chart.ylabels.specific'); - - for (var i=0; i= 5) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (1/10) ), RGraph.number_format(this, this.scale[3], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (3/10) ), RGraph.number_format(this, this.scale[1], units_pre, units_post), 'center', align); - } - - if (numYLabels >= 3) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (2/10) ), RGraph.number_format(this, this.scale[2], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (4/10) ), RGraph.number_format(this, this.scale[0], units_pre, units_post), 'center', align); - } - - // Draw the bottom halves labels - if (numYLabels >= 3) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (1/10) ) + this.halfGraphHeight, '-' + RGraph.number_format(this, this.scale[0], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (3/10) ) + this.halfGraphHeight, '-' + RGraph.number_format(this, this.scale[2], units_pre, units_post), 'center', align); - } - - if (numYLabels == 5) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (2/10) ) + this.halfGraphHeight, '-' + RGraph.number_format(this, this.scale[1], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (4/10) ) + this.halfGraphHeight, '-' + RGraph.number_format(this, this.scale[3], units_pre, units_post), 'center', align); - } - - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (5/10) ) + this.halfGraphHeight, '-' + RGraph.number_format(this, this.scale[4], units_pre, units_post), 'center', align); - - } else if (numYLabels == 10) { - // 10 Y labels - var interval = (this.grapharea / numYLabels) / 2; - - for (var i=0; i= 5) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (1/5) ), RGraph.number_format(this, this.scale[3], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (3/5) ), RGraph.number_format(this, this.scale[1], units_pre, units_post), 'center', align); - } - - if (numYLabels >= 3) { - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (2/5) ), RGraph.number_format(this, this.scale[2], units_pre, units_post), 'center', align); - RGraph.Text(context, font, text_size, xPos, gutter + ((canvas.height - (2 * gutter)) * (4/5) ), RGraph.number_format(this, this.scale[0], units_pre, units_post), 'center', align); - } - } else if (numYLabels == 10) { - - // 10 Y labels - var interval = (this.grapharea / numYLabels) / 2; - - for (var i=0; i 0) { - angle = -1 * this.Get('chart.text.angle'); - valign = 'center'; - halign = 'right'; - yPos -= 10; - } - - for (i=0; i= 2) { - - this.context.lineCap = 'round'; - this.context.lineJoin = 'round'; - this.context.lineWidth = this.GetLineWidth(i);// i is the index of the set of coordinates - this.context.strokeStyle = this.Get('chart.line.colors')[i]; - this.context.beginPath(); - - var len = this.coords[i].length; - - for (var j=0; j0){G=F-1;do{D=E.subscribers[G];if(D&&D.obj==I&&D.fn==H){return true;}}while(G--);}return false;};YAHOO.lang.augmentProto(A,YAHOO.util.EventProvider);}());(function(){YAHOO.widget.Module=function(R,Q){if(R){this.init(R,Q);}else{}};var F=YAHOO.util.Dom,D=YAHOO.util.Config,N=YAHOO.util.Event,M=YAHOO.util.CustomEvent,G=YAHOO.widget.Module,I=YAHOO.env.ua,H,P,O,E,A={"BEFORE_INIT":"beforeInit","INIT":"init","APPEND":"append","BEFORE_RENDER":"beforeRender","RENDER":"render","CHANGE_HEADER":"changeHeader","CHANGE_BODY":"changeBody","CHANGE_FOOTER":"changeFooter","CHANGE_CONTENT":"changeContent","DESTROY":"destroy","BEFORE_SHOW":"beforeShow","SHOW":"show","BEFORE_HIDE":"beforeHide","HIDE":"hide"},J={"VISIBLE":{key:"visible",value:true,validator:YAHOO.lang.isBoolean},"EFFECT":{key:"effect",suppressEvent:true,supercedes:["visible"]},"MONITOR_RESIZE":{key:"monitorresize",value:true},"APPEND_TO_DOCUMENT_BODY":{key:"appendtodocumentbody",value:false}};G.IMG_ROOT=null;G.IMG_ROOT_SSL=null;G.CSS_MODULE="yui-module";G.CSS_HEADER="hd";G.CSS_BODY="bd";G.CSS_FOOTER="ft";G.RESIZE_MONITOR_SECURE_URL="javascript:false;";G.RESIZE_MONITOR_BUFFER=1;G.textResizeEvent=new M("textResize");G.forceDocumentRedraw=function(){var Q=document.documentElement;if(Q){Q.className+=" ";Q.className=YAHOO.lang.trim(Q.className);}};function L(){if(!H){H=document.createElement("div");H.innerHTML=('
    '+'
    ');P=H.firstChild;O=P.nextSibling;E=O.nextSibling;}return H;}function K(){if(!P){L();}return(P.cloneNode(false));}function B(){if(!O){L();}return(O.cloneNode(false));}function C(){if(!E){L();}return(E.cloneNode(false));}G.prototype={constructor:G,element:null,header:null,body:null,footer:null,id:null,imageRoot:G.IMG_ROOT,initEvents:function(){var Q=M.LIST; -this.beforeInitEvent=this.createEvent(A.BEFORE_INIT);this.beforeInitEvent.signature=Q;this.initEvent=this.createEvent(A.INIT);this.initEvent.signature=Q;this.appendEvent=this.createEvent(A.APPEND);this.appendEvent.signature=Q;this.beforeRenderEvent=this.createEvent(A.BEFORE_RENDER);this.beforeRenderEvent.signature=Q;this.renderEvent=this.createEvent(A.RENDER);this.renderEvent.signature=Q;this.changeHeaderEvent=this.createEvent(A.CHANGE_HEADER);this.changeHeaderEvent.signature=Q;this.changeBodyEvent=this.createEvent(A.CHANGE_BODY);this.changeBodyEvent.signature=Q;this.changeFooterEvent=this.createEvent(A.CHANGE_FOOTER);this.changeFooterEvent.signature=Q;this.changeContentEvent=this.createEvent(A.CHANGE_CONTENT);this.changeContentEvent.signature=Q;this.destroyEvent=this.createEvent(A.DESTROY);this.destroyEvent.signature=Q;this.beforeShowEvent=this.createEvent(A.BEFORE_SHOW);this.beforeShowEvent.signature=Q;this.showEvent=this.createEvent(A.SHOW);this.showEvent.signature=Q;this.beforeHideEvent=this.createEvent(A.BEFORE_HIDE);this.beforeHideEvent.signature=Q;this.hideEvent=this.createEvent(A.HIDE);this.hideEvent.signature=Q;},platform:function(){var Q=navigator.userAgent.toLowerCase();if(Q.indexOf("windows")!=-1||Q.indexOf("win32")!=-1){return"windows";}else{if(Q.indexOf("macintosh")!=-1){return"mac";}else{return false;}}}(),browser:function(){var Q=navigator.userAgent.toLowerCase();if(Q.indexOf("opera")!=-1){return"opera";}else{if(Q.indexOf("msie 7")!=-1){return"ie7";}else{if(Q.indexOf("msie")!=-1){return"ie";}else{if(Q.indexOf("safari")!=-1){return"safari";}else{if(Q.indexOf("gecko")!=-1){return"gecko";}else{return false;}}}}}}(),isSecure:function(){if(window.location.href.toLowerCase().indexOf("https")===0){return true;}else{return false;}}(),initDefaultConfig:function(){this.cfg.addProperty(J.VISIBLE.key,{handler:this.configVisible,value:J.VISIBLE.value,validator:J.VISIBLE.validator});this.cfg.addProperty(J.EFFECT.key,{suppressEvent:J.EFFECT.suppressEvent,supercedes:J.EFFECT.supercedes});this.cfg.addProperty(J.MONITOR_RESIZE.key,{handler:this.configMonitorResize,value:J.MONITOR_RESIZE.value});this.cfg.addProperty(J.APPEND_TO_DOCUMENT_BODY.key,{value:J.APPEND_TO_DOCUMENT_BODY.value});},init:function(V,U){var S,W;this.initEvents();this.beforeInitEvent.fire(G);this.cfg=new D(this);if(this.isSecure){this.imageRoot=G.IMG_ROOT_SSL;}if(typeof V=="string"){S=V;V=document.getElementById(V);if(!V){V=(L()).cloneNode(false);V.id=S;}}this.id=F.generateId(V);this.element=V;W=this.element.firstChild;if(W){var R=false,Q=false,T=false;do{if(1==W.nodeType){if(!R&&F.hasClass(W,G.CSS_HEADER)){this.header=W;R=true;}else{if(!Q&&F.hasClass(W,G.CSS_BODY)){this.body=W;Q=true;}else{if(!T&&F.hasClass(W,G.CSS_FOOTER)){this.footer=W;T=true;}}}}}while((W=W.nextSibling));}this.initDefaultConfig();F.addClass(this.element,G.CSS_MODULE);if(U){this.cfg.applyConfig(U,true);}if(!D.alreadySubscribed(this.renderEvent,this.cfg.fireQueue,this.cfg)){this.renderEvent.subscribe(this.cfg.fireQueue,this.cfg,true);}this.initEvent.fire(G);},initResizeMonitor:function(){var R=(I.gecko&&this.platform=="windows");if(R){var Q=this;setTimeout(function(){Q._initResizeMonitor();},0);}else{this._initResizeMonitor();}},_initResizeMonitor:function(){var Q,S,U;function W(){G.textResizeEvent.fire();}if(!I.opera){S=F.get("_yuiResizeMonitor");var V=this._supportsCWResize();if(!S){S=document.createElement("iframe");if(this.isSecure&&G.RESIZE_MONITOR_SECURE_URL&&I.ie){S.src=G.RESIZE_MONITOR_SECURE_URL;}if(!V){U=[" - - - - - - - - - - -
    {title}
    - -
    - -
    - - - - - - - - - - - -
    -

    Class Coverage Distribution

    - - -
    -

    Class Complexity

    - - -
    -

    Top Project Risks

    -
      -{top_project_risks} -
    -
    -

    Least Tested Methods

    -
      -{least_tested_methods} -
    -
    -
    - -
    - - - - -
    Generated by PHP_CodeCoverage {version} using PHP {php_version}{generator} at {date}.
    - -
    - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/directory.html.dist b/PHP/CodeCoverage/Report/HTML/Template/directory.html.dist deleted file mode 100644 index fda0ed109..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/directory.html.dist +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - {title} - - - - - - - - - - - - - - -
    {title}
    - - - - - - - - - -
    Current directory:{link} ({dashboard_link})
    Legend: - - Low: 0% to {low_upper_bound}% - - - Medium: {low_upper_bound}% to {high_lower_bound}% - - - High: {high_lower_bound}% to 100% - -
    -
    - -
    - -
    - - - - - - - - - - - -{total_item}{items} -
     Coverage
     LinesFunctions / MethodsClasses
    -
    - -
    - - - - -
    Generated by PHP_CodeCoverage {version} using PHP {php_version}{generator} at {date}.
    - -
    - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/directory.png b/PHP/CodeCoverage/Report/HTML/Template/directory.png deleted file mode 100644 index 65bd0bbdc..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/directory.png and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/directory_item.html.dist b/PHP/CodeCoverage/Report/HTML/Template/directory_item.html.dist deleted file mode 100644 index a0065f9e5..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/directory_item.html.dist +++ /dev/null @@ -1,31 +0,0 @@ - - {icon}{name} - - - - - -
    {lines_executed_percent}{lines_executed_percent}
    - - {lines_executed_percent} - {num_executed_lines} / {num_executable_lines} - - - - - -
    {methods_tested_percent}{methods_tested_percent}
    - - {methods_tested_percent} - {methods_number} - - - - - -
    {classes_tested_percent}{classes_tested_percent}
    - - {classes_tested_percent} - {classes_number} - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/excanvas.compressed.js b/PHP/CodeCoverage/Report/HTML/Template/excanvas.compressed.js deleted file mode 100644 index fc3d35c73..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/excanvas.compressed.js and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/file.html.dist b/PHP/CodeCoverage/Report/HTML/Template/file.html.dist deleted file mode 100644 index a1a5766d1..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/file.html.dist +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - {title} - - - - - - - - - - - - - - - - - - -
    {title}
    - - - - - - - - - -
    Current file:{link}
    Legend: - executed - not executed - dead code -
    -
    - -
    - -
    - - - - - - - - - - - -{total_item}{items} -
     Coverage
     ClassesFunctions / MethodsLines
    -
    - -
    - - - - - - - - -

    -
    -{lines}
    -
    -
    - - - - -
    Generated by PHP_CodeCoverage {version} using PHP {php_version}{generator} at {date}.
    - -
    - - - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/file.png b/PHP/CodeCoverage/Report/HTML/Template/file.png deleted file mode 100644 index 2d7f2d601..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/file.png and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/file_item.html.dist b/PHP/CodeCoverage/Report/HTML/Template/file_item.html.dist deleted file mode 100644 index 938db30af..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/file_item.html.dist +++ /dev/null @@ -1,32 +0,0 @@ - - {name} - - - - - -
    {classes_tested_percent}{classes_tested_percent}
    - - {classes_tested_percent} - {classes_number} - - - - - -
    {methods_tested_percent}{methods_tested_percent}
    - - {methods_tested_percent} - {methods_number} - {crap} - - - - - -
    {lines_executed_percent}{lines_executed_percent}
    - - {lines_executed_percent} - {num_executed_lines} / {num_executable_lines} - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/file_no_yui.html.dist b/PHP/CodeCoverage/Report/HTML/Template/file_no_yui.html.dist deleted file mode 100644 index 9bdb84f7d..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/file_no_yui.html.dist +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - {title} - - - - - - - - - - - - - - - -
    {title}
    - - - - - - - - - -
    Current file:{link}
    Legend: - executed - not executed - dead code -
    -
    - -
    - -
    - - - - - - - - - - - -{total_item}{items} -
     Coverage
     ClassesFunctions / MethodsLines
    -
    - -
    - - - - - - - - -

    -
    -{lines}
    -
    -
    - - - - -
    Generated by PHP_CodeCoverage {version} using PHP {php_version}{generator} at {date}.
    - -
    - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/glass.png b/PHP/CodeCoverage/Report/HTML/Template/glass.png deleted file mode 100644 index e1abc0068..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/glass.png and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/method_item.html.dist b/PHP/CodeCoverage/Report/HTML/Template/method_item.html.dist deleted file mode 100644 index 1efeeb120..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/method_item.html.dist +++ /dev/null @@ -1,23 +0,0 @@ - - {name} - - - - - -
    {methods_tested_percent}{methods_tested_percent}
    - - {methods_tested_percent} - {methods_number} - {crap} - - - - - -
    {lines_executed_percent}{lines_executed_percent}
    - - {lines_executed_percent} - {num_executed_lines} / {num_executable_lines} - - diff --git a/PHP/CodeCoverage/Report/HTML/Template/scarlet_red.png b/PHP/CodeCoverage/Report/HTML/Template/scarlet_red.png deleted file mode 100644 index a879424d5..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/scarlet_red.png and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/snow.png b/PHP/CodeCoverage/Report/HTML/Template/snow.png deleted file mode 100644 index 2cdae107f..000000000 Binary files a/PHP/CodeCoverage/Report/HTML/Template/snow.png and /dev/null differ diff --git a/PHP/CodeCoverage/Report/HTML/Template/style.css b/PHP/CodeCoverage/Report/HTML/Template/style.css deleted file mode 100644 index 8c58ddcb7..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/style.css +++ /dev/null @@ -1,459 +0,0 @@ -/* All views: initial background and text color */ -body -{ - background-color: #fff; - color: #2e3436; - font-family: arial, helvetica, sans-serif; - font-size: 12px; - margin: 0 auto; - width: 100%; -} - -/* All views: standard link format*/ -a:link -{ - color: #2e3436; - text-decoration: underline; -} - -/* All views: standard link - visited format */ -a:visited -{ - color: #2e3436; - text-decoration: underline; -} - -/* All views: standard link - activated format */ -a:active -{ - color: #2e3436; - text-decoration: underline; -} - -/* All views: main title format */ -td.title -{ - text-align: center; - padding: 10px; - font-family: sans-serif; - font-style: italic; - font-weight: bold; - font-size: 1.6em; -} - -/* All views: header item format */ -td.headerItem -{ - text-align: right; - padding-right: 6px; - font-family: sans-serif; - font-weight: bold; -} - -/* All views: header item value format */ -td.headerValue -{ - text-align: left; - font-family: sans-serif; - font-weight: bold; -} - -/* All views: header legend item format */ -td.legendItem -{ - text-align: right; - padding-right: 6px; - padding-top: 10px; - padding-bottom: 2px; - font-family: sans-serif; - font-weight: bold; -} - -/* All views: header legend item value format */ -td.legendValue -{ - text-align: left; - padding-top: 10px; - padding-bottom: 2px; - color: #2e3436; - font-family: sans-serif; - font-weight: bold; -} - -/* All views: color of horizontal ruler */ -td.ruler -{ - background-color: #d3d7cf; -} - -/* All views: version string format */ -td.versionInfo -{ - text-align: center; - padding-top: 2px; - font-family: sans-serif; - font-style: italic; -} - -/* Directory view/File view (all)/Test case descriptions: -table headline format */ -td.tableHead -{ - text-align: center; - color: #ffffff; - background-color: #555753; - font-family: sans-serif; - font-weight: bold; -} - -/* Directory view/File view (all): filename entry format */ -td.coverItem, td.coverDirectory, td.coverFile -{ - text-align: left; - padding-left: 10px; - padding-right: 20px; - background-color: #d3d7cf; - font-family: monospace; -} - -td.coverDirectory -{ - font-weight: bold; -} - -/* Directory view/File view (all): bar-graph entry format*/ -td.coverBar -{ - padding-left: 10px; - padding-right: 10px; - background-color: #d3d7cf; -} - -/* Directory view/File view (all): bar-graph outline color */ -td.coverBarOutline -{ - background-color: #2e3436; -} - -/* Directory view/File view (all): percentage entry for files with -no coverage rate */ -td.coverPerNone -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #d3d7cf; - font-weight: bold; -} - -/* Directory view/File view (all): line count entry for files with -no coverage rate */ -td.coverNumNone -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #d3d7cf; - white-space: nowrap; -} - -/* Directory view/File view (all): percentage entry for files with -high coverage rate */ -td.coverPerHi -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #8ae234; - font-weight: bold; -} - -/* Directory view/File view (all): line count entry for files with -high coverage rate */ -td.coverNumHi -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #8ae234; - white-space: nowrap; -} - -/* Directory view/File view (all): legend entry for high coverage -rate */ -span.coverLegendHi -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - background-color: #8ae234; -} - -/* Directory view/File view (all): percentage entry for files with -medium coverage rate */ -td.coverPerMed -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #fce94f; - font-weight: bold; -} - -/* Directory view/File view (all): line count entry for files with -medium coverage rate */ -td.coverNumMed -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #fce94f; - white-space: nowrap; -} - -/* Directory view/File view (all): legend entry for medium coverage -rate */ -span.coverLegendMed -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - margin-top: 5px; - margin-bottom: 5px; - margin-right: 2px; - background-color: #fce94f; -} - -/* Directory view/File view (all): percentage entry for files with -low coverage rate */ -td.coverPerLo -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #f57900; - font-weight: bold; -} - -/* Directory view/File view (all): line count entry for files with -low coverage rate */ -td.coverNumLo -{ - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #f57900; - white-space: nowrap; -} - -/* Directory view/File view (all): legend entry for low coverage -rate */ -span.coverLegendLo -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - margin-right: 2px; - background-color: #f57900; -} - -/* File view (all): "show/hide details" link format */ -a.detail:link -{ - color: #ffffff; -} - -/* File view (all): "show/hide details" link - visited format */ -a.detail:visited -{ - color: #ffffff; -} - -/* File view (all): "show/hide details" link - activated format */ -a.detail:active -{ - color: #ffffff; -} - -/* File view (detail): test name table headline format */ -td.testNameHead -{ - text-align: left; - padding-left: 10px; - background-color: #729fcf; - font-family: sans-serif; - font-weight: bold; -} - -/* File view (detail): test lines table headline format */ -td.testLinesHead -{ - text-align: center; - background-color: #729fcf; - font-family: sans-serif; - font-weight: bold; -} - -/* File view (detail): test name entry */ -td.testName -{ - text-align: left; - padding-left: 10px; - background-color: #729fcf; -} - -/* File view (detail): test percentage entry */ -td.testPer -{ - text-align: right; - vertical-align: top; - padding-left: 10px; - padding-right: 10px; - background-color: #729fcf; -} - -/* File view (detail): test lines count entry */ -td.testNum -{ - text-align: right; - vertical-align: top; - padding-left: 10px; - padding-right: 10px; - background-color: #729fcf; - white-space: nowrap; -} - -/* Test case descriptions: test name format*/ -dt -{ - font-family: sans-serif; - font-weight: bold; -} - -/* Test case descriptions: description table body */ -td.testDescription -{ - padding-top: 10px; - padding-left: 30px; - padding-bottom: 10px; - padding-right: 30px; - background-color: #729fcf; -} - -/* Source code view: source code format */ -pre.source -{ - font-family: monospace; - white-space: pre; -} - -/* Source code view: line number format */ -span.lineNum -{ - background-color: #d3d7cf; -} - -span.lineNum a { - text-decoration: none; -} - -/* Source code view: format for lines which were executed */ -span.lineCov -{ - background-color: #8ae234; -} - -/* Source code view: format for Cov legend */ -span.LegendCov -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - margin-right: 2px; - background-color: #8ae234; -} - -/* Source code view: format for lines which were not executed */ -span.lineNoCov -{ - background-color: #f57900; -} - -/* Source code view: format for NoCov legend */ -span.LegendNoCov -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - margin-right: 2px; - background-color: #f57900; -} - -/* Source code view: format for lines which are dead code */ -span.lineDeadCode -{ - background-color: #d3d7cf; -} - -/* Source code view: format for NoCov legend */ -span.LegendDeadCode -{ - text-align: center; - padding-left: 10px; - padding-right: 10px; - margin-right: 2px; - background-color: #d3d7cf; -} - -/* Test view: format for tests which have passed */ -li.testPassed -{ -} - -/* Test view: format for tests which failed */ -li.testFailure -{ - background-color: #f57900; -} - -/* Test view: format for tests which failed with an error */ -li.testError -{ - background-color: #f57900; -} - -/* Test view: format for incomplete and skipped tests */ -li.testIncomplete -{ - background-color: #fcaf3e; -} - -/* CRAP */ -td.crap -{ - text-align: right; - padding-left: 10px; - padding-right: 20px; - background-color: #d3d7cf; -} - -pre span.comment { - color: #888a85; -} - -pre span.default { - color: #2e3436; -} - -pre span.html { - color: #888a85; -} - -pre span.keyword { - color: #2e3436; - font-weight: bold; -} - -pre span.string { - color: #2e3436; -} diff --git a/PHP/CodeCoverage/Report/HTML/Template/yahoo-dom-event.js b/PHP/CodeCoverage/Report/HTML/Template/yahoo-dom-event.js deleted file mode 100644 index 8ab7c8685..000000000 --- a/PHP/CodeCoverage/Report/HTML/Template/yahoo-dom-event.js +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright (c) 2010, Yahoo! Inc. All rights reserved. -Code licensed under the BSD License: -http://developer.yahoo.com/yui/license.html -version: 2.8.2r1 -*/ -if(typeof YAHOO=="undefined"||!YAHOO){var YAHOO={};}YAHOO.namespace=function(){var A=arguments,E=null,C,B,D;for(C=0;C0)?B.dump(I[K],N-1):Q);}else{P.push(I[K]);}P.push(O);}if(P.length>1){P.pop();}P.push("]");}else{P.push("{");for(K in I){if(B.hasOwnProperty(I,K)){P.push(K+L);if(B.isObject(I[K])){P.push((N>0)?B.dump(I[K],N-1):Q);}else{P.push(I[K]);}P.push(O);}}if(P.length>1){P.pop();}P.push("}");}return P.join("");},substitute:function(Y,J,R){var N,M,L,U,V,X,T=[],K,O="dump",S=" ",I="{",W="}",Q,P;for(;;){N=Y.lastIndexOf(I);if(N<0){break;}M=Y.indexOf(W,N);if(N+1>=M){break;}K=Y.substring(N+1,M);U=K;X=null;L=U.indexOf(S);if(L>-1){X=U.substring(L+1);U=U.substring(0,L);}V=J[U];if(R){V=R(U,V,X);}if(B.isObject(V)){if(B.isArray(V)){V=B.dump(V,parseInt(X,10));}else{X=X||"";Q=X.indexOf(O);if(Q>-1){X=X.substring(4);}P=V.toString();if(P===G||Q>-1){V=B.dump(V,parseInt(X,10));}else{V=P;}}}else{if(!B.isString(V)&&!B.isNumber(V)){V="~-"+T.length+"-~";T[T.length]=K;}}Y=Y.substring(0,N)+V+Y.substring(M+1);}for(N=T.length-1;N>=0;N=N-1){Y=Y.replace(new RegExp("~-"+N+"-~"),"{"+T[N]+"}","g");}return Y;},trim:function(I){try{return I.replace(/^\s+|\s+$/g,"");}catch(J){return I;}},merge:function(){var L={},J=arguments,I=J.length,K;for(K=0;K519)?true:false);while((G=G[u])){z[0]+=G[b];z[1]+=G[P];if(AC){z=E.Dom._calcBorders(G,z);}}if(E.Dom._getStyle(y,p)!==f){G=y;while((G=G[Z])&&G[C]){AA=G[i];AB=G[O];if(H&&(E.Dom._getStyle(G,"overflow")!=="visible")){z=E.Dom._calcBorders(G,z);}if(AA||AB){z[0]-=AB;z[1]-=AA;}}z[0]+=x;z[1]+=Y;}else{if(D){z[0]-=x;z[1]-=Y;}else{if(I||H){z[0]+=x;z[1]+=Y;}}}z[0]=Math.floor(z[0]);z[1]=Math.floor(z[1]);}else{}return z;};}}(),getX:function(G){var Y=function(x){return E.Dom.getXY(x)[0];};return E.Dom.batch(G,Y,E.Dom,true);},getY:function(G){var Y=function(x){return E.Dom.getXY(x)[1];};return E.Dom.batch(G,Y,E.Dom,true);},setXY:function(G,x,Y){E.Dom.batch(G,E.Dom._setXY,{pos:x,noRetry:Y});},_setXY:function(G,z){var AA=E.Dom._getStyle(G,p),y=E.Dom.setStyle,AD=z.pos,Y=z.noRetry,AB=[parseInt(E.Dom.getComputedStyle(G,j),10),parseInt(E.Dom.getComputedStyle(G,o),10)],AC,x;if(AA=="static"){AA=V;y(G,p,AA);}AC=E.Dom._getXY(G);if(!AD||AC===false){return false;}if(isNaN(AB[0])){AB[0]=(AA==V)?0:G[b];}if(isNaN(AB[1])){AB[1]=(AA==V)?0:G[P];}if(AD[0]!==null){y(G,j,AD[0]-AC[0]+AB[0]+"px");}if(AD[1]!==null){y(G,o,AD[1]-AC[1]+AB[1]+"px");}if(!Y){x=E.Dom._getXY(G);if((AD[0]!==null&&x[0]!=AD[0])||(AD[1]!==null&&x[1]!=AD[1])){E.Dom._setXY(G,{pos:AD,noRetry:true});}}},setX:function(Y,G){E.Dom.setXY(Y,[G,null]);},setY:function(G,Y){E.Dom.setXY(G,[null,Y]);},getRegion:function(G){var Y=function(x){var y=false;if(E.Dom._canPosition(x)){y=E.Region.getRegion(x);}else{}return y;};return E.Dom.batch(G,Y,E.Dom,true);},getClientWidth:function(){return E.Dom.getViewportWidth();},getClientHeight:function(){return E.Dom.getViewportHeight();},getElementsByClassName:function(AB,AF,AC,AE,x,AD){AF=AF||"*";AC=(AC)?E.Dom.get(AC):null||K;if(!AC){return[];}var Y=[],G=AC.getElementsByTagName(AF),z=E.Dom.hasClass;for(var y=0,AA=G.length;y-1;}}else{}return G;},addClass:function(Y,G){return E.Dom.batch(Y,E.Dom._addClass,G);},_addClass:function(x,Y){var G=false,y;if(x&&Y){y=E.Dom._getAttribute(x,F)||J;if(!E.Dom._hasClass(x,Y)){E.Dom.setAttribute(x,F,A(y+B+Y));G=true;}}else{}return G;},removeClass:function(Y,G){return E.Dom.batch(Y,E.Dom._removeClass,G);},_removeClass:function(y,x){var Y=false,AA,z,G;if(y&&x){AA=E.Dom._getAttribute(y,F)||J;E.Dom.setAttribute(y,F,AA.replace(E.Dom._getClassRegex(x),J));z=E.Dom._getAttribute(y,F);if(AA!==z){E.Dom.setAttribute(y,F,A(z));Y=true;if(E.Dom._getAttribute(y,F)===""){G=(y.hasAttribute&&y.hasAttribute(g))?g:F; -y.removeAttribute(G);}}}else{}return Y;},replaceClass:function(x,Y,G){return E.Dom.batch(x,E.Dom._replaceClass,{from:Y,to:G});},_replaceClass:function(y,x){var Y,AB,AA,G=false,z;if(y&&x){AB=x.from;AA=x.to;if(!AA){G=false;}else{if(!AB){G=E.Dom._addClass(y,x.to);}else{if(AB!==AA){z=E.Dom._getAttribute(y,F)||J;Y=(B+z.replace(E.Dom._getClassRegex(AB),B+AA)).split(E.Dom._getClassRegex(AA));Y.splice(1,0,B+AA);E.Dom.setAttribute(y,F,A(Y.join(J)));G=true;}}}}else{}return G;},generateId:function(G,x){x=x||"yui-gen";var Y=function(y){if(y&&y.id){return y.id;}var z=x+YAHOO.env._id_counter++;if(y){if(y[e]&&y[e].getElementById(z)){return E.Dom.generateId(y,z+x);}y.id=z;}return z;};return E.Dom.batch(G,Y,E.Dom,true)||Y.apply(E.Dom,arguments);},isAncestor:function(Y,x){Y=E.Dom.get(Y);x=E.Dom.get(x);var G=false;if((Y&&x)&&(Y[l]&&x[l])){if(Y.contains&&Y!==x){G=Y.contains(x);}else{if(Y.compareDocumentPosition){G=!!(Y.compareDocumentPosition(x)&16);}}}else{}return G;},inDocument:function(G,Y){return E.Dom._inDoc(E.Dom.get(G),Y);},_inDoc:function(Y,x){var G=false;if(Y&&Y[C]){x=x||Y[e];G=E.Dom.isAncestor(x[v],Y);}else{}return G;},getElementsBy:function(Y,AF,AB,AD,y,AC,AE){AF=AF||"*";AB=(AB)?E.Dom.get(AB):null||K;if(!AB){return[];}var x=[],G=AB.getElementsByTagName(AF);for(var z=0,AA=G.length;z=8&&K.documentElement.hasAttribute){E.Dom.DOT_ATTRIBUTES.type=true;}})();YAHOO.util.Region=function(C,D,A,B){this.top=C;this.y=C;this[1]=C;this.right=D;this.bottom=A;this.left=B;this.x=B;this[0]=B; -this.width=this.right-this.left;this.height=this.bottom-this.top;};YAHOO.util.Region.prototype.contains=function(A){return(A.left>=this.left&&A.right<=this.right&&A.top>=this.top&&A.bottom<=this.bottom);};YAHOO.util.Region.prototype.getArea=function(){return((this.bottom-this.top)*(this.right-this.left));};YAHOO.util.Region.prototype.intersect=function(E){var C=Math.max(this.top,E.top),D=Math.min(this.right,E.right),A=Math.min(this.bottom,E.bottom),B=Math.max(this.left,E.left);if(A>=C&&D>=B){return new YAHOO.util.Region(C,D,A,B);}else{return null;}};YAHOO.util.Region.prototype.union=function(E){var C=Math.min(this.top,E.top),D=Math.max(this.right,E.right),A=Math.max(this.bottom,E.bottom),B=Math.min(this.left,E.left);return new YAHOO.util.Region(C,D,A,B);};YAHOO.util.Region.prototype.toString=function(){return("Region {"+"top: "+this.top+", right: "+this.right+", bottom: "+this.bottom+", left: "+this.left+", height: "+this.height+", width: "+this.width+"}");};YAHOO.util.Region.getRegion=function(D){var F=YAHOO.util.Dom.getXY(D),C=F[1],E=F[0]+D.offsetWidth,A=F[1]+D.offsetHeight,B=F[0];return new YAHOO.util.Region(C,E,A,B);};YAHOO.util.Point=function(A,B){if(YAHOO.lang.isArray(A)){B=A[1];A=A[0];}YAHOO.util.Point.superclass.constructor.call(this,B,A,B,A);};YAHOO.extend(YAHOO.util.Point,YAHOO.util.Region);(function(){var B=YAHOO.util,A="clientTop",F="clientLeft",J="parentNode",K="right",W="hasLayout",I="px",U="opacity",L="auto",D="borderLeftWidth",G="borderTopWidth",P="borderRightWidth",V="borderBottomWidth",S="visible",Q="transparent",N="height",E="width",H="style",T="currentStyle",R=/^width|height$/,O=/^(\d[.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz|%){1}?/i,M={get:function(X,Z){var Y="",a=X[T][Z];if(Z===U){Y=B.Dom.getStyle(X,U);}else{if(!a||(a.indexOf&&a.indexOf(I)>-1)){Y=a;}else{if(B.Dom.IE_COMPUTED[Z]){Y=B.Dom.IE_COMPUTED[Z](X,Z);}else{if(O.test(a)){Y=B.Dom.IE.ComputedStyle.getPixel(X,Z);}else{Y=a;}}}}return Y;},getOffset:function(Z,e){var b=Z[T][e],X=e.charAt(0).toUpperCase()+e.substr(1),c="offset"+X,Y="pixel"+X,a="",d;if(b==L){d=Z[c];if(d===undefined){a=0;}a=d;if(R.test(e)){Z[H][e]=d;if(Z[c]>d){a=d-(Z[c]-d);}Z[H][e]=L;}}else{if(!Z[H][Y]&&!Z[H][e]){Z[H][e]=b;}a=Z[H][Y];}return a+I;},getBorderWidth:function(X,Z){var Y=null;if(!X[T][W]){X[H].zoom=1;}switch(Z){case G:Y=X[A];break;case V:Y=X.offsetHeight-X.clientHeight-X[A];break;case D:Y=X[F];break;case P:Y=X.offsetWidth-X.clientWidth-X[F];break;}return Y+I;},getPixel:function(Y,X){var a=null,b=Y[T][K],Z=Y[T][X];Y[H][K]=Z;a=Y[H].pixelRight;Y[H][K]=b;return a+I;},getMargin:function(Y,X){var Z;if(Y[T][X]==L){Z=0+I;}else{Z=B.Dom.IE.ComputedStyle.getPixel(Y,X);}return Z;},getVisibility:function(Y,X){var Z;while((Z=Y[T])&&Z[X]=="inherit"){Y=Y[J];}return(Z)?Z[X]:S;},getColor:function(Y,X){return B.Dom.Color.toRGB(Y[T][X])||Q;},getBorderColor:function(Y,X){var Z=Y[T],a=Z[X]||Z.color;return B.Dom.Color.toRGB(B.Dom.Color.toHex(a));}},C={};C.top=C.right=C.bottom=C.left=C[E]=C[N]=M.getOffset;C.color=M.getColor;C[G]=C[P]=C[V]=C[D]=M.getBorderWidth;C.marginTop=C.marginRight=C.marginBottom=C.marginLeft=M.getMargin;C.visibility=M.getVisibility;C.borderColor=C.borderTopColor=C.borderRightColor=C.borderBottomColor=C.borderLeftColor=M.getBorderColor;B.Dom.IE_COMPUTED=C;B.Dom.IE_ComputedStyle=M;})();(function(){var C="toString",A=parseInt,B=RegExp,D=YAHOO.util;D.Dom.Color={KEYWORDS:{black:"000",silver:"c0c0c0",gray:"808080",white:"fff",maroon:"800000",red:"f00",purple:"800080",fuchsia:"f0f",green:"008000",lime:"0f0",olive:"808000",yellow:"ff0",navy:"000080",blue:"00f",teal:"008080",aqua:"0ff"},re_RGB:/^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,re_hex:/^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,re_hex3:/([0-9A-F])/gi,toRGB:function(E){if(!D.Dom.Color.re_RGB.test(E)){E=D.Dom.Color.toHex(E);}if(D.Dom.Color.re_hex.exec(E)){E="rgb("+[A(B.$1,16),A(B.$2,16),A(B.$3,16)].join(", ")+")";}return E;},toHex:function(H){H=D.Dom.Color.KEYWORDS[H]||H;if(D.Dom.Color.re_RGB.exec(H)){var G=(B.$1.length===1)?"0"+B.$1:Number(B.$1),F=(B.$2.length===1)?"0"+B.$2:Number(B.$2),E=(B.$3.length===1)?"0"+B.$3:Number(B.$3);H=[G[C](16),F[C](16),E[C](16)].join("");}if(H.length<6){H=H.replace(D.Dom.Color.re_hex3,"$1$1");}if(H!=="transparent"&&H.indexOf("#")<0){H="#"+H;}return H.toLowerCase();}};}());YAHOO.register("dom",YAHOO.util.Dom,{version:"2.8.2r1",build:"7"});YAHOO.util.CustomEvent=function(D,C,B,A,E){this.type=D;this.scope=C||window;this.silent=B;this.fireOnce=E;this.fired=false;this.firedWith=null;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent){}var F="_YUICEOnSubscribe";if(D!==F){this.subscribeEvent=new YAHOO.util.CustomEvent(F,this,true);}this.lastError=null;};YAHOO.util.CustomEvent.LIST=0;YAHOO.util.CustomEvent.FLAT=1;YAHOO.util.CustomEvent.prototype={subscribe:function(B,C,D){if(!B){throw new Error("Invalid callback for subscriber to '"+this.type+"'");}if(this.subscribeEvent){this.subscribeEvent.fire(B,C,D);}var A=new YAHOO.util.Subscriber(B,C,D);if(this.fireOnce&&this.fired){this.notify(A,this.firedWith);}else{this.subscribers.push(A);}},unsubscribe:function(D,F){if(!D){return this.unsubscribeAll();}var E=false;for(var B=0,A=this.subscribers.length;B0){H=C[0];}try{B=F.fn.call(E,H,F.obj);}catch(G){this.lastError=G;if(A){throw G;}}}else{try{B=F.fn.call(E,this.type,C,F.obj);}catch(D){this.lastError=D;if(A){throw D;}}}return B;},unsubscribeAll:function(){var A=this.subscribers.length,B;for(B=A-1;B>-1;B--){this._delete(B);}this.subscribers=[];return A;},_delete:function(A){var B=this.subscribers[A];if(B){delete B.fn;delete B.obj;}this.subscribers.splice(A,1);},toString:function(){return"CustomEvent: "+"'"+this.type+"', "+"context: "+this.scope;}};YAHOO.util.Subscriber=function(A,B,C){this.fn=A;this.obj=YAHOO.lang.isUndefined(B)?null:B;this.overrideContext=C;};YAHOO.util.Subscriber.prototype.getScope=function(A){if(this.overrideContext){if(this.overrideContext===true){return this.obj;}else{return this.overrideContext;}}return A;};YAHOO.util.Subscriber.prototype.contains=function(A,B){if(B){return(this.fn==A&&this.obj==B);}else{return(this.fn==A);}};YAHOO.util.Subscriber.prototype.toString=function(){return"Subscriber { obj: "+this.obj+", overrideContext: "+(this.overrideContext||"no")+" }";};if(!YAHOO.util.Event){YAHOO.util.Event=function(){var G=false,H=[],J=[],A=0,E=[],B=0,C={63232:38,63233:40,63234:37,63235:39,63276:33,63277:34,25:9},D=YAHOO.env.ua.ie,F="focusin",I="focusout";return{POLL_RETRYS:500,POLL_INTERVAL:40,EL:0,TYPE:1,FN:2,WFN:3,UNLOAD_OBJ:3,ADJ_SCOPE:4,OBJ:5,OVERRIDE:6,CAPTURE:7,lastError:null,isSafari:YAHOO.env.ua.webkit,webkit:YAHOO.env.ua.webkit,isIE:D,_interval:null,_dri:null,_specialTypes:{focusin:(D?"focusin":"focus"),focusout:(D?"focusout":"blur")},DOMReady:false,throwErrors:false,startInterval:function(){if(!this._interval){this._interval=YAHOO.lang.later(this.POLL_INTERVAL,this,this._tryPreloadAttach,null,true);}},onAvailable:function(Q,M,O,P,N){var K=(YAHOO.lang.isString(Q))?[Q]:Q;for(var L=0;L-1;M--){S=(this.removeListener(L[M],K,R)&&S);}return S;}}if(!R||!R.call){return this.purgeElement(L,false,K);}if("unload"==K){for(M=J.length-1;M>-1;M--){U=J[M];if(U&&U[0]==L&&U[1]==K&&U[2]==R){J.splice(M,1);return true;}}return false;}var N=null;var O=arguments[3];if("undefined"===typeof O){O=this._getCacheIndex(H,L,K,R);}if(O>=0){N=H[O];}if(!L||!N){return false;}var T=N[this.CAPTURE]===true?true:false;try{this._simpleRemove(L,K,N[this.WFN],T);}catch(Q){this.lastError=Q;return false;}delete H[O][this.WFN];delete H[O][this.FN];H.splice(O,1);return true;},getTarget:function(M,L){var K=M.target||M.srcElement;return this.resolveTextNode(K);},resolveTextNode:function(L){try{if(L&&3==L.nodeType){return L.parentNode;}}catch(K){}return L;},getPageX:function(L){var K=L.pageX;if(!K&&0!==K){K=L.clientX||0;if(this.isIE){K+=this._getScrollLeft();}}return K;},getPageY:function(K){var L=K.pageY;if(!L&&0!==L){L=K.clientY||0;if(this.isIE){L+=this._getScrollTop();}}return L;},getXY:function(K){return[this.getPageX(K),this.getPageY(K)];},getRelatedTarget:function(L){var K=L.relatedTarget;if(!K){if(L.type=="mouseout"){K=L.toElement; -}else{if(L.type=="mouseover"){K=L.fromElement;}}}return this.resolveTextNode(K);},getTime:function(M){if(!M.time){var L=new Date().getTime();try{M.time=L;}catch(K){this.lastError=K;return L;}}return M.time;},stopEvent:function(K){this.stopPropagation(K);this.preventDefault(K);},stopPropagation:function(K){if(K.stopPropagation){K.stopPropagation();}else{K.cancelBubble=true;}},preventDefault:function(K){if(K.preventDefault){K.preventDefault();}else{K.returnValue=false;}},getEvent:function(M,K){var L=M||window.event;if(!L){var N=this.getEvent.caller;while(N){L=N.arguments[0];if(L&&Event==L.constructor){break;}N=N.caller;}}return L;},getCharCode:function(L){var K=L.keyCode||L.charCode||0;if(YAHOO.env.ua.webkit&&(K in C)){K=C[K];}return K;},_getCacheIndex:function(M,P,Q,O){for(var N=0,L=M.length;N0&&E.length>0);}var P=[];var R=function(T,U){var S=T;if(U.overrideContext){if(U.overrideContext===true){S=U.obj;}else{S=U.overrideContext;}}U.fn.call(S,U.obj);};var L,K,O,N,M=[];for(L=0,K=E.length;L-1;L--){O=E[L];if(!O||!O.id){E.splice(L,1);}}this.startInterval();}else{if(this._interval){this._interval.cancel();this._interval=null;}}this.locked=false;},purgeElement:function(O,P,R){var M=(YAHOO.lang.isString(O))?this.getEl(O):O;var Q=this.getListeners(M,R),N,K;if(Q){for(N=Q.length-1;N>-1;N--){var L=Q[N];this.removeListener(M,L.type,L.fn);}}if(P&&M&&M.childNodes){for(N=0,K=M.childNodes.length;N-1;N--){M=H[N];if(M){L.removeListener(M[L.EL],M[L.TYPE],M[L.FN],N);}}M=null;}L._simpleRemove(window,"unload",L._unload);},_getScrollLeft:function(){return this._getScroll()[1];},_getScrollTop:function(){return this._getScroll()[0];},_getScroll:function(){var K=document.documentElement,L=document.body;if(K&&(K.scrollTop||K.scrollLeft)){return[K.scrollTop,K.scrollLeft];}else{if(L){return[L.scrollTop,L.scrollLeft];}else{return[0,0];}}},regCE:function(){},_simpleAdd:function(){if(window.addEventListener){return function(M,N,L,K){M.addEventListener(N,L,(K));};}else{if(window.attachEvent){return function(M,N,L,K){M.attachEvent("on"+N,L);};}else{return function(){};}}}(),_simpleRemove:function(){if(window.removeEventListener){return function(M,N,L,K){M.removeEventListener(N,L,(K));};}else{if(window.detachEvent){return function(L,M,K){L.detachEvent("on"+M,K);};}else{return function(){};}}}()};}();(function(){var EU=YAHOO.util.Event;EU.on=EU.addListener;EU.onFocus=EU.addFocusListener;EU.onBlur=EU.addBlurListener; -/* DOMReady: based on work by: Dean Edwards/John Resig/Matthias Miller/Diego Perini */ -if(EU.isIE){if(self!==self.top){document.onreadystatechange=function(){if(document.readyState=="complete"){document.onreadystatechange=null;EU._ready();}};}else{YAHOO.util.Event.onDOMReady(YAHOO.util.Event._tryPreloadAttach,YAHOO.util.Event,true);var n=document.createElement("p");EU._dri=setInterval(function(){try{n.doScroll("left");clearInterval(EU._dri);EU._dri=null;EU._ready();n=null;}catch(ex){}},EU.POLL_INTERVAL);}}else{if(EU.webkit&&EU.webkit<525){EU._dri=setInterval(function(){var rs=document.readyState;if("loaded"==rs||"complete"==rs){clearInterval(EU._dri);EU._dri=null;EU._ready();}},EU.POLL_INTERVAL);}else{EU._simpleAdd(document,"DOMContentLoaded",EU._ready);}}EU._simpleAdd(window,"load",EU._load);EU._simpleAdd(window,"unload",EU._unload);EU._tryPreloadAttach();})();}YAHOO.util.EventProvider=function(){};YAHOO.util.EventProvider.prototype={__yui_events:null,__yui_subscribers:null,subscribe:function(A,C,F,E){this.__yui_events=this.__yui_events||{};var D=this.__yui_events[A];if(D){D.subscribe(C,F,E);}else{this.__yui_subscribers=this.__yui_subscribers||{};var B=this.__yui_subscribers;if(!B[A]){B[A]=[];}B[A].push({fn:C,obj:F,overrideContext:E});}},unsubscribe:function(C,E,G){this.__yui_events=this.__yui_events||{};var A=this.__yui_events;if(C){var F=A[C];if(F){return F.unsubscribe(E,G);}}else{var B=true;for(var D in A){if(YAHOO.lang.hasOwnProperty(A,D)){B=B&&A[D].unsubscribe(E,G);}}return B;}return false;},unsubscribeAll:function(A){return this.unsubscribe(A); -},createEvent:function(B,G){this.__yui_events=this.__yui_events||{};var E=G||{},D=this.__yui_events,F;if(D[B]){}else{F=new YAHOO.util.CustomEvent(B,E.scope||this,E.silent,YAHOO.util.CustomEvent.FLAT,E.fireOnce);D[B]=F;if(E.onSubscribeCallback){F.subscribeEvent.subscribe(E.onSubscribeCallback);}this.__yui_subscribers=this.__yui_subscribers||{};var A=this.__yui_subscribers[B];if(A){for(var C=0;C{tests}", - "footer": "" - }, diff --git a/PHP/CodeCoverage/TextUI/Command.php b/PHP/CodeCoverage/TextUI/Command.php deleted file mode 100644 index f3981052b..000000000 --- a/PHP/CodeCoverage/TextUI/Command.php +++ /dev/null @@ -1,268 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage.php'; - -require_once 'ezc/Base/base.php'; -spl_autoload_register(array('ezcBase', 'autoload')); - -/** - * TextUI frontend for PHP_CodeCoverage. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_TextUI_Command -{ - /** - * Main method. - */ - public static function main() - { - $input = new ezcConsoleInput; - - $input->registerOption( - new ezcConsoleOption( - '', - 'clover', - ezcConsoleInput::TYPE_STRING - ) - ); - - $input->registerOption( - new ezcConsoleOption( - '', - 'html', - ezcConsoleInput::TYPE_STRING - ) - ); - - $input->registerOption( - new ezcConsoleOption( - '', - 'blacklist', - ezcConsoleInput::TYPE_STRING, - array(), - TRUE - ) - ); - - $input->registerOption( - new ezcConsoleOption( - '', - 'whitelist', - ezcConsoleInput::TYPE_STRING, - array(), - TRUE - ) - ); - - $input->registerOption( - new ezcConsoleOption( - 'h', - 'help', - ezcConsoleInput::TYPE_NONE, - NULL, - FALSE, - '', - '', - array(), - array(), - FALSE, - FALSE, - TRUE - ) - ); - - $input->registerOption( - new ezcConsoleOption( - 'v', - 'version', - ezcConsoleInput::TYPE_NONE, - NULL, - FALSE, - '', - '', - array(), - array(), - FALSE, - FALSE, - TRUE - ) - ); - - try { - $input->process(); - } - - catch (ezcConsoleOptionException $e) { - print $e->getMessage() . "\n"; - exit(1); - } - - if ($input->getOption('help')->value) { - self::showHelp(); - exit(0); - } - - else if ($input->getOption('version')->value) { - self::printVersionString(); - exit(0); - } - - $arguments = $input->getArguments(); - $clover = $input->getOption('clover')->value; - $html = $input->getOption('html')->value; - $blacklist = $input->getOption('blacklist')->value; - $whitelist = $input->getOption('whitelist')->value; - - if (count($arguments) == 1) { - self::printVersionString(); - - $coverage = new PHP_CodeCoverage; - $filter = $coverage->filter(); - - if (empty($whitelist)) { - $c = new ReflectionClass('ezcBase'); - $filter->addDirectoryToBlacklist(dirname($c->getFileName())); - $c = new ReflectionClass('ezcConsoleInput'); - $filter->addDirectoryToBlacklist(dirname($c->getFileName())); - - foreach ($blacklist as $item) { - if (is_dir($item)) { - $filter->addDirectoryToBlacklist($item); - } - - else if (is_file($item)) { - $filter->addFileToBlacklist($item); - } - } - } else { - foreach ($whitelist as $item) { - if (is_dir($item)) { - $filter->addDirectoryToWhitelist($item); - } - - else if (is_file($item)) { - $filter->addFileToWhitelist($item); - } - } - } - - $coverage->start('phpcov'); - - require $arguments[0]; - - $coverage->stop(); - - if ($clover) { - require 'PHP/CodeCoverage/Report/Clover.php'; - - $writer = new PHP_CodeCoverage_Report_Clover; - $writer->process($coverage, $clover); - } - - if ($html) { - require 'PHP/CodeCoverage/Report/HTML.php'; - - $writer = new PHP_CodeCoverage_Report_HTML; - $writer->process($coverage, $html); - } - } else { - self::showHelp(); - exit(1); - } - } - - /** - * Shows an error. - * - * @param string $message - */ - protected static function showError($message) - { - self::printVersionString(); - - print $message; - - exit(1); - } - - /** - * Shows the help. - */ - protected static function showHelp() - { - self::printVersionString(); - - print << - - --clover Write code coverage data in Clover XML format. - --html Generate code coverage report in HTML format. - - --blacklist Adds to the blacklist. - --whitelist Adds to the whitelist. - - --help Prints this usage information. - --version Prints the version and exits. - -EOT; - } - - /** - * Prints the version string. - */ - protected static function printVersionString() - { - print "phpcov @package_version@ by Sebastian Bergmann.\n\n"; - } -} diff --git a/PHP/CodeCoverage/Util.php b/PHP/CodeCoverage/Util.php deleted file mode 100644 index a08fa6815..000000000 --- a/PHP/CodeCoverage/Util.php +++ /dev/null @@ -1,643 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -if (!defined('T_NAMESPACE')) { - define('T_NAMESPACE', 377); -} - -require_once 'PHP/Token/Stream/CachingFactory.php'; - -/** - * Utility methods. - * - * @category PHP - * @package CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Util -{ - /** - * @var string - */ - const REGEX = '(@covers\s+(?P.*?)\s*$)m'; - - /** - * @var array - */ - protected static $ignoredLines = array(); - - /** - * @var array - */ - protected static $templateMethods = array( - 'setUp', 'assertPreConditions', 'assertPostConditions', 'tearDown' - ); - - /** - * Builds an array representation of the directory structure. - * - * For instance, - * - * - * Array - * ( - * [Money.php] => Array - * ( - * ... - * ) - * - * [MoneyBag.php] => Array - * ( - * ... - * ) - * ) - * - * - * is transformed into - * - * - * Array - * ( - * [.] => Array - * ( - * [Money.php] => Array - * ( - * ... - * ) - * - * [MoneyBag.php] => Array - * ( - * ... - * ) - * ) - * ) - * - * - * @param array $files - * @return array - */ - public static function buildDirectoryStructure($files) - { - $result = array(); - - foreach ($files as $path => $file) { - $path = explode('/', $path); - $pointer = &$result; - $max = count($path); - - for ($i = 0; $i < $max; $i++) { - if ($i == ($max - 1)) { - $type = '/f'; - } else { - $type = ''; - } - - $pointer = &$pointer[$path[$i] . $type]; - } - - $pointer = $file; - } - - return $result; - } - - /** - * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code - * based on its cyclomatic complexity and percentage of code coverage. - * - * @param integer $ccn - * @param float $coverage - * @return string - */ - public static function crap($ccn, $coverage) - { - if ($coverage == 0) { - return (string)pow($ccn, 2) + $ccn; - } - - if ($coverage >= 95) { - return (string)$ccn; - } - - return sprintf( - '%01.2F', pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn - ); - } - - /** - * @param string $directory - * @return string - * @throws RuntimeException - */ - public static function getDirectory($directory) - { - if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) { - $directory .= DIRECTORY_SEPARATOR; - } - - if (is_dir($directory)) { - return $directory; - } - - if (mkdir($directory, 0777, TRUE)) { - return $directory; - } - - throw new RuntimeException( - sprintf( - 'Directory "%s" does not exist.', - $directory - ) - ); - } - - /** - * Returns the files and lines a test method wants to cover. - * - * @param string $className - * @param string $methodName - * @return array - */ - public static function getLinesToBeCovered($className, $methodName) - { - $codeToCoverList = array(); - $result = array(); - // @codeCoverageIgnoreStart - if (($pos = strpos($methodName, ' ')) !== FALSE) { - $methodName = substr($methodName, 0, $pos); - } - // @codeCoverageIgnoreEnd - $class = new ReflectionClass($className); - $method = new ReflectionMethod($className, $methodName); - $docComment = $class->getDocComment() . $method->getDocComment(); - - foreach (self::$templateMethods as $templateMethod) { - if ($class->hasMethod($templateMethod)) { - $reflector = $class->getMethod($templateMethod); - $docComment .= $reflector->getDocComment(); - unset($reflector); - } - } - - if (preg_match_all(self::REGEX, $docComment, $matches)) { - foreach ($matches['coveredElement'] as $coveredElement) { - $codeToCoverList = array_merge( - $codeToCoverList, - self::resolveCoversToReflectionObjects($coveredElement) - ); - } - - foreach ($codeToCoverList as $codeToCover) { - $fileName = $codeToCover->getFileName(); - - if (!isset($result[$fileName])) { - $result[$fileName] = array(); - } - - $result[$fileName] = array_unique( - array_merge( - $result[$fileName], - range( - $codeToCover->getStartLine(), $codeToCover->getEndLine() - ) - ) - ); - } - } - - return $result; - } - - /** - * Returns the lines of a source file that should be ignored. - * - * @param string $filename - * @return array - */ - public static function getLinesToBeIgnored($filename) - { - if (!isset(self::$ignoredLines[$filename])) { - self::$ignoredLines[$filename] = array(); - - $ignore = FALSE; - $stop = FALSE; - $tokens = PHP_Token_Stream_CachingFactory::get($filename)->tokens(); - - foreach ($tokens as $token) { - switch (get_class($token)) { - case 'PHP_Token_CLASS': - case 'PHP_Token_FUNCTION': { - $docblock = $token->getDocblock(); - $endLine = $token->getEndLine(); - - if (strpos($docblock, '@codeCoverageIgnore')) { - for ($i = $token->getLine(); $i <= $endLine; $i++) { - self::$ignoredLines[$filename][$i] = TRUE; - } - } - } - break; - - case 'PHP_Token_COMMENT': { - $_token = trim($token); - - if ($_token == '// @codeCoverageIgnoreStart' || - $_token == '//@codeCoverageIgnoreStart') { - $ignore = TRUE; - } - - else if ($_token == '// @codeCoverageIgnoreEnd' || - $_token == '//@codeCoverageIgnoreEnd') { - $stop = TRUE; - } - } - break; - } - - if ($ignore) { - self::$ignoredLines[$filename][$token->getLine()] = TRUE; - - if ($stop) { - $ignore = FALSE; - $stop = FALSE; - } - } - } - } - - return self::$ignoredLines[$filename]; - } - - /** - * Returns the package information of a user-defined class. - * - * @param string $className - * @param string $docComment - * @return array - */ - public static function getPackageInformation($className, $docComment) - { - $result = array( - 'namespace' => '', - 'fullPackage' => '', - 'category' => '', - 'package' => '', - 'subpackage' => '' - ); - - if (strpos($className, '\\') !== FALSE) { - $result['namespace'] = self::arrayToName( - explode('\\', $className) - ); - } - - if (preg_match('/@category[\s]+([\.\w]+)/', $docComment, $matches)) { - $result['category'] = $matches[1]; - } - - if (preg_match('/@package[\s]+([\.\w]+)/', $docComment, $matches)) { - $result['package'] = $matches[1]; - $result['fullPackage'] = $matches[1]; - } - - if (preg_match('/@subpackage[\s]+([\.\w]+)/', $docComment, $matches)) { - $result['subpackage'] = $matches[1]; - $result['fullPackage'] .= '.' . $matches[1]; - } - - if (empty($result['fullPackage'])) { - $result['fullPackage'] = self::arrayToName( - explode('_', str_replace('\\', '_', $className)), '.' - ); - } - - return $result; - } - - /** - * Returns a filesystem safe version of the passed filename. - * This function does not operate on full paths, just filenames. - * - * @param string $filename - * @return string - * @author Michael Lively Jr. - */ - public static function getSafeFilename($filename) - { - /* characters allowed: A-Z, a-z, 0-9, _ and . */ - return preg_replace('#[^\w.]#', '_', $filename); - } - - /** - * @param float $a - * @param float $b - * @return float ($a / $b) * 100 - */ - public static function percent($a, $b, $asString = FALSE) - { - if ($b > 0) { - $percent = ($a / $b) * 100; - } else { - $percent = 100; - } - - if ($asString) { - return sprintf('%01.2F', $percent); - } else { - return $percent; - } - } - - /** - * Reduces the paths by cutting the longest common start path. - * - * For instance, - * - * - * Array - * ( - * [/home/sb/Money/Money.php] => Array - * ( - * ... - * ) - * - * [/home/sb/Money/MoneyBag.php] => Array - * ( - * ... - * ) - * ) - * - * - * is reduced to - * - * - * Array - * ( - * [Money.php] => Array - * ( - * ... - * ) - * - * [MoneyBag.php] => Array - * ( - * ... - * ) - * ) - * - * - * @param array $files - * @return string - */ - public static function reducePaths(&$files) - { - if (empty($files)) { - return '.'; - } - - $commonPath = ''; - $paths = array_keys($files); - - if (count($files) == 1) { - $commonPath = dirname($paths[0]) . '/'; - $files[basename($paths[0])] = $files[$paths[0]]; - - unset($files[$paths[0]]); - - return $commonPath; - } - - $max = count($paths); - - for ($i = 0; $i < $max; $i++) { - $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]); - - if (empty($paths[$i][0])) { - $paths[$i][0] = DIRECTORY_SEPARATOR; - } - } - - $done = FALSE; - $max = count($paths); - - while (!$done) { - for ($i = 0; $i < $max - 1; $i++) { - if (!isset($paths[$i][0]) || - !isset($paths[$i+1][0]) || - $paths[$i][0] != $paths[$i+1][0]) { - $done = TRUE; - break; - } - } - - if (!$done) { - $commonPath .= $paths[0][0]; - - if ($paths[0][0] != DIRECTORY_SEPARATOR) { - $commonPath .= DIRECTORY_SEPARATOR; - } - - for ($i = 0; $i < $max; $i++) { - array_shift($paths[$i]); - } - } - } - - $original = array_keys($files); - $max = count($original); - - for ($i = 0; $i < $max; $i++) { - $files[join('/', $paths[$i])] = $files[$original[$i]]; - unset($files[$original[$i]]); - } - - ksort($files); - - return $commonPath; - } - - /** - * Returns the package information of a user-defined class. - * - * @param array $parts - * @param string $join - * @return string - */ - protected static function arrayToName(array $parts, $join = '\\') - { - $result = ''; - - if (count($parts) > 1) { - array_pop($parts); - - $result = join($join, $parts); - } - - return $result; - } - - /** - * @param string $coveredElement - * @return array - */ - protected static function resolveCoversToReflectionObjects($coveredElement) - { - $codeToCoverList = array(); - - if (strpos($coveredElement, '::') !== FALSE) { - list($className, $methodName) = explode('::', $coveredElement); - - if ($methodName[0] == '<') { - $classes = array($className); - - foreach ($classes as $className) { - if (!class_exists($className) && - !interface_exists($className)) { - throw new RuntimeException( - sprintf( - 'Trying to @cover not existing class or ' . - 'interface "%s".', - $className - ) - ); - } - - $class = new ReflectionClass($className); - $methods = $class->getMethods(); - $inverse = isset($methodName[1]) && $methodName[1] == '!'; - - if (strpos($methodName, 'protected')) { - $visibility = 'isProtected'; - } - - else if (strpos($methodName, 'private')) { - $visibility = 'isPrivate'; - } - - else if (strpos($methodName, 'public')) { - $visibility = 'isPublic'; - } - - foreach ($methods as $method) { - if ($inverse && !$method->$visibility()) { - $codeToCoverList[] = $method; - } - - else if (!$inverse && $method->$visibility()) { - $codeToCoverList[] = $method; - } - } - } - } else { - $classes = array($className); - - foreach ($classes as $className) { - if ($className == '' && function_exists($methodName)) { - $codeToCoverList[] = new ReflectionFunction( - $methodName - ); - } else { - if (!((class_exists($className) || - interface_exists($className)) && - method_exists($className, $methodName))) { - throw new RuntimeException( - sprintf( - 'Trying to @cover not existing method "%s::%s".', - $className, - $methodName - ) - ); - } - - $codeToCoverList[] = new ReflectionMethod( - $className, $methodName - ); - } - } - } - } else { - $extended = FALSE; - - if (strpos($coveredElement, '') !== FALSE) { - $coveredElement = str_replace( - '', '', $coveredElement - ); - - $extended = TRUE; - } - - $classes = array($coveredElement); - - if ($extended) { - $classes = array_merge( - $classes, - class_implements($coveredElement), - class_parents($coveredElement) - ); - } - - foreach ($classes as $className) { - if (!class_exists($className) && - !interface_exists($className)) { - throw new RuntimeException( - sprintf( - 'Trying to @cover not existing class or ' . - 'interface "%s".', - $className - ) - ); - } - - $codeToCoverList[] = new ReflectionClass($className); - } - } - - return $codeToCoverList; - } -} diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 1ca62a406..000000000 --- a/README.markdown +++ /dev/null @@ -1,87 +0,0 @@ -PHP_CodeCoverage -================ - -**PHP_CodeCoverage** is a library that provides collection, processing, and rendering functionality for PHP code coverage information. - -Installation ------------- - -PHP_CodeCoverage should be installed using the [PEAR Installer](http://pear.php.net/). This installer is the backbone of PEAR, which provides a distribution system for PHP packages, and is shipped with every release of PHP since version 4.3.0. - -The PEAR channel (`pear.phpunit.de`) that is used to distribute PHP_CodeCoverage needs to be registered with the local PEAR environment. Furthermore, a component that PHP_CodeCoverage depends upon is hosted on the eZ Components PEAR channel (`components.ez.no`). - - sb@ubuntu ~ % pear channel-discover pear.phpunit.de - Adding Channel "pear.phpunit.de" succeeded - Discovery of channel "pear.phpunit.de" succeeded - - sb@ubuntu ~ % pear channel-discover components.ez.no - Adding Channel "components.ez.no" succeeded - Discovery of channel "components.ez.no" succeeded - -This has to be done only once. Now the PEAR Installer can be used to install packages from the PHPUnit channel: - - sb@vmware ~ % pear install phpunit/PHP_CodeCoverage - downloading PHP_CodeCoverage-0.9.0.tgz ... - Starting to download PHP_CodeCoverage-0.9.0.tgz (108,376 bytes) - .........................done: 108,376 bytes - install ok: channel://pear.phpunit.de/PHP_CodeCoverage-0.9.0 - -After the installation you can find the PHP_CodeCoverage source files inside your local PEAR directory; the path is usually `/usr/lib/php/PHP/CodeCoverage`. - -Using the PHP_CodeCoverage API ------------------------------- - - start(''); - - // ... - - $coverage->stop(); - - $writer = new PHP_CodeCoverage_Report_Clover; - $writer->process($coverage, '/tmp/clover.xml'); - - $writer = new PHP_CodeCoverage_Report_HTML; - $writer->process($coverage, '/tmp/code-coverage-report'); - -Using the `phpcov` tool ------------------------ - - sb@vmware examples % cat add.php - - - - - - - - - - - - - diff --git a/README.md b/README.md new file mode 100644 index 000000000..f5d452724 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# phpunit/php-code-coverage + +[![Latest Stable Version](https://poser.pugx.org/phpunit/php-code-coverage/v)](https://packagist.org/packages/phpunit/php-code-coverage) +[![CI Status](https://github.com/sebastianbergmann/php-code-coverage/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-code-coverage/actions) +[![codecov](https://codecov.io/gh/sebastianbergmann/php-code-coverage/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-code-coverage) + +Provides collection, processing, and rendering functionality for PHP code coverage information. + +## Installation + +You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): + +``` +composer require phpunit/php-code-coverage +``` + +If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: + +``` +composer require --dev phpunit/php-code-coverage +``` + +## Usage + +```php +includeFiles( + [ + '/path/to/file.php', + '/path/to/another_file.php', + ] +); + +$coverage = new CodeCoverage( + (new Selector)->forLineCoverage($filter), + $filter +); + +$coverage->start(''); + +// ... + +$coverage->stop(); + + +(new HtmlReport)->process($coverage, '/tmp/code-coverage-report'); +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..d88ff0019 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please email `sebastian@phpunit.de`. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +* The type of issue +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Web Context + +The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. + +The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. + +If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. + +Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. + diff --git a/Tests/PHP/CodeCoverage/FilterTest.php b/Tests/PHP/CodeCoverage/FilterTest.php deleted file mode 100644 index ed6dd22ad..000000000 --- a/Tests/PHP/CodeCoverage/FilterTest.php +++ /dev/null @@ -1,336 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage/Filter.php'; - -if (!defined('TEST_FILES_PATH')) { - define( - 'TEST_FILES_PATH', - dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . - '_files' . DIRECTORY_SEPARATOR - ); -} - -/** - * Tests for the PHP_CodeCoverage_Filter class. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_FilterTest extends PHPUnit_Framework_TestCase -{ - protected $filter; - protected $files; - - protected function setUp() - { - $this->filter = new PHP_CodeCoverage_Filter; - - $this->files = array( - TEST_FILES_PATH . 'BankAccount.php', - TEST_FILES_PATH . 'BankAccountTest.php', - TEST_FILES_PATH . 'CoverageClassExtendedTest.php', - TEST_FILES_PATH . 'CoverageClassTest.php', - TEST_FILES_PATH . 'CoverageFunctionTest.php', - TEST_FILES_PATH . 'CoverageMethodTest.php', - TEST_FILES_PATH . 'CoverageNoneTest.php', - TEST_FILES_PATH . 'CoverageNotPrivateTest.php', - TEST_FILES_PATH . 'CoverageNotProtectedTest.php', - TEST_FILES_PATH . 'CoverageNotPublicTest.php', - TEST_FILES_PATH . 'CoveragePrivateTest.php', - TEST_FILES_PATH . 'CoverageProtectedTest.php', - TEST_FILES_PATH . 'CoveragePublicTest.php', - TEST_FILES_PATH . 'CoveredClass.php', - TEST_FILES_PATH . 'CoveredFunction.php', - TEST_FILES_PATH . 'NamespaceCoverageClassExtendedTest.php', - TEST_FILES_PATH . 'NamespaceCoverageClassTest.php', - TEST_FILES_PATH . 'NamespaceCoverageMethodTest.php', - TEST_FILES_PATH . 'NamespaceCoverageNotPrivateTest.php', - TEST_FILES_PATH . 'NamespaceCoverageNotProtectedTest.php', - TEST_FILES_PATH . 'NamespaceCoverageNotPublicTest.php', - TEST_FILES_PATH . 'NamespaceCoveragePrivateTest.php', - TEST_FILES_PATH . 'NamespaceCoverageProtectedTest.php', - TEST_FILES_PATH . 'NamespaceCoveragePublicTest.php', - TEST_FILES_PATH . 'NamespaceCoveredClass.php', - TEST_FILES_PATH . 'NotExistingCoveredElementTest.php', - TEST_FILES_PATH . 'source_with_ignore.php', - TEST_FILES_PATH . 'source_with_namespace.php', - TEST_FILES_PATH . 'source_without_ignore.php', - TEST_FILES_PATH . 'source_without_namespace.php' - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::getInstance - */ - public function testFactory() - { - $filter = PHP_CodeCoverage_Filter::getInstance(); - $this->assertSame($filter, PHP_CodeCoverage_Filter::getInstance()); - } - - /** - * @covers PHP_CodeCoverage_Filter::addFileToBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - */ - public function testAddingAFileToTheBlacklistWorks() - { - $this->filter->addFileToBlacklist($this->files[0]); - - $this->assertEquals( - array('DEFAULT' => array($this->files[0])), - $this->filter->getBlacklist() - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::addFileToBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - */ - public function testAddingAFileToTheBlacklistWorks2() - { - $this->filter->addFileToBlacklist($this->files[0], 'group'); - - $this->assertEquals( - array('DEFAULT' => array(), 'group' => array($this->files[0])), - $this->filter->getBlacklist() - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::removeFileFromBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - */ - public function testRemovingAFileFromTheBlacklistWorks() - { - $this->filter->addFileToBlacklist($this->files[0]); - $this->filter->removeFileFromBlacklist($this->files[0]); - - $this->assertEquals( - array('DEFAULT' => array()), $this->filter->getBlacklist() - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::addDirectoryToBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - * @depends testAddingAFileToTheBlacklistWorks - */ - public function testAddingADirectoryToTheBlacklistWorks() - { - $this->filter->addDirectoryToBlacklist(TEST_FILES_PATH); - - $blacklist = $this->filter->getBlacklist(); - - foreach (array_keys($blacklist) as $group) { - sort($blacklist[$group]); - } - - $this->assertEquals(array('DEFAULT' => $this->files), $blacklist); - } - - /** - * @covers PHP_CodeCoverage_Filter::addFilesToBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - */ - public function testAddingFilesToTheBlacklistWorks() - { - $this->filter->addFilesToBlacklist( - File_Iterator_Factory::getFilesAsArray( - TEST_FILES_PATH, $suffixes = '.php' - ) - ); - - $blacklist = $this->filter->getBlacklist(); - - foreach (array_keys($blacklist) as $group) { - sort($blacklist[$group]); - } - - $this->assertEquals(array('DEFAULT' => $this->files), $blacklist); - } - - /** - * @covers PHP_CodeCoverage_Filter::removeDirectoryFromBlacklist - * @covers PHP_CodeCoverage_Filter::getBlacklist - * @depends testAddingADirectoryToTheBlacklistWorks - */ - public function testRemovingADirectoryFromTheBlacklistWorks() - { - $this->filter->addDirectoryToBlacklist(TEST_FILES_PATH); - $this->filter->removeDirectoryFromBlacklist(TEST_FILES_PATH); - - $this->assertEquals( - array('DEFAULT' => array()), $this->filter->getBlacklist() - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::addFileToWhitelist - * @covers PHP_CodeCoverage_Filter::getWhitelist - */ - public function testAddingAFileToTheWhitelistWorks() - { - $this->filter->addFileToWhitelist($this->files[0]); - - $this->assertEquals( - array($this->files[0]), $this->filter->getWhitelist() - ); - } - - /** - * @covers PHP_CodeCoverage_Filter::removeFileFromWhitelist - * @covers PHP_CodeCoverage_Filter::getWhitelist - */ - public function testRemovingAFileFromTheWhitelistWorks() - { - $this->filter->addFileToWhitelist($this->files[0]); - $this->filter->removeFileFromWhitelist($this->files[0]); - - $this->assertEquals(array(), $this->filter->getWhitelist()); - } - - /** - * @covers PHP_CodeCoverage_Filter::addDirectoryToWhitelist - * @covers PHP_CodeCoverage_Filter::getWhitelist - * @depends testAddingAFileToTheWhitelistWorks - */ - public function testAddingADirectoryToTheWhitelistWorks() - { - $this->filter->addDirectoryToWhitelist(TEST_FILES_PATH); - - $whitelist = $this->filter->getWhitelist(); - sort($whitelist); - - $this->assertEquals($this->files, $whitelist); - } - - /** - * @covers PHP_CodeCoverage_Filter::addFilesToWhitelist - * @covers PHP_CodeCoverage_Filter::getBlacklist - */ - public function testAddingFilesToTheWhitelistWorks() - { - $this->filter->addFilesToWhitelist( - File_Iterator_Factory::getFilesAsArray( - TEST_FILES_PATH, $suffixes = '.php' - ) - ); - - $whitelist = $this->filter->getWhitelist(); - sort($whitelist); - - $this->assertEquals($this->files, $whitelist); - } - - /** - * @covers PHP_CodeCoverage_Filter::removeDirectoryFromWhitelist - * @covers PHP_CodeCoverage_Filter::getWhitelist - * @depends testAddingADirectoryToTheWhitelistWorks - */ - public function testRemovingADirectoryFromTheWhitelistWorks() - { - $this->filter->addDirectoryToWhitelist(TEST_FILES_PATH); - $this->filter->removeDirectoryFromWhitelist(TEST_FILES_PATH); - - $this->assertEquals(array(), $this->filter->getWhitelist()); - } - - /** - * @covers PHP_CodeCoverage_Filter::isFile - */ - public function testIsFile() - { - $this->assertFalse($this->filter->isFile('eval()\'d code')); - $this->assertFalse($this->filter->isFile('runtime-created function')); - $this->assertFalse($this->filter->isFile('assert code')); - $this->assertFalse($this->filter->isFile('regexp code')); - $this->assertTrue($this->filter->isFile('filename')); - } - - /** - * @covers PHP_CodeCoverage_Filter::isFiltered - */ - public function testBlacklistedFileIsFiltered() - { - $this->filter->addFileToBlacklist($this->files[0]); - $this->assertTrue($this->filter->isFiltered($this->files[0])); - } - - /** - * @covers PHP_CodeCoverage_Filter::isFiltered - */ - public function testWhitelistedFileIsNotFiltered() - { - $this->filter->addFileToWhitelist($this->files[0]); - $this->assertFalse($this->filter->isFiltered($this->files[0])); - } - - /** - * @covers PHP_CodeCoverage_Filter::isFiltered - */ - public function testNotWhitelistedFileIsFiltered() - { - $this->filter->addFileToWhitelist($this->files[0]); - $this->assertTrue($this->filter->isFiltered($this->files[1])); - } - - /** - * @covers PHP_CodeCoverage_Filter::isFiltered - * @expectedException InvalidArgumentException - */ - public function testIsFilteredThrowsExceptionForInvalidArgument() - { - $this->filter->isFiltered('foo', array(), NULL); - } -} diff --git a/Tests/PHP/CodeCoverage/Report/CloverTest.php b/Tests/PHP/CodeCoverage/Report/CloverTest.php deleted file mode 100644 index 08990091d..000000000 --- a/Tests/PHP/CodeCoverage/Report/CloverTest.php +++ /dev/null @@ -1,98 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -if (!defined('TEST_FILES_PATH')) { - define( - 'TEST_FILES_PATH', - dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . - '_files' . DIRECTORY_SEPARATOR - ); -} - -require_once TEST_FILES_PATH . '../TestCase.php'; -require_once 'PHP/CodeCoverage/Report/Clover.php'; - -/** - * Tests for the PHP_CodeCoverage_Report_Clover class. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_Report_CloverTest extends PHP_CodeCoverage_TestCase -{ - /** - * @covers PHP_CodeCoverage_Report_Clover - */ - public function testCloverForBankAccountTest() - { - $clover = new PHP_CodeCoverage_Report_Clover; - - $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'BankAccount-clover.xml', - $clover->process($this->getCoverageForBankAccount(), NULL, 'BankAccount') - ); - } - - /** - * @covers PHP_CodeCoverage_Report_Clover - */ - public function testCloverForFileWithIgnoredLines() - { - $clover = new PHP_CodeCoverage_Report_Clover; - - $this->assertStringMatchesFormatFile( - TEST_FILES_PATH . 'ignored-lines-clover.xml', - $clover->process($this->getCoverageForFileWithIgnoredLines()) - ); - } -} diff --git a/Tests/PHP/CodeCoverage/UtilTest.php b/Tests/PHP/CodeCoverage/UtilTest.php deleted file mode 100644 index fc4472a2a..000000000 --- a/Tests/PHP/CodeCoverage/UtilTest.php +++ /dev/null @@ -1,534 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage/Util.php'; -@include_once 'vfsStream/vfsStream.php'; - -if (!defined('TEST_FILES_PATH')) { - define( - 'TEST_FILES_PATH', - dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . - '_files' . DIRECTORY_SEPARATOR - ); -} - -require_once TEST_FILES_PATH . 'CoverageClassExtendedTest.php'; -require_once TEST_FILES_PATH . 'CoverageClassTest.php'; -require_once TEST_FILES_PATH . 'CoverageFunctionTest.php'; -require_once TEST_FILES_PATH . 'CoverageMethodTest.php'; -require_once TEST_FILES_PATH . 'CoverageNoneTest.php'; -require_once TEST_FILES_PATH . 'CoverageNotPrivateTest.php'; -require_once TEST_FILES_PATH . 'CoverageNotProtectedTest.php'; -require_once TEST_FILES_PATH . 'CoverageNotPublicTest.php'; -require_once TEST_FILES_PATH . 'CoveragePrivateTest.php'; -require_once TEST_FILES_PATH . 'CoverageProtectedTest.php'; -require_once TEST_FILES_PATH . 'CoveragePublicTest.php'; -require_once TEST_FILES_PATH . 'CoveredClass.php'; -require_once TEST_FILES_PATH . 'CoveredFunction.php'; -require_once TEST_FILES_PATH . 'NotExistingCoveredElementTest.php'; - -if (version_compare(PHP_VERSION, '5.3', '>')) { - require_once TEST_FILES_PATH . 'NamespaceCoverageClassExtendedTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageClassTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageMethodTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageNotPrivateTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageNotProtectedTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageNotPublicTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoveragePrivateTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoverageProtectedTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoveragePublicTest.php'; - require_once TEST_FILES_PATH . 'NamespaceCoveredClass.php'; -} - -/** - * Tests for the PHP_CodeCoverage_Util class. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverage_UtilTest extends PHPUnit_Framework_TestCase -{ - protected function setUp() - { - if (!class_exists('vfsStream')) { - $this->markTestSkipped('vfsStream is not available.'); - } - - vfsStream::setup('UtilTest'); - } - - /** - * @covers PHP_CodeCoverage_Util::buildDirectoryStructure - */ - public function testBuildDirectoryStructure() - { - $this->assertEquals( - array( - 'src' => array( - 'Money.php/f' => array(), - 'MoneyBag.php/f' => array() - ) - ), - PHP_CodeCoverage_Util::buildDirectoryStructure( - array('src/Money.php' => array(), 'src/MoneyBag.php' => array()) - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::crap - */ - public function testCrap() - { - $this->assertEquals(6, PHP_CodeCoverage_Util::crap(2, 0)); - $this->assertEquals(2, PHP_CodeCoverage_Util::crap(2, 95)); - $this->assertEquals(2.5, PHP_CodeCoverage_Util::crap(2, 50)); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeCovered - * @covers PHP_CodeCoverage_Util::resolveCoversToReflectionObjects - * @dataProvider getLinesToBeCoveredProvider - */ - public function testGetLinesToBeCovered($test, $lines) - { - if (strpos($test, 'Namespace') === 0) { - if (!version_compare(PHP_VERSION, '5.3', '>')) { - $this->markTestSkipped('PHP 5.3 (or later) is required.'); - } - - $expected = array( - TEST_FILES_PATH . 'NamespaceCoveredClass.php' => $lines - ); - } - - else if ($test === 'CoverageNoneTest') { - $expected = array(); - } - - else if ($test === 'CoverageFunctionTest') { - $expected = array( - TEST_FILES_PATH . 'CoveredFunction.php' => $lines - ); - } - - else { - $expected = array(TEST_FILES_PATH . 'CoveredClass.php' => $lines); - } - - $this->assertEquals( - $expected, - PHP_CodeCoverage_Util::getLinesToBeCovered( - $test, 'testSomething' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeCovered - * @covers PHP_CodeCoverage_Util::resolveCoversToReflectionObjects - * @expectedException RuntimeException - */ - public function testGetLinesToBeCovered2() - { - PHP_CodeCoverage_Util::getLinesToBeCovered( - 'NotExistingCoveredElementTest', 'testOne' - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeCovered - * @covers PHP_CodeCoverage_Util::resolveCoversToReflectionObjects - * @expectedException RuntimeException - */ - public function testGetLinesToBeCovered3() - { - PHP_CodeCoverage_Util::getLinesToBeCovered( - 'NotExistingCoveredElementTest', 'testTwo' - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeCovered - * @covers PHP_CodeCoverage_Util::resolveCoversToReflectionObjects - * @expectedException RuntimeException - */ - public function testGetLinesToBeCovered4() - { - PHP_CodeCoverage_Util::getLinesToBeCovered( - 'NotExistingCoveredElementTest', 'testThree' - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnored() - { - $this->assertEquals( - array( - 3 => TRUE, - 4 => TRUE, - 5 => TRUE, - 11 => TRUE, - 12 => TRUE, - 13 => TRUE, - 14 => TRUE, - 15 => TRUE, - 16 => TRUE, - 23 => TRUE, - 24 => TRUE, - 25 => TRUE - ), - PHP_CodeCoverage_Util::getLinesToBeIgnored( - TEST_FILES_PATH . 'source_with_ignore.php' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnored2() - { - $this->assertEquals( - array(), - PHP_CodeCoverage_Util::getLinesToBeIgnored( - TEST_FILES_PATH . 'source_without_ignore.php' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getPackageInformation - */ - public function testGetPackageInformation() - { - $this->assertEquals( - array( - 'category' => 'Foo', - 'fullPackage' => 'Bar.Baz', - 'namespace' => '', - 'package' => 'Bar', - 'subpackage' => 'Baz' - ), - PHP_CodeCoverage_Util::getPackageInformation( - 'Foo', - '/** - * @category Foo - * @package Bar - * @subpackage Baz - */' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getPackageInformation - * @covers PHP_CodeCoverage_Util::arrayToName - */ - public function testGetPackageInformation2() - { - $this->assertEquals( - array( - 'category' => 'Foo', - 'fullPackage' => 'Bar.Baz', - 'namespace' => 'Foo', - 'package' => 'Bar', - 'subpackage' => 'Baz' - ), - PHP_CodeCoverage_Util::getPackageInformation( - 'Foo\\Bar', - '/** - * @category Foo - * @package Bar - * @subpackage Baz - */' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getPackageInformation - */ - public function testGetPackageInformation3() - { - $this->assertEquals( - array( - 'category' => '', - 'fullPackage' => '', - 'namespace' => '', - 'package' => '', - 'subpackage' => '' - ), - PHP_CodeCoverage_Util::getPackageInformation( - 'Foo', - '' - ) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::reducePaths - */ - public function testReducePaths() - { - $files = array( - '/home/sb/Money/Money.php' => array(), - '/home/sb/Money/MoneyBag.php' => array() - ); - - $commonPath = PHP_CodeCoverage_Util::reducePaths($files); - - $this->assertEquals( - array( - 'Money.php' => array(), - 'MoneyBag.php' => array() - ), - $files - ); - - $this->assertEquals('/home/sb/Money/', $commonPath); - } - - /** - * @covers PHP_CodeCoverage_Util::reducePaths - */ - public function testReducePaths2() - { - $files = array(); - - $commonPath = PHP_CodeCoverage_Util::reducePaths($files); - - $this->assertEquals('.', $commonPath); - } - - /** - * @covers PHP_CodeCoverage_Util::reducePaths - */ - public function testReducePaths3() - { - $files = array( - '/home/sb/Money/Money.php' => array() - ); - - $commonPath = PHP_CodeCoverage_Util::reducePaths($files); - - $this->assertEquals( - array( - 'Money.php' => array() - ), - $files - ); - - $this->assertEquals('/home/sb/Money/', $commonPath); - } - - /** - * @covers PHP_CodeCoverage_Util::getDirectory - */ - public function testGetDirectory() - { - if (!class_exists('vfsStream')) { - $this->markTestSkipped('vfsStream is not installed'); - } - - $this->assertEquals( - vfsStream::url('UtilTest') . '/', - PHP_CodeCoverage_Util::getDirectory(vfsStream::url('UtilTest')) - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getDirectory - */ - public function testGetDirectory2() - { - if (!class_exists('vfsStream')) { - $this->markTestSkipped('vfsStream is not installed'); - } - - PHP_CodeCoverage_Util::getDirectory( - vfsStream::url('UtilTest') . '/report' - ); - - $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('report')); - } - - /** - * @covers PHP_CodeCoverage_Util::getDirectory - * @expectedException RuntimeException - */ - public function testGetDirectory3() - { - if (!class_exists('vfsStream')) { - $this->markTestSkipped('vfsStream is not installed'); - } - - PHP_CodeCoverage_Util::getDirectory( - vfsStream::url('/not/existing/path') - ); - } - - /** - * @covers PHP_CodeCoverage_Util::getSafeFilename - */ - public function testGetSafeFilename() - { - $this->assertEquals( - 'foo', PHP_CodeCoverage_Util::getSafeFilename('foo') - ); - - $this->assertEquals( - 'foo_bar', PHP_CodeCoverage_Util::getSafeFilename('foo/bar') - ); - } - - /** - * @covers PHP_CodeCoverage_Util::percent - */ - public function testPercent() - { - $this->assertEquals(100, PHP_CodeCoverage_Util::percent(100, 0)); - $this->assertEquals(100, PHP_CodeCoverage_Util::percent(100, 100)); - $this->assertEquals( - '100.00', PHP_CodeCoverage_Util::percent(100, 100, TRUE) - ); - } - - public function getLinesToBeCoveredProvider() - { - return array( - array( - 'CoverageNoneTest', - array() - ), - array( - 'CoverageClassExtendedTest', - array_merge(range(19, 36), range(2, 17)) - ), - array( - 'CoverageClassTest', - range(19, 36) - ), - array( - 'CoverageMethodTest', - range(31, 35) - ), - array( - 'CoverageNotPrivateTest', - array_merge(range(25, 29), range(31, 35)) - ), - array( - 'CoverageNotProtectedTest', - array_merge(range(21, 23), range(31, 35)) - ), - array( - 'CoverageNotPublicTest', - array_merge(range(21, 23), range(25, 29)) - ), - array( - 'CoveragePrivateTest', - range(21, 23) - ), - array( - 'CoverageProtectedTest', - range(25, 29) - ), - array( - 'CoveragePublicTest', - range(31, 35) - ), - array( - 'CoverageFunctionTest', - range(2, 4) - ), - array( - 'NamespaceCoverageClassExtendedTest', - array_merge(range(21, 38), range(4, 19)) - ), - array( - 'NamespaceCoverageClassTest', - range(21, 38) - ), - array( - 'NamespaceCoverageMethodTest', - range(33, 37) - ), - array( - 'NamespaceCoverageNotPrivateTest', - array_merge(range(27, 31), range(33, 37)) - ), - array( - 'NamespaceCoverageNotProtectedTest', - array_merge(range(23, 25), range(33, 37)) - ), - array( - 'NamespaceCoverageNotPublicTest', - array_merge(range(23, 25), range(27, 31)) - ), - array( - 'NamespaceCoveragePrivateTest', - range(23, 25) - ), - array( - 'NamespaceCoverageProtectedTest', - range(27, 31) - ), - array( - 'NamespaceCoveragePublicTest', - range(33, 37) - ) - ); - } -} diff --git a/Tests/PHP/CodeCoverageTest.php b/Tests/PHP/CodeCoverageTest.php deleted file mode 100644 index 42918d411..000000000 --- a/Tests/PHP/CodeCoverageTest.php +++ /dev/null @@ -1,403 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -require_once 'PHP/CodeCoverage.php'; - -if (!defined('TEST_FILES_PATH')) { - define( - 'TEST_FILES_PATH', - dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . - '_files' . DIRECTORY_SEPARATOR - ); -} - -require_once TEST_FILES_PATH . '../TestCase.php'; - -require_once TEST_FILES_PATH . 'BankAccount.php'; -require_once TEST_FILES_PATH . 'BankAccountTest.php'; - -/** - * Tests for the PHP_CodeCoverage class. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -class PHP_CodeCoverageTest extends PHP_CodeCoverage_TestCase -{ - /** - * @covers PHP_CodeCoverage::__construct - * @covers PHP_CodeCoverage::filter - */ - public function testConstructor() - { - $coverage = new PHP_CodeCoverage; - - $this->assertAttributeInstanceOf( - 'PHP_CodeCoverage_Driver_Xdebug', 'driver', $coverage - ); - - $this->assertAttributeInstanceOf( - 'PHP_CodeCoverage_Filter', 'filter', $coverage - ); - } - - /** - * @covers PHP_CodeCoverage::__construct - * @covers PHP_CodeCoverage::filter - */ - public function testConstructor2() - { - $filter = new PHP_CodeCoverage_Filter; - $coverage = new PHP_CodeCoverage(NULL, $filter); - - $this->assertAttributeInstanceOf( - 'PHP_CodeCoverage_Driver_Xdebug', 'driver', $coverage - ); - - $this->assertSame($filter, $coverage->filter()); - } - - /** - * @covers PHP_CodeCoverage::getInstance - */ - public function testFactory() - { - $coverage = PHP_CodeCoverage::getInstance(); - $this->assertSame($coverage, PHP_CodeCoverage::getInstance()); - } - - /** - * @covers PHP_CodeCoverage::start - * @expectedException InvalidArgumentException - */ - public function testStartThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->start(NULL, NULL); - } - - /** - * @covers PHP_CodeCoverage::stop - * @expectedException InvalidArgumentException - */ - public function testStopThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->stop(NULL); - } - - /** - * @covers PHP_CodeCoverage::append - * @expectedException InvalidArgumentException - */ - public function testAppendThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->append(array(), NULL); - } - - /** - * @covers PHP_CodeCoverage::setForceCoversAnnotation - * @expectedException InvalidArgumentException - */ - public function testSetForceCoversAnnotationThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->setForceCoversAnnotation(NULL); - } - - /** - * @covers PHP_CodeCoverage::setForceCoversAnnotation - */ - public function testSetForceCoversAnnotation() - { - $coverage = new PHP_CodeCoverage; - $coverage->setForceCoversAnnotation(TRUE); - $this->assertAttributeEquals(TRUE, 'forceCoversAnnotation', $coverage); - } - - /** - * @covers PHP_CodeCoverage::setProcessUncoveredFilesFromWhitelist - * @expectedException InvalidArgumentException - */ - public function testSetProcessUncoveredFilesFromWhitelistThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->setProcessUncoveredFilesFromWhitelist(NULL); - } - - /** - * @covers PHP_CodeCoverage::setProcessUncoveredFilesFromWhitelist - */ - public function testSetProcessUncoveredFilesFromWhitelist() - { - $coverage = new PHP_CodeCoverage; - $coverage->setProcessUncoveredFilesFromWhitelist(TRUE); - $this->assertAttributeEquals( - TRUE, 'processUncoveredFilesFromWhitelist', $coverage - ); - } - - /** - * @covers PHP_CodeCoverage::setMapTestClassNameToCoveredClassName - */ - public function testSetMapTestClassNameToCoveredClassName() - { - $coverage = new PHP_CodeCoverage; - $coverage->setMapTestClassNameToCoveredClassName(TRUE); - $this->assertAttributeEquals( - TRUE, 'mapTestClassNameToCoveredClassName', $coverage - ); - } - - /** - * @covers PHP_CodeCoverage::setMapTestClassNameToCoveredClassName - * @expectedException InvalidArgumentException - */ - public function testSetMapTestClassNameToCoveredClassNameThrowsExceptionForInvalidArgument() - { - $coverage = new PHP_CodeCoverage; - $coverage->setMapTestClassNameToCoveredClassName(NULL); - } - - /** - * @covers PHP_CodeCoverage::clear - */ - public function testClear() - { - $coverage = new PHP_CodeCoverage; - $coverage->clear(); - - $this->assertAttributeEquals(array(), 'data', $coverage); - $this->assertAttributeEquals(array(), 'coveredFiles', $coverage); - $this->assertAttributeEquals(array(), 'summary', $coverage); - $this->assertAttributeEquals(NULL, 'currentId', $coverage); - } - - /** - * @covers PHP_CodeCoverage::start - * @covers PHP_CodeCoverage::stop - * @covers PHP_CodeCoverage::append - * @covers PHP_CodeCoverage::applySelfFilter - * @covers PHP_CodeCoverage::applyListsFilter - * @covers PHP_CodeCoverage::applyCoversAnnotationFilter - */ - public function testCollect() - { - $this->assertAttributeEquals( - array( - 'BankAccountTest::testBalanceIsInitiallyZero' => array( - 'filtered' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 9 => -2 - ) - ), - 'raw' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 9 => -2, - 13 => -1, - 14 => -1, - 15 => -1, - 16 => -1, - 18 => -1, - 22 => -1, - 24 => -1, - 25 => -2, - 29 => -1, - 31 => -1, - 32 => -2 - ) - ) - ), - 'BankAccountTest::testBalanceCannotBecomeNegative' => array( - 'filtered' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 29 => 1 - ) - ), - 'raw' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 16 => 1, - 29 => 1 - ) - ) - ), - 'BankAccountTest::testBalanceCannotBecomeNegative2' => array( - 'filtered' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 22 => 1 - ) - ), - 'raw' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 16 => 1, - 22 => 1 - ) - ) - ), - 'BankAccountTest::testDepositWithdrawMoney' => array( - 'filtered' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 22 => 1, - 24 => 1, - 29 => 1, - 31 => 1 - ) - ), - 'raw' => array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 14 => 1, - 15 => 1, - 18 => 1, - 22 => 1, - 24 => 1, - 29 => 1, - 31 => 1 - ) - ) - ) - ), - 'data', - $this->getCoverageForBankAccount() - ); - } - - /** - * @covers PHP_CodeCoverage::merge - */ - public function testMerge() - { - $coverage = new PHP_CodeCoverage( - $this->setUpXdebugStubForBankAccount(), new PHP_CodeCoverage_Filter - ); - - $coverage->merge($this->getCoverageForBankAccount()); - - $this->assertEquals( - $this->getCoverageForBankAccount()->getSummary(), $coverage->getSummary() - ); - } - - /** - * @covers PHP_CodeCoverage::getSummary - */ - public function testSummary() - { - $this->assertEquals( - array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => array( - 0 => array( - 'id' => 'BankAccountTest::testBalanceIsInitiallyZero', - 'status' => NULL - ), - 1 => array( - 'id' => 'BankAccountTest::testDepositWithdrawMoney', - 'status' => NULL - ) - ), - 9 => -2, - 13 => -1, - 14 => -1, - 15 => -1, - 16 => -1, - 18 => -1, - 22 => array( - 0 => array( - 'id' => 'BankAccountTest::testBalanceCannotBecomeNegative2', - 'status' => NULL - ), - 1 => array( - 'id' => 'BankAccountTest::testDepositWithdrawMoney', - 'status' => NULL - ) - ), - 24 => array( - 0 => array( - 'id' => 'BankAccountTest::testDepositWithdrawMoney', - 'status' => NULL - ) - ), - 25 => -2, - 29 => array( - 0 => array( - 'id' => 'BankAccountTest::testBalanceCannotBecomeNegative', - 'status' => NULL - ), - 1 => array( - 'id' => 'BankAccountTest::testDepositWithdrawMoney', - 'status' => NULL - ) - ), - 31 => array( - 0 => array( - 'id' => 'BankAccountTest::testDepositWithdrawMoney', - 'status' => NULL - ) - ), - 32 => -2 - ) - ), - $this->getCoverageForBankAccount()->getSummary() - ); - } -} diff --git a/Tests/TestCase.php b/Tests/TestCase.php deleted file mode 100644 index 21cb5585a..000000000 --- a/Tests/TestCase.php +++ /dev/null @@ -1,179 +0,0 @@ -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 - */ - -/** - * Abstract base class for test case classes. - * - * @category PHP - * @package CodeCoverage - * @subpackage Tests - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version Release: @package_version@ - * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 - */ -abstract class PHP_CodeCoverage_TestCase extends PHPUnit_Framework_TestCase -{ - protected function getCoverageForBankAccount() - { - $coverage = new PHP_CodeCoverage( - $this->setUpXdebugStubForBankAccount(), new PHP_CodeCoverage_Filter - ); - - $coverage->start( - new BankAccountTest('testBalanceIsInitiallyZero'), TRUE - ); - $coverage->stop(); - - $coverage->start( - new BankAccountTest('testBalanceCannotBecomeNegative') - ); - $coverage->stop(); - - $coverage->start( - new BankAccountTest('testBalanceCannotBecomeNegative2') - ); - $coverage->stop(); - - $coverage->start( - new BankAccountTest('testDepositWithdrawMoney') - ); - $coverage->stop(); - - return $coverage; - } - - protected function setUpXdebugStubForBankAccount() - { - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); - $stub->expects($this->any()) - ->method('stop') - ->will($this->onConsecutiveCalls( - array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 9 => -2, - 13 => -1, - 14 => -1, - 15 => -1, - 16 => -1, - 18 => -1, - 22 => -1, - 24 => -1, - 25 => -2, - 29 => -1, - 31 => -1, - 32 => -2 - ) - ), - array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 16 => 1, - 29 => 1, - ) - ), - array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 16 => 1, - 22 => 1, - ) - ), - array( - TEST_FILES_PATH . 'BankAccount.php' => array( - 8 => 1, - 13 => 1, - 14 => 1, - 15 => 1, - 18 => 1, - 22 => 1, - 24 => 1, - 29 => 1, - 31 => 1, - ) - ) - )); - - return $stub; - } - - protected function getCoverageForFileWithIgnoredLines() - { - $coverage = new PHP_CodeCoverage( - $this->setUpXdebugStubForFileWithIgnoredLines(), - new PHP_CodeCoverage_Filter - ); - - $coverage->start('FileWithIgnoredLines', TRUE); - $coverage->stop(); - - return $coverage; - } - - protected function setUpXdebugStubForFileWithIgnoredLines() - { - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); - $stub->expects($this->any()) - ->method('stop') - ->will($this->returnValue( - array( - TEST_FILES_PATH . 'source_with_ignore.php' => array( - 2 => 1, - 4 => -1, - 6 => -1, - 7 => 1 - ) - ) - )); - - return $stub; - } -} diff --git a/Tests/_files/BankAccount-clover.xml b/Tests/_files/BankAccount-clover.xml deleted file mode 100644 index 578a7ccc1..000000000 --- a/Tests/_files/BankAccount-clover.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/_files/CoverageClassExtendedTest.php b/Tests/_files/CoverageClassExtendedTest.php deleted file mode 100644 index df12d3470..000000000 --- a/Tests/_files/CoverageClassExtendedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoverageClassTest.php b/Tests/_files/CoverageClassTest.php deleted file mode 100644 index 7f569ae60..000000000 --- a/Tests/_files/CoverageClassTest.php +++ /dev/null @@ -1,12 +0,0 @@ -publicMethod(); - } -} diff --git a/Tests/_files/CoverageFunctionTest.php b/Tests/_files/CoverageFunctionTest.php deleted file mode 100644 index c621fd210..000000000 --- a/Tests/_files/CoverageFunctionTest.php +++ /dev/null @@ -1,11 +0,0 @@ -publicMethod(); - } -} diff --git a/Tests/_files/CoverageNotPrivateTest.php b/Tests/_files/CoverageNotPrivateTest.php deleted file mode 100644 index 12b56e80e..000000000 --- a/Tests/_files/CoverageNotPrivateTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoverageNotProtectedTest.php b/Tests/_files/CoverageNotProtectedTest.php deleted file mode 100644 index c69d261de..000000000 --- a/Tests/_files/CoverageNotProtectedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoverageNotPublicTest.php b/Tests/_files/CoverageNotPublicTest.php deleted file mode 100644 index aebfe4bd9..000000000 --- a/Tests/_files/CoverageNotPublicTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoveragePrivateTest.php b/Tests/_files/CoveragePrivateTest.php deleted file mode 100644 index f09560d3a..000000000 --- a/Tests/_files/CoveragePrivateTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoverageProtectedTest.php b/Tests/_files/CoverageProtectedTest.php deleted file mode 100644 index 9b3acbf6c..000000000 --- a/Tests/_files/CoverageProtectedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/CoveragePublicTest.php b/Tests/_files/CoveragePublicTest.php deleted file mode 100644 index 480a522b4..000000000 --- a/Tests/_files/CoveragePublicTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageClassExtendedTest.php b/Tests/_files/NamespaceCoverageClassExtendedTest.php deleted file mode 100644 index d0954cba9..000000000 --- a/Tests/_files/NamespaceCoverageClassExtendedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageClassTest.php b/Tests/_files/NamespaceCoverageClassTest.php deleted file mode 100644 index 63912c089..000000000 --- a/Tests/_files/NamespaceCoverageClassTest.php +++ /dev/null @@ -1,12 +0,0 @@ -publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageMethodTest.php b/Tests/_files/NamespaceCoverageMethodTest.php deleted file mode 100644 index 35dfb8b1d..000000000 --- a/Tests/_files/NamespaceCoverageMethodTest.php +++ /dev/null @@ -1,12 +0,0 @@ -publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageNotPrivateTest.php b/Tests/_files/NamespaceCoverageNotPrivateTest.php deleted file mode 100644 index 552c9ec50..000000000 --- a/Tests/_files/NamespaceCoverageNotPrivateTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageNotProtectedTest.php b/Tests/_files/NamespaceCoverageNotProtectedTest.php deleted file mode 100644 index 33fc8c72c..000000000 --- a/Tests/_files/NamespaceCoverageNotProtectedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageNotPublicTest.php b/Tests/_files/NamespaceCoverageNotPublicTest.php deleted file mode 100644 index ccbc5009c..000000000 --- a/Tests/_files/NamespaceCoverageNotPublicTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoveragePrivateTest.php b/Tests/_files/NamespaceCoveragePrivateTest.php deleted file mode 100644 index cce7ba9d6..000000000 --- a/Tests/_files/NamespaceCoveragePrivateTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoverageProtectedTest.php b/Tests/_files/NamespaceCoverageProtectedTest.php deleted file mode 100644 index dbbcc1c31..000000000 --- a/Tests/_files/NamespaceCoverageProtectedTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NamespaceCoveragePublicTest.php b/Tests/_files/NamespaceCoveragePublicTest.php deleted file mode 100644 index bf1bff8c4..000000000 --- a/Tests/_files/NamespaceCoveragePublicTest.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function testSomething() - { - $o = new Foo\CoveredClass; - $o->publicMethod(); - } -} diff --git a/Tests/_files/NotExistingCoveredElementTest.php b/Tests/_files/NotExistingCoveredElementTest.php deleted file mode 100644 index be07ef45e..000000000 --- a/Tests/_files/NotExistingCoveredElementTest.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - public function testThree() - { - } -} diff --git a/Tests/_files/ignored-lines-clover.xml b/Tests/_files/ignored-lines-clover.xml deleted file mode 100644 index cefc8f9b4..000000000 --- a/Tests/_files/ignored-lines-clover.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Tests/_files/source_with_namespace.php b/Tests/_files/source_with_namespace.php deleted file mode 100644 index 1709fb6c7..000000000 --- a/Tests/_files/source_with_namespace.php +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/scripts/extract-release-notes.php b/build/scripts/extract-release-notes.php new file mode 100755 index 000000000..390143980 --- /dev/null +++ b/build/scripts/extract-release-notes.php @@ -0,0 +1,47 @@ +#!/usr/bin/env php +' . PHP_EOL; + + exit(1); +} + +$version = $argv[1]; +$versionSeries = explode('.', $version)[0] . '.' . explode('.', $version)[1]; + +$file = __DIR__ . '/../../ChangeLog-' . $versionSeries . '.md'; + +if (!is_file($file) || !is_readable($file)) { + print $file . ' cannot be read' . PHP_EOL; + + exit(1); +} + +$buffer = ''; +$append = false; + +foreach (file($file) as $line) { + if (str_starts_with($line, '## [' . $version . ']')) { + $append = true; + + continue; + } + + if ($append && (str_starts_with($line, '## ') || str_starts_with($line, '['))) { + break; + } + + if ($append) { + $buffer .= $line; + } +} + +$buffer = trim($buffer); + +if ($buffer === '') { + print 'Unable to extract release notes' . PHP_EOL; + + exit(1); +} + +print $buffer . PHP_EOL; diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..bfd990ca1 --- /dev/null +++ b/composer.json @@ -0,0 +1,67 @@ +{ + "name": "phpunit/php-code-coverage", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "type": "library", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy" + }, + "config": { + "platform": { + "php": "8.3.0" + }, + "optimize-autoloader": true, + "sort-packages": true + }, + "prefer-stable": true, + "require": { + "php": ">=8.3", + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "12.3.x-dev" + } + } +} diff --git a/package.xml b/package.xml deleted file mode 100644 index 0d7a5f75d..000000000 --- a/package.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - PHP_CodeCoverage - pear.phpunit.de - Library that provides collection, processing, and rendering functionality for PHP code coverage information. - Library that provides collection, processing, and rendering functionality for PHP code coverage information. - - Sebastian Bergmann - sb - sb@sebastian-bergmann.de - yes - - 2011-02-12 - - 1.0.4 - 1.0.3 - - - stable - stable - - BSD License - http://github.com/sebastianbergmann/php-code-coverage/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5.2.7 - - - 1.9.2 - - - ConsoleTools - components.ez.no - 1.6 - - - File_Iterator - pear.phpunit.de - 1.2.2 - - - PHP_TokenStream - pear.phpunit.de - 1.0.0 - - - Text_Template - pear.phpunit.de - 1.0.0 - - - - - dom - - - reflection - - - spl - - - xdebug - 2.0.5 - - - - - - - windows - - - - - - - - - - - - - - diff --git a/phpcov.bat b/phpcov.bat deleted file mode 100644 index 72bb19e19..000000000 --- a/phpcov.bat +++ /dev/null @@ -1,38 +0,0 @@ -@echo off -REM PHP_CodeCoverage -REM -REM Copyright (c) 2009-2010, Sebastian Bergmann . -REM All rights reserved. -REM -REM Redistribution and use in source and binary forms, with or without -REM modification, are permitted provided that the following conditions -REM are met: -REM -REM * Redistributions of source code must retain the above copyright -REM notice, this list of conditions and the following disclaimer. -REM -REM * Redistributions in binary form must reproduce the above copyright -REM notice, this list of conditions and the following disclaimer in -REM the documentation and/or other materials provided with the -REM distribution. -REM -REM * Neither the name of Sebastian Bergmann nor the names of his -REM contributors may be used to endorse or promote products derived -REM from this software without specific prior written permission. -REM -REM THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -REM "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -REM LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -REM FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -REM COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -REM INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -REM BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -REM LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -REM CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC -REM LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -REM ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -REM POSSIBILITY OF SUCH DAMAGE. -REM - -set PHPBIN="@php_bin@" -"@php_bin@" "@bin_dir@\phpcov" %* diff --git a/phpcov.php b/phpcov.php deleted file mode 100755 index ebf65500c..000000000 --- a/phpcov.php +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env php -. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * * Neither the name of Sebastian Bergmann nor the names of his - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * @package PHP_CodeCoverage - * @author Sebastian Bergmann - * @copyright 2009-2011 Sebastian Bergmann - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @since File available since Release 1.0.0 - */ - -if (strpos('@php_bin@', '@php_bin') === 0) { - set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path()); -} - -require 'PHP/CodeCoverage/TextUI/Command.php'; - -PHP_CodeCoverage_TextUI_Command::main(); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..7d7f95975 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,57 @@ +parameters: + level: 5 + paths: + - src + - tests/tests + - tests/bootstrap.php + + checkTooWideReturnTypesInProtectedAndPublicMethods: true + reportAlwaysTrueInLastCondition: true + reportPossiblyNonexistentConstantArrayOffset: true + reportPossiblyNonexistentGeneralArrayOffset: true + treatPhpDocTypesAsCertain: false + + strictRules: + allRules: false + booleansInConditions: true + closureUsesThis: true + disallowedBacktick: true + disallowedEmpty: true + disallowedImplicitArrayCreation: true + disallowedLooseComparison: true + disallowedShortTernary: true + illegalConstructorMethodCall: true + matchingInheritedMethodNames: true + noVariableVariables: true + numericOperandsInArithmeticOperators: true + overwriteVariablesWithLoop: true + requireParentConstructorCall: true + strictArrayFilter: true + strictFunctionCalls: true + switchConditionsMatchingType: true + uselessCast: true + + ergebnis: + allRules: false + final: + enabled: true + classesNotRequiredToBeAbstractOrFinal: + - SebastianBergmann\CodeCoverage\Report\Xml\File + + type_coverage: + declare: 100 + return: 100 + param: 100 + property: 100 + constant: 100 + + ignoreErrors: + # Ignore errors caused by defensive programming + - '#Call to function assert\(\) with true will always evaluate to true.#' + - '#Call to method .* will always evaluate to true.#' + - '#Call to method .* will always evaluate to false.#' + - '#Instanceof between .* and .* will always evaluate to true.#' + - '#SebastianBergmann\\CodeCoverage\\Node\\Iterator::current\(\) should be covariant with return type#' + +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 000000000..c7f22893f --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + tests/tests + + + + + + src + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index a231776d9..000000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Tests/PHP - - - - - - - - - - - - - - - - PHP - - - diff --git a/scripts/auto_append.php b/scripts/auto_append.php deleted file mode 100644 index 3b152aca2..000000000 --- a/scripts/auto_append.php +++ /dev/null @@ -1,7 +0,0 @@ -stop(); - -require 'PHP/CodeCoverage/Report/HTML.php'; - -$writer = new PHP_CodeCoverage_Report_HTML; -$writer->process($coverage, '/tmp/coverage'); diff --git a/scripts/auto_prepend.php b/scripts/auto_prepend.php deleted file mode 100644 index a44000195..000000000 --- a/scripts/auto_prepend.php +++ /dev/null @@ -1,10 +0,0 @@ -filter(); - -$filter->addFileToBlacklist(__FILE__); -$filter->addFileToBlacklist(dirname(__FILE__) . '/auto_append.php'); - -$coverage->start($_SERVER['SCRIPT_FILENAME']); diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php new file mode 100644 index 000000000..bedc11927 --- /dev/null +++ b/src/CodeCoverage.php @@ -0,0 +1,647 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use function array_diff; +use function array_diff_key; +use function array_flip; +use function array_keys; +use function array_merge; +use function array_unique; +use function count; +use function explode; +use function is_file; +use function sort; +use ReflectionClass; +use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData; +use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; +use SebastianBergmann\CodeCoverage\Driver\Driver; +use SebastianBergmann\CodeCoverage\Node\Builder; +use SebastianBergmann\CodeCoverage\Node\Directory; +use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingSourceAnalyser; +use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; +use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingSourceAnalyser; +use SebastianBergmann\CodeCoverage\Test\Target\MapBuilder; +use SebastianBergmann\CodeCoverage\Test\Target\Mapper; +use SebastianBergmann\CodeCoverage\Test\Target\TargetCollection; +use SebastianBergmann\CodeCoverage\Test\Target\TargetCollectionValidator; +use SebastianBergmann\CodeCoverage\Test\Target\ValidationResult; +use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize; +use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus; + +/** + * Provides collection functionality for PHP code coverage information. + * + * @phpstan-type TestType array{size: string, status: string} + * @phpstan-type TargetedLines array> + */ +final class CodeCoverage +{ + private const string UNCOVERED_FILES = 'UNCOVERED_FILES'; + private readonly Driver $driver; + private readonly Filter $filter; + private ?Mapper $targetMapper = null; + private bool $checkForUnintentionallyCoveredCode = false; + private bool $includeUncoveredFiles = true; + private bool $ignoreDeprecatedCode = false; + private ?string $currentId = null; + private ?TestSize $currentSize = null; + private ProcessedCodeCoverageData $data; + private bool $useAnnotationsForIgnoringCode = true; + + /** + * @var array + */ + private array $tests = []; + + /** + * @var list + */ + private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = []; + private ?FileAnalyser $analyser = null; + private ?string $cacheDirectory = null; + private ?Directory $cachedReport = null; + + public function __construct(Driver $driver, Filter $filter) + { + $this->driver = $driver; + $this->filter = $filter; + $this->data = new ProcessedCodeCoverageData; + } + + /** + * Returns the code coverage information as a graph of node objects. + */ + public function getReport(): Directory + { + if ($this->cachedReport === null) { + $this->cachedReport = (new Builder($this->analyser()))->build($this); + } + + return $this->cachedReport; + } + + /** + * Clears collected code coverage data. + */ + public function clear(): void + { + $this->currentId = null; + $this->currentSize = null; + $this->data = new ProcessedCodeCoverageData; + $this->tests = []; + $this->cachedReport = null; + } + + /** + * @internal + */ + public function clearCache(): void + { + $this->cachedReport = null; + } + + /** + * Returns the filter object used. + */ + public function filter(): Filter + { + return $this->filter; + } + + /** + * Returns the collected code coverage data. + */ + public function getData(bool $raw = false): ProcessedCodeCoverageData + { + if (!$raw) { + if ($this->includeUncoveredFiles) { + $this->addUncoveredFilesFromFilter(); + } + } + + return $this->data; + } + + /** + * Sets the coverage data. + */ + public function setData(ProcessedCodeCoverageData $data): void + { + $this->data = $data; + } + + /** + * @return array + */ + public function getTests(): array + { + return $this->tests; + } + + /** + * @param array $tests + */ + public function setTests(array $tests): void + { + $this->tests = $tests; + } + + public function start(string $id, ?TestSize $size = null, bool $clear = false): void + { + if ($clear) { + $this->clear(); + } + + $this->currentId = $id; + $this->currentSize = $size; + + $this->driver->start(); + + $this->cachedReport = null; + } + + public function stop(bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): RawCodeCoverageData + { + $data = $this->driver->stop(); + + $this->append($data, null, $append, $status, $covers, $uses); + + $this->currentId = null; + $this->currentSize = null; + $this->cachedReport = null; + + return $data; + } + + /** + * @throws ReflectionException + * @throws TestIdMissingException + * @throws UnintentionallyCoveredCodeException + */ + public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): void + { + if ($id === null) { + $id = $this->currentId; + } + + if ($id === null) { + throw new TestIdMissingException; + } + + if ($status === null) { + $status = TestStatus::unknown(); + } + + if ($covers === null) { + $covers = TargetCollection::fromArray([]); + } + + if ($uses === null) { + $uses = TargetCollection::fromArray([]); + } + + $size = $this->currentSize; + + if ($size === null) { + $size = TestSize::unknown(); + } + + $this->cachedReport = null; + + $this->applyFilter($rawData); + + $this->applyExecutableLinesFilter($rawData); + + if ($this->useAnnotationsForIgnoringCode) { + $this->applyIgnoredLinesFilter($rawData); + } + + $this->data->initializeUnseenData($rawData); + + if (!$append) { + return; + } + + if ($id === self::UNCOVERED_FILES) { + return; + } + + $linesToBeCovered = false; + $linesToBeUsed = []; + + if ($covers !== false) { + $linesToBeCovered = $this->targetMapper()->mapTargets($covers); + } + + if ($linesToBeCovered !== false) { + $linesToBeUsed = $this->targetMapper()->mapTargets($uses); + } + + $this->applyCoversAndUsesFilter( + $rawData, + $linesToBeCovered, + $linesToBeUsed, + $size, + ); + + if ($rawData->lineCoverage() === []) { + return; + } + + $this->tests[$id] = [ + 'size' => $size->asString(), + 'status' => $status->asString(), + ]; + + $this->data->markCodeAsExecutedByTestCase($id, $rawData); + } + + /** + * Merges the data from another instance. + */ + public function merge(self $that): void + { + $this->filter->includeFiles( + $that->filter()->files(), + ); + + $this->data->merge($that->data); + + $this->tests = array_merge($this->tests, $that->getTests()); + + $this->cachedReport = null; + } + + public function enableCheckForUnintentionallyCoveredCode(): void + { + $this->checkForUnintentionallyCoveredCode = true; + } + + public function disableCheckForUnintentionallyCoveredCode(): void + { + $this->checkForUnintentionallyCoveredCode = false; + } + + public function includeUncoveredFiles(): void + { + $this->includeUncoveredFiles = true; + } + + public function excludeUncoveredFiles(): void + { + $this->includeUncoveredFiles = false; + } + + public function enableAnnotationsForIgnoringCode(): void + { + $this->useAnnotationsForIgnoringCode = true; + } + + public function disableAnnotationsForIgnoringCode(): void + { + $this->useAnnotationsForIgnoringCode = false; + } + + public function ignoreDeprecatedCode(): void + { + $this->ignoreDeprecatedCode = true; + } + + public function doNotIgnoreDeprecatedCode(): void + { + $this->ignoreDeprecatedCode = false; + } + + /** + * @phpstan-assert-if-true !null $this->cacheDirectory + */ + public function cachesStaticAnalysis(): bool + { + return $this->cacheDirectory !== null; + } + + public function cacheStaticAnalysis(string $directory): void + { + $this->cacheDirectory = $directory; + } + + public function doNotCacheStaticAnalysis(): void + { + $this->cacheDirectory = null; + } + + /** + * @throws StaticAnalysisCacheNotConfiguredException + */ + public function cacheDirectory(): string + { + if (!$this->cachesStaticAnalysis()) { + throw new StaticAnalysisCacheNotConfiguredException( + 'The static analysis cache is not configured', + ); + } + + return $this->cacheDirectory; + } + + /** + * @param class-string $className + */ + public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void + { + $this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className; + } + + public function enableBranchAndPathCoverage(): void + { + $this->driver->enableBranchAndPathCoverage(); + } + + public function disableBranchAndPathCoverage(): void + { + $this->driver->disableBranchAndPathCoverage(); + } + + public function collectsBranchAndPathCoverage(): bool + { + return $this->driver->collectsBranchAndPathCoverage(); + } + + public function validate(TargetCollection $targets): ValidationResult + { + return (new TargetCollectionValidator)->validate($this->targetMapper(), $targets); + } + + /** + * @param false|TargetedLines $linesToBeCovered + * @param TargetedLines $linesToBeUsed + * + * @throws ReflectionException + * @throws UnintentionallyCoveredCodeException + */ + private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size): void + { + if ($linesToBeCovered === false) { + $rawData->clear(); + + return; + } + + if ($linesToBeCovered === []) { + return; + } + + if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) { + $this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed); + } + + $rawLineData = $rawData->lineCoverage(); + $filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered); + + foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) { + $rawData->removeCoverageDataForFile($fileWithNoCoverage); + } + + foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) { + $rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines); + $rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines); + } + } + + private function applyFilter(RawCodeCoverageData $data): void + { + if ($this->filter->isEmpty()) { + return; + } + + foreach (array_keys($data->lineCoverage()) as $filename) { + if ($this->filter->isExcluded($filename)) { + $data->removeCoverageDataForFile($filename); + } + } + } + + private function applyExecutableLinesFilter(RawCodeCoverageData $data): void + { + foreach (array_keys($data->lineCoverage()) as $filename) { + if (!$this->filter->isFile($filename)) { + continue; + } + + $linesToBranchMap = $this->analyser()->analyse($filename)->executableLines(); + + $data->keepLineCoverageDataOnlyForLines( + $filename, + array_keys($linesToBranchMap), + ); + + $data->markExecutableLineByBranch( + $filename, + $linesToBranchMap, + ); + } + } + + private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void + { + foreach (array_keys($data->lineCoverage()) as $filename) { + if (!$this->filter->isFile($filename)) { + continue; + } + + $data->removeCoverageDataForLines( + $filename, + $this->analyser()->analyse($filename)->ignoredLines(), + ); + } + } + + /** + * @throws UnintentionallyCoveredCodeException + */ + private function addUncoveredFilesFromFilter(): void + { + $uncoveredFiles = array_diff( + $this->filter->files(), + $this->data->coveredFiles(), + ); + + foreach ($uncoveredFiles as $uncoveredFile) { + if (is_file($uncoveredFile)) { + $this->append( + RawCodeCoverageData::fromUncoveredFile( + $uncoveredFile, + $this->analyser(), + ), + self::UNCOVERED_FILES, + ); + } + } + } + + /** + * @param TargetedLines $linesToBeCovered + * @param TargetedLines $linesToBeUsed + * + * @throws ReflectionException + * @throws UnintentionallyCoveredCodeException + */ + private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void + { + $allowedLines = $this->getAllowedLines( + $linesToBeCovered, + $linesToBeUsed, + ); + + $unintentionallyCoveredUnits = []; + + foreach ($data->lineCoverage() as $file => $_data) { + foreach ($_data as $line => $flag) { + if ($flag === 1 && !isset($allowedLines[$file][$line])) { + $unintentionallyCoveredUnits[] = $this->targetMapper->lookup($file, $line); + } + } + } + + $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits); + + if ($unintentionallyCoveredUnits !== []) { + throw new UnintentionallyCoveredCodeException( + $unintentionallyCoveredUnits, + ); + } + } + + /** + * @param TargetedLines $linesToBeCovered + * @param TargetedLines $linesToBeUsed + * + * @return TargetedLines + */ + private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array + { + $allowedLines = []; + + foreach (array_keys($linesToBeCovered) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = []; + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeCovered[$file], + ); + } + + foreach (array_keys($linesToBeUsed) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = []; + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeUsed[$file], + ); + } + + foreach (array_keys($allowedLines) as $file) { + $allowedLines[$file] = array_flip( + array_unique($allowedLines[$file]), + ); + } + + return $allowedLines; + } + + /** + * @param list $unintentionallyCoveredUnits + * + * @throws ReflectionException + * + * @return list + */ + private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array + { + $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits); + $processed = []; + + foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) { + $tmp = explode('::', $unintentionallyCoveredUnit); + + if (count($tmp) !== 2) { + $processed[] = $unintentionallyCoveredUnit; + + continue; + } + + try { + $class = new ReflectionClass($tmp[0]); + + foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) { + if ($class->isSubclassOf($parentClass)) { + continue 2; + } + } + } catch (\ReflectionException $e) { + throw new ReflectionException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + + $processed[] = $tmp[0]; + } + + $processed = array_unique($processed); + + sort($processed); + + return $processed; + } + + private function targetMapper(): Mapper + { + if ($this->targetMapper !== null) { + return $this->targetMapper; + } + + $this->targetMapper = new Mapper( + (new MapBuilder)->build($this->filter, $this->analyser()), + ); + + return $this->targetMapper; + } + + private function analyser(): FileAnalyser + { + if ($this->analyser !== null) { + return $this->analyser; + } + + $sourceAnalyser = new ParsingSourceAnalyser; + + if ($this->cachesStaticAnalysis()) { + $sourceAnalyser = new CachingSourceAnalyser( + $this->cacheDirectory, + $sourceAnalyser, + ); + } + + $this->analyser = new FileAnalyser( + $sourceAnalyser, + $this->useAnnotationsForIgnoringCode, + $this->ignoreDeprecatedCode, + ); + + return $this->analyser; + } +} diff --git a/src/Data/ProcessedCodeCoverageData.php b/src/Data/ProcessedCodeCoverageData.php new file mode 100644 index 000000000..57ccbb166 --- /dev/null +++ b/src/Data/ProcessedCodeCoverageData.php @@ -0,0 +1,283 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Data; + +use function array_key_exists; +use function array_keys; +use function array_merge; +use function array_unique; +use function count; +use function is_array; +use function ksort; +use SebastianBergmann\CodeCoverage\Driver\Driver; +use SebastianBergmann\CodeCoverage\Driver\XdebugDriver; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @phpstan-import-type XdebugFunctionCoverageType from XdebugDriver + * + * @phpstan-type TestIdType string + * @phpstan-type FunctionCoverageDataType array{ + * branches: array, + * out: array, + * out_hit: array, + * }>, + * paths: array, + * hit: list, + * }>, + * hit: list + * } + * @phpstan-type FunctionCoverageType array> + */ +final class ProcessedCodeCoverageData +{ + /** + * Line coverage data. + * An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids. + * + * @var array>> + */ + private array $lineCoverage = []; + + /** + * Function coverage data. + * Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array + * of testcase ids. + * + * @var FunctionCoverageType + */ + private array $functionCoverage = []; + + public function initializeUnseenData(RawCodeCoverageData $rawData): void + { + foreach ($rawData->lineCoverage() as $file => $lines) { + if (!isset($this->lineCoverage[$file])) { + $this->lineCoverage[$file] = []; + + foreach ($lines as $k => $v) { + $this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : []; + } + } + } + + foreach ($rawData->functionCoverage() as $file => $functions) { + foreach ($functions as $functionName => $functionData) { + if (isset($this->functionCoverage[$file][$functionName])) { + $this->initPreviouslySeenFunction($file, $functionName, $functionData); + } else { + $this->initPreviouslyUnseenFunction($file, $functionName, $functionData); + } + } + } + } + + public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void + { + foreach ($executedCode->lineCoverage() as $file => $lines) { + foreach ($lines as $k => $v) { + if ($v === Driver::LINE_EXECUTED) { + $this->lineCoverage[$file][$k][] = $testCaseId; + } + } + } + + foreach ($executedCode->functionCoverage() as $file => $functions) { + foreach ($functions as $functionName => $functionData) { + foreach ($functionData['branches'] as $branchId => $branchData) { + if ($branchData['hit'] === Driver::BRANCH_HIT) { + $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'][] = $testCaseId; + } + } + + foreach ($functionData['paths'] as $pathId => $pathData) { + if ($pathData['hit'] === Driver::BRANCH_HIT) { + $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'][] = $testCaseId; + } + } + } + } + } + + public function setLineCoverage(array $lineCoverage): void + { + $this->lineCoverage = $lineCoverage; + } + + public function lineCoverage(): array + { + ksort($this->lineCoverage); + + return $this->lineCoverage; + } + + public function setFunctionCoverage(array $functionCoverage): void + { + $this->functionCoverage = $functionCoverage; + } + + public function functionCoverage(): array + { + ksort($this->functionCoverage); + + return $this->functionCoverage; + } + + public function coveredFiles(): array + { + ksort($this->lineCoverage); + + return array_keys($this->lineCoverage); + } + + public function renameFile(string $oldFile, string $newFile): void + { + $this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile]; + + if (isset($this->functionCoverage[$oldFile])) { + $this->functionCoverage[$newFile] = $this->functionCoverage[$oldFile]; + } + + unset($this->lineCoverage[$oldFile], $this->functionCoverage[$oldFile]); + } + + public function merge(self $newData): void + { + foreach ($newData->lineCoverage as $file => $lines) { + if (!isset($this->lineCoverage[$file])) { + $this->lineCoverage[$file] = $lines; + + continue; + } + + // we should compare the lines if any of two contains data + $compareLineNumbers = array_unique( + array_merge( + array_keys($this->lineCoverage[$file]), + array_keys($newData->lineCoverage[$file]), + ), + ); + + foreach ($compareLineNumbers as $line) { + $thatPriority = $this->priorityForLine($newData->lineCoverage[$file], $line); + $thisPriority = $this->priorityForLine($this->lineCoverage[$file], $line); + + if ($thatPriority > $thisPriority) { + $this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line]; + } elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) { + $this->lineCoverage[$file][$line] = array_unique( + array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]), + ); + } + } + } + + foreach ($newData->functionCoverage as $file => $functions) { + if (!isset($this->functionCoverage[$file])) { + $this->functionCoverage[$file] = $functions; + + continue; + } + + foreach ($functions as $functionName => $functionData) { + if (isset($this->functionCoverage[$file][$functionName])) { + $this->initPreviouslySeenFunction($file, $functionName, $functionData); + } else { + $this->initPreviouslyUnseenFunction($file, $functionName, $functionData); + } + + foreach ($functionData['branches'] as $branchId => $branchData) { + $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'], $branchData['hit'])); + } + + foreach ($functionData['paths'] as $pathId => $pathData) { + $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'], $pathData['hit'])); + } + } + } + } + + /** + * Determine the priority for a line. + * + * 1 = the line is not set + * 2 = the line has not been tested + * 3 = the line is dead code + * 4 = the line has been tested + * + * During a merge, a higher number is better. + * + * @return 1|2|3|4 + */ + private function priorityForLine(array $data, int $line): int + { + if (!array_key_exists($line, $data)) { + return 1; + } + + if (is_array($data[$line]) && count($data[$line]) === 0) { + return 2; + } + + if ($data[$line] === null) { + return 3; + } + + return 4; + } + + /** + * For a function we have never seen before, copy all data over and simply init the 'hit' array. + * + * @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData + */ + private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void + { + $this->functionCoverage[$file][$functionName] = $functionData; + + foreach (array_keys($functionData['branches']) as $branchId) { + $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = []; + } + + foreach (array_keys($functionData['paths']) as $pathId) { + $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = []; + } + } + + /** + * For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths. + * Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling + * containers) mean that the functions inside a file cannot be relied upon to be static. + * + * @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData + */ + private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void + { + foreach ($functionData['branches'] as $branchId => $branchData) { + if (!isset($this->functionCoverage[$file][$functionName]['branches'][$branchId])) { + $this->functionCoverage[$file][$functionName]['branches'][$branchId] = $branchData; + $this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = []; + } + } + + foreach ($functionData['paths'] as $pathId => $pathData) { + if (!isset($this->functionCoverage[$file][$functionName]['paths'][$pathId])) { + $this->functionCoverage[$file][$functionName]['paths'][$pathId] = $pathData; + $this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = []; + } + } + } +} diff --git a/src/Data/RawCodeCoverageData.php b/src/Data/RawCodeCoverageData.php new file mode 100644 index 000000000..f6e847ae3 --- /dev/null +++ b/src/Data/RawCodeCoverageData.php @@ -0,0 +1,285 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Data; + +use function array_diff; +use function array_diff_key; +use function array_flip; +use function array_intersect; +use function array_intersect_key; +use function array_map; +use function count; +use function explode; +use function file_get_contents; +use function in_array; +use function is_file; +use function preg_replace; +use function range; +use function str_ends_with; +use function str_starts_with; +use function trim; +use SebastianBergmann\CodeCoverage\Driver\Driver; +use SebastianBergmann\CodeCoverage\Driver\XdebugDriver; +use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @phpstan-import-type XdebugFunctionsCoverageType from XdebugDriver + * @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from XdebugDriver + * @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from XdebugDriver + */ +final class RawCodeCoverageData +{ + /** + * @var array> + */ + private static array $emptyLineCache = []; + + /** + * @var XdebugCodeCoverageWithoutPathCoverageType + */ + private array $lineCoverage; + + /** + * @var array + */ + private array $functionCoverage; + + /** + * @param XdebugCodeCoverageWithoutPathCoverageType $rawCoverage + */ + public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self + { + return new self($rawCoverage, []); + } + + /** + * @param XdebugCodeCoverageWithPathCoverageType $rawCoverage + */ + public static function fromXdebugWithPathCoverage(array $rawCoverage): self + { + $lineCoverage = []; + $functionCoverage = []; + + foreach ($rawCoverage as $file => $fileCoverageData) { + // Xdebug annotates the function name of traits, strip that off + foreach ($fileCoverageData['functions'] as $existingKey => $data) { + if (str_ends_with($existingKey, '}') && !str_starts_with($existingKey, '{')) { // don't want to catch {main} + $newKey = preg_replace('/\{.*}$/', '', $existingKey); + $fileCoverageData['functions'][$newKey] = $data; + unset($fileCoverageData['functions'][$existingKey]); + } + } + + $lineCoverage[$file] = $fileCoverageData['lines']; + $functionCoverage[$file] = $fileCoverageData['functions']; + } + + return new self($lineCoverage, $functionCoverage); + } + + public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self + { + $lineCoverage = array_map( + static fn (): int => Driver::LINE_NOT_EXECUTED, + $analyser->analyse($filename)->executableLines(), + ); + + return new self([$filename => $lineCoverage], []); + } + + /** + * @param XdebugCodeCoverageWithoutPathCoverageType $lineCoverage + * @param array $functionCoverage + */ + private function __construct(array $lineCoverage, array $functionCoverage) + { + $this->lineCoverage = $lineCoverage; + $this->functionCoverage = $functionCoverage; + + $this->skipEmptyLines(); + } + + public function clear(): void + { + $this->lineCoverage = $this->functionCoverage = []; + } + + /** + * @return XdebugCodeCoverageWithoutPathCoverageType + */ + public function lineCoverage(): array + { + return $this->lineCoverage; + } + + /** + * @return array + */ + public function functionCoverage(): array + { + return $this->functionCoverage; + } + + public function removeCoverageDataForFile(string $filename): void + { + unset($this->lineCoverage[$filename], $this->functionCoverage[$filename]); + } + + /** + * @param int[] $lines + */ + public function keepLineCoverageDataOnlyForLines(string $filename, array $lines): void + { + if (!isset($this->lineCoverage[$filename])) { + return; + } + + $this->lineCoverage[$filename] = array_intersect_key( + $this->lineCoverage[$filename], + array_flip($lines), + ); + } + + /** + * @param int[] $linesToBranchMap + */ + public function markExecutableLineByBranch(string $filename, array $linesToBranchMap): void + { + if (!isset($this->lineCoverage[$filename])) { + return; + } + + $linesByBranch = []; + + foreach ($linesToBranchMap as $line => $branch) { + $linesByBranch[$branch][] = $line; + } + + foreach ($this->lineCoverage[$filename] as $line => $lineStatus) { + if (!isset($linesToBranchMap[$line])) { + continue; + } + + $branch = $linesToBranchMap[$line]; + + if (!isset($linesByBranch[$branch])) { + continue; + } + + foreach ($linesByBranch[$branch] as $lineInBranch) { + $this->lineCoverage[$filename][$lineInBranch] = $lineStatus; + } + + if (Driver::LINE_EXECUTED === $lineStatus) { + unset($linesByBranch[$branch]); + } + } + } + + /** + * @param int[] $lines + */ + public function keepFunctionCoverageDataOnlyForLines(string $filename, array $lines): void + { + if (!isset($this->functionCoverage[$filename])) { + return; + } + + foreach ($this->functionCoverage[$filename] as $functionName => $functionData) { + foreach ($functionData['branches'] as $branchId => $branch) { + if (count(array_diff(range($branch['line_start'], $branch['line_end']), $lines)) > 0) { + unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]); + + foreach ($functionData['paths'] as $pathId => $path) { + if (in_array($branchId, $path['path'], true)) { + unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]); + } + } + } + } + } + } + + /** + * @param int[] $lines + */ + public function removeCoverageDataForLines(string $filename, array $lines): void + { + if ($lines === []) { + return; + } + + if (!isset($this->lineCoverage[$filename])) { + return; + } + + $this->lineCoverage[$filename] = array_diff_key( + $this->lineCoverage[$filename], + array_flip($lines), + ); + + if (isset($this->functionCoverage[$filename])) { + foreach ($this->functionCoverage[$filename] as $functionName => $functionData) { + foreach ($functionData['branches'] as $branchId => $branch) { + if (count(array_intersect($lines, range($branch['line_start'], $branch['line_end']))) > 0) { + unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]); + + foreach ($functionData['paths'] as $pathId => $path) { + if (in_array($branchId, $path['path'], true)) { + unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]); + } + } + } + } + } + } + } + + /** + * At the end of a file, the PHP interpreter always sees an implicit return. Where this occurs in a file that has + * e.g. a class definition, that line cannot be invoked from a test and results in confusing coverage. This engine + * implementation detail therefore needs to be masked which is done here by simply ensuring that all empty lines + * are skipped over for coverage purposes. + * + * @see https://github.com/sebastianbergmann/php-code-coverage/issues/799 + */ + private function skipEmptyLines(): void + { + foreach ($this->lineCoverage as $filename => $coverage) { + foreach ($this->getEmptyLinesForFile($filename) as $emptyLine) { + unset($this->lineCoverage[$filename][$emptyLine]); + } + } + } + + /** + * @return array + */ + private function getEmptyLinesForFile(string $filename): array + { + if (!isset(self::$emptyLineCache[$filename])) { + self::$emptyLineCache[$filename] = []; + + if (is_file($filename)) { + $sourceLines = explode("\n", file_get_contents($filename)); + + foreach ($sourceLines as $line => $source) { + if (trim($source) === '') { + self::$emptyLineCache[$filename][] = ($line + 1); + } + } + } + } + + return self::$emptyLineCache[$filename]; + } +} diff --git a/src/Driver/Driver.php b/src/Driver/Driver.php new file mode 100644 index 000000000..b839cca53 --- /dev/null +++ b/src/Driver/Driver.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use function sprintf; +use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException; +use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +abstract class Driver +{ + /** + * @see http://xdebug.org/docs/code_coverage + */ + public const int LINE_NOT_EXECUTABLE = -2; + + /** + * @see http://xdebug.org/docs/code_coverage + */ + public const int LINE_NOT_EXECUTED = -1; + + /** + * @see http://xdebug.org/docs/code_coverage + */ + public const int LINE_EXECUTED = 1; + + /** + * @see http://xdebug.org/docs/code_coverage + */ + public const int BRANCH_NOT_HIT = 0; + + /** + * @see http://xdebug.org/docs/code_coverage + */ + public const int BRANCH_HIT = 1; + private bool $collectBranchAndPathCoverage = false; + + public function canCollectBranchAndPathCoverage(): bool + { + return false; + } + + public function collectsBranchAndPathCoverage(): bool + { + return $this->collectBranchAndPathCoverage; + } + + /** + * @throws BranchAndPathCoverageNotSupportedException + */ + public function enableBranchAndPathCoverage(): void + { + if (!$this->canCollectBranchAndPathCoverage()) { + throw new BranchAndPathCoverageNotSupportedException( + sprintf( + '%s does not support branch and path coverage', + $this->nameAndVersion(), + ), + ); + } + + $this->collectBranchAndPathCoverage = true; + } + + public function disableBranchAndPathCoverage(): void + { + $this->collectBranchAndPathCoverage = false; + } + + abstract public function nameAndVersion(): string; + + abstract public function start(): void; + + abstract public function stop(): RawCodeCoverageData; +} diff --git a/src/Driver/PcovDriver.php b/src/Driver/PcovDriver.php new file mode 100644 index 000000000..d7975efe3 --- /dev/null +++ b/src/Driver/PcovDriver.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use const pcov\inclusive; +use function array_intersect; +use function extension_loaded; +use function pcov\clear; +use function pcov\collect; +use function pcov\start; +use function pcov\stop; +use function pcov\waiting; +use function phpversion; +use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; +use SebastianBergmann\CodeCoverage\Filter; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class PcovDriver extends Driver +{ + private readonly Filter $filter; + + /** + * @throws PcovNotAvailableException + */ + public function __construct(Filter $filter) + { + $this->ensurePcovIsAvailable(); + + $this->filter = $filter; + } + + /** + * @codeCoverageIgnore + */ + public function start(): void + { + start(); + } + + public function stop(): RawCodeCoverageData + { + stop(); + + // @codeCoverageIgnoreStart + $filesToCollectCoverageFor = waiting(); + $collected = []; + + if ($filesToCollectCoverageFor !== []) { + if (!$this->filter->isEmpty()) { + $filesToCollectCoverageFor = array_intersect($filesToCollectCoverageFor, $this->filter->files()); + } + + $collected = collect(inclusive, $filesToCollectCoverageFor); + + clear(); + } + + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collected); + // @codeCoverageIgnoreEnd + } + + public function nameAndVersion(): string + { + return 'PCOV ' . phpversion('pcov'); + } + + /** + * @throws PcovNotAvailableException + */ + private function ensurePcovIsAvailable(): void + { + if (!extension_loaded('pcov')) { + throw new PcovNotAvailableException; + } + } +} diff --git a/src/Driver/Selector.php b/src/Driver/Selector.php new file mode 100644 index 000000000..56ffbf80e --- /dev/null +++ b/src/Driver/Selector.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use SebastianBergmann\CodeCoverage\Filter; +use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException; +use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException; +use SebastianBergmann\Environment\Runtime; + +final class Selector +{ + /** + * @throws NoCodeCoverageDriverAvailableException + * @throws PcovNotAvailableException + * @throws XdebugNotAvailableException + * @throws XdebugNotEnabledException + * @throws XdebugVersionNotSupportedException + */ + public function forLineCoverage(Filter $filter): Driver + { + $runtime = new Runtime; + + if ($runtime->hasPCOV()) { + return new PcovDriver($filter); + } + + if ($runtime->hasXdebug()) { + return new XdebugDriver($filter); + } + + throw new NoCodeCoverageDriverAvailableException; + } + + /** + * @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException + * @throws XdebugNotAvailableException + * @throws XdebugNotEnabledException + * @throws XdebugVersionNotSupportedException + */ + public function forLineAndPathCoverage(Filter $filter): Driver + { + if ((new Runtime)->hasXdebug()) { + $driver = new XdebugDriver($filter); + + $driver->enableBranchAndPathCoverage(); + + return $driver; + } + + throw new NoCodeCoverageDriverWithPathCoverageSupportAvailableException; + } +} diff --git a/src/Driver/XdebugDriver.php b/src/Driver/XdebugDriver.php new file mode 100644 index 000000000..039df00d0 --- /dev/null +++ b/src/Driver/XdebugDriver.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use const XDEBUG_CC_BRANCH_CHECK; +use const XDEBUG_CC_DEAD_CODE; +use const XDEBUG_CC_UNUSED; +use const XDEBUG_FILTER_CODE_COVERAGE; +use const XDEBUG_PATH_INCLUDE; +use function extension_loaded; +use function in_array; +use function phpversion; +use function version_compare; +use function xdebug_get_code_coverage; +use function xdebug_info; +use function xdebug_set_filter; +use function xdebug_start_code_coverage; +use function xdebug_stop_code_coverage; +use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; +use SebastianBergmann\CodeCoverage\Filter; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage + * + * @phpstan-type XdebugLinesCoverageType array + * @phpstan-type XdebugBranchCoverageType array{ + * op_start: int, + * op_end: int, + * line_start: int, + * line_end: int, + * hit: int, + * out: array, + * out_hit: array, + * } + * @phpstan-type XdebugPathCoverageType array{ + * path: array, + * hit: int, + * } + * @phpstan-type XdebugFunctionCoverageType array{ + * branches: array, + * paths: array, + * } + * @phpstan-type XdebugFunctionsCoverageType array + * @phpstan-type XdebugPathAndBranchesCoverageType array{ + * lines: XdebugLinesCoverageType, + * functions: XdebugFunctionsCoverageType, + * } + * @phpstan-type XdebugCodeCoverageWithoutPathCoverageType array + * @phpstan-type XdebugCodeCoverageWithPathCoverageType array + */ +final class XdebugDriver extends Driver +{ + /** + * @throws XdebugNotAvailableException + * @throws XdebugNotEnabledException + * @throws XdebugVersionNotSupportedException + */ + public function __construct(Filter $filter) + { + $this->ensureXdebugIsAvailable(); + + if (!$filter->isEmpty()) { + xdebug_set_filter( + XDEBUG_FILTER_CODE_COVERAGE, + XDEBUG_PATH_INCLUDE, + $filter->files(), + ); + } + } + + public function canCollectBranchAndPathCoverage(): bool + { + return true; + } + + public function start(): void + { + $flags = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE; + + if ($this->collectsBranchAndPathCoverage()) { + $flags |= XDEBUG_CC_BRANCH_CHECK; + } + + xdebug_start_code_coverage($flags); + } + + public function stop(): RawCodeCoverageData + { + $data = xdebug_get_code_coverage(); + + xdebug_stop_code_coverage(); + + if ($this->collectsBranchAndPathCoverage()) { + /* @var XdebugCodeCoverageWithPathCoverageType $data */ + return RawCodeCoverageData::fromXdebugWithPathCoverage($data); + } + + /* @var XdebugCodeCoverageWithoutPathCoverageType $data */ + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data); + } + + public function nameAndVersion(): string + { + return 'Xdebug ' . phpversion('xdebug'); + } + + /** + * @throws XdebugNotAvailableException + * @throws XdebugNotEnabledException + * @throws XdebugVersionNotSupportedException + */ + private function ensureXdebugIsAvailable(): void + { + if (!extension_loaded('xdebug')) { + throw new XdebugNotAvailableException; + } + + if (!version_compare(phpversion('xdebug'), '3.1', '>=')) { + throw new XdebugVersionNotSupportedException(phpversion('xdebug')); + } + + if (!in_array('coverage', xdebug_info('mode'), true)) { + throw new XdebugNotEnabledException; + } + } +} diff --git a/src/Exception/BranchAndPathCoverageNotSupportedException.php b/src/Exception/BranchAndPathCoverageNotSupportedException.php new file mode 100644 index 000000000..ab2089197 --- /dev/null +++ b/src/Exception/BranchAndPathCoverageNotSupportedException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class BranchAndPathCoverageNotSupportedException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/DirectoryCouldNotBeCreatedException.php b/src/Exception/DirectoryCouldNotBeCreatedException.php new file mode 100644 index 000000000..fdd9bfdf1 --- /dev/null +++ b/src/Exception/DirectoryCouldNotBeCreatedException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Util; + +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class DirectoryCouldNotBeCreatedException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php new file mode 100644 index 000000000..28dc48b8a --- /dev/null +++ b/src/Exception/Exception.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use Throwable; + +interface Exception extends Throwable +{ +} diff --git a/src/Exception/FileCouldNotBeWrittenException.php b/src/Exception/FileCouldNotBeWrittenException.php new file mode 100644 index 000000000..db9cdac34 --- /dev/null +++ b/src/Exception/FileCouldNotBeWrittenException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class FileCouldNotBeWrittenException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..17e4b7076 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +final class InvalidArgumentException extends \InvalidArgumentException implements Exception +{ +} diff --git a/src/Exception/InvalidCodeCoverageTargetException.php b/src/Exception/InvalidCodeCoverageTargetException.php new file mode 100644 index 000000000..09139585a --- /dev/null +++ b/src/Exception/InvalidCodeCoverageTargetException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Test\Target; + +use function sprintf; +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class InvalidCodeCoverageTargetException extends RuntimeException implements Exception +{ + public function __construct(Target $target) + { + parent::__construct( + sprintf( + '%s is not a valid target for code coverage', + $target->description(), + ), + ); + } +} diff --git a/src/Exception/NoCodeCoverageDriverAvailableException.php b/src/Exception/NoCodeCoverageDriverAvailableException.php new file mode 100644 index 000000000..b1494e267 --- /dev/null +++ b/src/Exception/NoCodeCoverageDriverAvailableException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class NoCodeCoverageDriverAvailableException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('No code coverage driver available'); + } +} diff --git a/src/Exception/NoCodeCoverageDriverWithPathCoverageSupportAvailableException.php b/src/Exception/NoCodeCoverageDriverWithPathCoverageSupportAvailableException.php new file mode 100644 index 000000000..0065b740d --- /dev/null +++ b/src/Exception/NoCodeCoverageDriverWithPathCoverageSupportAvailableException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class NoCodeCoverageDriverWithPathCoverageSupportAvailableException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('No code coverage driver with path coverage support available'); + } +} diff --git a/src/Exception/ParserException.php b/src/Exception/ParserException.php new file mode 100644 index 000000000..a907e34e8 --- /dev/null +++ b/src/Exception/ParserException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class ParserException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/PathExistsButIsNotDirectoryException.php b/src/Exception/PathExistsButIsNotDirectoryException.php new file mode 100644 index 000000000..fd6f80a70 --- /dev/null +++ b/src/Exception/PathExistsButIsNotDirectoryException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use function sprintf; +use RuntimeException; + +final class PathExistsButIsNotDirectoryException extends RuntimeException implements Exception +{ + public function __construct(string $path) + { + parent::__construct(sprintf('"%s" exists but is not a directory', $path)); + } +} diff --git a/src/Exception/PcovNotAvailableException.php b/src/Exception/PcovNotAvailableException.php new file mode 100644 index 000000000..2f0a66e5a --- /dev/null +++ b/src/Exception/PcovNotAvailableException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class PcovNotAvailableException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('The PCOV extension is not available'); + } +} diff --git a/src/Exception/ReflectionException.php b/src/Exception/ReflectionException.php new file mode 100644 index 000000000..78db430be --- /dev/null +++ b/src/Exception/ReflectionException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class ReflectionException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/ReportAlreadyFinalizedException.php b/src/Exception/ReportAlreadyFinalizedException.php new file mode 100644 index 000000000..0481f1610 --- /dev/null +++ b/src/Exception/ReportAlreadyFinalizedException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class ReportAlreadyFinalizedException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('The code coverage report has already been finalized'); + } +} diff --git a/src/Exception/StaticAnalysisCacheNotConfiguredException.php b/src/Exception/StaticAnalysisCacheNotConfiguredException.php new file mode 100644 index 000000000..fd58fd6b6 --- /dev/null +++ b/src/Exception/StaticAnalysisCacheNotConfiguredException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class StaticAnalysisCacheNotConfiguredException extends RuntimeException implements Exception +{ +} diff --git a/src/Exception/TestIdMissingException.php b/src/Exception/TestIdMissingException.php new file mode 100644 index 000000000..4cc3e0c2b --- /dev/null +++ b/src/Exception/TestIdMissingException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class TestIdMissingException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('Test ID is missing'); + } +} diff --git a/src/Exception/UnintentionallyCoveredCodeException.php b/src/Exception/UnintentionallyCoveredCodeException.php new file mode 100644 index 000000000..bb7d88c97 --- /dev/null +++ b/src/Exception/UnintentionallyCoveredCodeException.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception +{ + /** + * @var list + */ + private readonly array $unintentionallyCoveredUnits; + + /** + * @param list $unintentionallyCoveredUnits + */ + public function __construct(array $unintentionallyCoveredUnits) + { + $this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits; + + parent::__construct($this->toString()); + } + + /** + * @return list + */ + public function getUnintentionallyCoveredUnits(): array + { + return $this->unintentionallyCoveredUnits; + } + + private function toString(): string + { + $message = ''; + + foreach ($this->unintentionallyCoveredUnits as $unit) { + $message .= '- ' . $unit . "\n"; + } + + return $message; + } +} diff --git a/src/Exception/WriteOperationFailedException.php b/src/Exception/WriteOperationFailedException.php new file mode 100644 index 000000000..c6b5e516a --- /dev/null +++ b/src/Exception/WriteOperationFailedException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use function sprintf; +use RuntimeException; + +final class WriteOperationFailedException extends RuntimeException implements Exception +{ + public function __construct(string $path) + { + parent::__construct(sprintf('Cannot write to "%s"', $path)); + } +} diff --git a/src/Exception/XdebugNotAvailableException.php b/src/Exception/XdebugNotAvailableException.php new file mode 100644 index 000000000..1622c5a63 --- /dev/null +++ b/src/Exception/XdebugNotAvailableException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class XdebugNotAvailableException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('The Xdebug extension is not available'); + } +} diff --git a/src/Exception/XdebugNotEnabledException.php b/src/Exception/XdebugNotEnabledException.php new file mode 100644 index 000000000..a8df4645b --- /dev/null +++ b/src/Exception/XdebugNotEnabledException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class XdebugNotEnabledException extends RuntimeException implements Exception +{ + public function __construct() + { + parent::__construct('XDEBUG_MODE=coverage (environment variable) or xdebug.mode=coverage (PHP configuration setting) has to be set'); + } +} diff --git a/src/Exception/XdebugVersionNotSupportedException.php b/src/Exception/XdebugVersionNotSupportedException.php new file mode 100644 index 000000000..c785af145 --- /dev/null +++ b/src/Exception/XdebugVersionNotSupportedException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Driver; + +use function sprintf; +use RuntimeException; +use SebastianBergmann\CodeCoverage\Exception; + +final class XdebugVersionNotSupportedException extends RuntimeException implements Exception +{ + /** + * @param non-empty-string $version + */ + public function __construct(string $version) + { + parent::__construct( + sprintf( + 'Version %s of the Xdebug extension is not supported', + $version, + ), + ); + } +} diff --git a/src/Exception/XmlException.php b/src/Exception/XmlException.php new file mode 100644 index 000000000..31e4623df --- /dev/null +++ b/src/Exception/XmlException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use RuntimeException; + +final class XmlException extends RuntimeException implements Exception +{ +} diff --git a/src/Filter.php b/src/Filter.php new file mode 100644 index 000000000..f9086542b --- /dev/null +++ b/src/Filter.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use function array_keys; +use function is_file; +use function realpath; +use function str_contains; +use function str_starts_with; + +final class Filter +{ + /** + * @var array + */ + private array $files = []; + + /** + * @var array + */ + private array $isFileCache = []; + + /** + * @param list $filenames + */ + public function includeFiles(array $filenames): void + { + foreach ($filenames as $filename) { + $this->includeFile($filename); + } + } + + public function includeFile(string $filename): void + { + $filename = realpath($filename); + + if (!$filename) { + return; + } + + $this->files[$filename] = true; + } + + public function isFile(string $filename): bool + { + if (isset($this->isFileCache[$filename])) { + return $this->isFileCache[$filename]; + } + + if ($filename === '-' || + str_starts_with($filename, 'vfs://') || + str_contains($filename, 'xdebug://debug-eval') || + str_contains($filename, 'eval()\'d code') || + str_contains($filename, 'runtime-created function') || + str_contains($filename, 'runkit created function') || + str_contains($filename, 'assert code') || + str_contains($filename, 'regexp code') || + str_contains($filename, 'Standard input code')) { + $isFile = false; + } else { + $isFile = is_file($filename); + } + + $this->isFileCache[$filename] = $isFile; + + return $isFile; + } + + public function isExcluded(string $filename): bool + { + return !isset($this->files[$filename]) || !$this->isFile($filename); + } + + /** + * @return list + */ + public function files(): array + { + return array_keys($this->files); + } + + public function isEmpty(): bool + { + return $this->files === []; + } +} diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php new file mode 100644 index 000000000..7e82a3daf --- /dev/null +++ b/src/Node/AbstractNode.php @@ -0,0 +1,275 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use const DIRECTORY_SEPARATOR; +use function array_merge; +use function str_ends_with; +use function str_replace; +use function substr; +use Countable; +use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode; +use SebastianBergmann\CodeCoverage\Util\Percentage; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @phpstan-import-type ProcessedFunctionType from File + * @phpstan-import-type ProcessedClassType from File + * @phpstan-import-type ProcessedTraitType from File + */ +abstract class AbstractNode implements Countable +{ + private readonly string $name; + private string $pathAsString; + + /** + * @var non-empty-list + */ + private array $pathAsArray; + private readonly ?AbstractNode $parent; + private string $id; + + public function __construct(string $name, ?self $parent = null) + { + if (str_ends_with($name, DIRECTORY_SEPARATOR)) { + $name = substr($name, 0, -1); + } + + $this->name = $name; + $this->parent = $parent; + + $this->processId(); + $this->processPath(); + } + + public function name(): string + { + return $this->name; + } + + public function id(): string + { + return $this->id; + } + + public function pathAsString(): string + { + return $this->pathAsString; + } + + /** + * @return non-empty-list + */ + public function pathAsArray(): array + { + return $this->pathAsArray; + } + + public function parent(): ?self + { + return $this->parent; + } + + public function percentageOfTestedClasses(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedClasses(), + $this->numberOfClasses(), + ); + } + + public function percentageOfTestedTraits(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedTraits(), + $this->numberOfTraits(), + ); + } + + public function percentageOfTestedClassesAndTraits(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedClassesAndTraits(), + $this->numberOfClassesAndTraits(), + ); + } + + public function percentageOfTestedFunctions(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedFunctions(), + $this->numberOfFunctions(), + ); + } + + public function percentageOfTestedMethods(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedMethods(), + $this->numberOfMethods(), + ); + } + + public function percentageOfTestedFunctionsAndMethods(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfTestedFunctionsAndMethods(), + $this->numberOfFunctionsAndMethods(), + ); + } + + public function percentageOfExecutedLines(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfExecutedLines(), + $this->numberOfExecutableLines(), + ); + } + + public function percentageOfExecutedBranches(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfExecutedBranches(), + $this->numberOfExecutableBranches(), + ); + } + + public function percentageOfExecutedPaths(): Percentage + { + return Percentage::fromFractionAndTotal( + $this->numberOfExecutedPaths(), + $this->numberOfExecutablePaths(), + ); + } + + public function numberOfClassesAndTraits(): int + { + return $this->numberOfClasses() + $this->numberOfTraits(); + } + + public function numberOfTestedClassesAndTraits(): int + { + return $this->numberOfTestedClasses() + $this->numberOfTestedTraits(); + } + + /** + * @return array + */ + public function classesAndTraits(): array + { + return array_merge($this->classes(), $this->traits()); + } + + public function numberOfFunctionsAndMethods(): int + { + return $this->numberOfFunctions() + $this->numberOfMethods(); + } + + public function numberOfTestedFunctionsAndMethods(): int + { + return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods(); + } + + /** + * @return non-negative-int + */ + public function cyclomaticComplexity(): int + { + $ccn = 0; + + foreach ($this->classesAndTraits() as $classLike) { + $ccn += $classLike['ccn']; + } + + foreach ($this->functions() as $function) { + $ccn += $function['ccn']; + } + + return $ccn; + } + + /** + * @return array + */ + abstract public function classes(): array; + + /** + * @return array + */ + abstract public function traits(): array; + + /** + * @return array + */ + abstract public function functions(): array; + + abstract public function linesOfCode(): LinesOfCode; + + abstract public function numberOfExecutableLines(): int; + + abstract public function numberOfExecutedLines(): int; + + abstract public function numberOfExecutableBranches(): int; + + abstract public function numberOfExecutedBranches(): int; + + abstract public function numberOfExecutablePaths(): int; + + abstract public function numberOfExecutedPaths(): int; + + abstract public function numberOfClasses(): int; + + abstract public function numberOfTestedClasses(): int; + + abstract public function numberOfTraits(): int; + + abstract public function numberOfTestedTraits(): int; + + abstract public function numberOfMethods(): int; + + abstract public function numberOfTestedMethods(): int; + + abstract public function numberOfFunctions(): int; + + abstract public function numberOfTestedFunctions(): int; + + private function processId(): void + { + if ($this->parent === null) { + $this->id = 'index'; + + return; + } + + $parentId = $this->parent->id(); + + if ($parentId === 'index') { + $this->id = str_replace(':', '_', $this->name); + } else { + $this->id = $parentId . '/' . $this->name; + } + } + + private function processPath(): void + { + if ($this->parent === null) { + $this->pathAsArray = [$this]; + $this->pathAsString = $this->name; + + return; + } + + $this->pathAsArray = $this->parent->pathAsArray(); + $this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name; + + $this->pathAsArray[] = $this; + } +} diff --git a/src/Node/Builder.php b/src/Node/Builder.php new file mode 100644 index 000000000..19fc3a24d --- /dev/null +++ b/src/Node/Builder.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use const DIRECTORY_SEPARATOR; +use function array_shift; +use function basename; +use function count; +use function dirname; +use function explode; +use function implode; +use function is_file; +use function str_ends_with; +use function str_replace; +use function str_starts_with; +use function substr; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData; +use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @phpstan-import-type TestType from CodeCoverage + */ +final readonly class Builder +{ + private FileAnalyser $analyser; + + public function __construct(FileAnalyser $analyser) + { + $this->analyser = $analyser; + } + + public function build(CodeCoverage $coverage): Directory + { + $data = clone $coverage->getData(); // clone because path munging is destructive to the original data + $commonPath = $this->reducePaths($data); + $root = new Directory( + $commonPath, + null, + ); + + $this->addItems( + $root, + $this->buildDirectoryStructure($data), + $coverage->getTests(), + ); + + return $root; + } + + /** + * @param array $tests + */ + private function addItems(Directory $root, array $items, array $tests): void + { + foreach ($items as $key => $value) { + $key = (string) $key; + + if (str_ends_with($key, '/f')) { + $key = substr($key, 0, -2); + $filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key; + + if (is_file($filename)) { + $analysisResult = $this->analyser->analyse($filename); + + $root->addFile( + new File( + $key, + $root, + $value['lineCoverage'], + $value['functionCoverage'], + $tests, + $analysisResult->classes(), + $analysisResult->traits(), + $analysisResult->functions(), + $analysisResult->linesOfCode(), + ), + ); + } + } else { + $child = $root->addDirectory($key); + + $this->addItems($child, $value, $tests); + } + } + } + + /** + * Builds an array representation of the directory structure. + * + * For instance, + * + * + * Array + * ( + * [Money.php] => Array + * ( + * ... + * ) + * + * [MoneyBag.php] => Array + * ( + * ... + * ) + * ) + * + * + * is transformed into + * + * + * Array + * ( + * [.] => Array + * ( + * [Money.php] => Array + * ( + * ... + * ) + * + * [MoneyBag.php] => Array + * ( + * ... + * ) + * ) + * ) + * + * + * @return array, functionCoverage: array>}>> + */ + private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array + { + $result = []; + + foreach ($data->coveredFiles() as $originalPath) { + $path = explode(DIRECTORY_SEPARATOR, $originalPath); + $pointer = &$result; + $max = count($path); + + for ($i = 0; $i < $max; $i++) { + $type = ''; + + if ($i === ($max - 1)) { + $type = '/f'; + } + + $pointer = &$pointer[$path[$i] . $type]; + } + + $pointer = [ + 'lineCoverage' => $data->lineCoverage()[$originalPath] ?? [], + 'functionCoverage' => $data->functionCoverage()[$originalPath] ?? [], + ]; + } + + return $result; + } + + /** + * Reduces the paths by cutting the longest common start path. + * + * For instance, + * + * + * Array + * ( + * [/home/sb/Money/Money.php] => Array + * ( + * ... + * ) + * + * [/home/sb/Money/MoneyBag.php] => Array + * ( + * ... + * ) + * ) + * + * + * is reduced to + * + * + * Array + * ( + * [Money.php] => Array + * ( + * ... + * ) + * + * [MoneyBag.php] => Array + * ( + * ... + * ) + * ) + * + */ + private function reducePaths(ProcessedCodeCoverageData $coverage): string + { + if ($coverage->coveredFiles() === []) { + return '.'; + } + + $commonPath = ''; + $paths = $coverage->coveredFiles(); + + if (count($paths) === 1) { + $commonPath = dirname($paths[0]) . DIRECTORY_SEPARATOR; + $coverage->renameFile($paths[0], basename($paths[0])); + + return $commonPath; + } + + $max = count($paths); + + for ($i = 0; $i < $max; $i++) { + // strip phar:// prefixes + if (str_starts_with($paths[$i], 'phar://')) { + $paths[$i] = substr($paths[$i], 7); + $paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]); + } + + $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]); + + if ($paths[$i][0] === '') { + $paths[$i][0] = DIRECTORY_SEPARATOR; + } + } + + $done = false; + $max = count($paths); + + while (!$done) { + for ($i = 0; $i < $max - 1; $i++) { + if (!isset($paths[$i][0]) || + !isset($paths[$i + 1][0]) || + $paths[$i][0] !== $paths[$i + 1][0]) { + $done = true; + + break; + } + } + + if (!$done) { + $commonPath .= $paths[0][0]; + + if ($paths[0][0] !== DIRECTORY_SEPARATOR) { + $commonPath .= DIRECTORY_SEPARATOR; + } + + for ($i = 0; $i < $max; $i++) { + array_shift($paths[$i]); + } + } + } + + $original = $coverage->coveredFiles(); + $max = count($original); + + for ($i = 0; $i < $max; $i++) { + $coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i])); + } + + return substr($commonPath, 0, -1); + } +} diff --git a/src/Node/CrapIndex.php b/src/Node/CrapIndex.php new file mode 100644 index 000000000..a07a55048 --- /dev/null +++ b/src/Node/CrapIndex.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use function sprintf; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final readonly class CrapIndex +{ + private int $cyclomaticComplexity; + private float $codeCoverage; + + public function __construct(int $cyclomaticComplexity, float $codeCoverage) + { + $this->cyclomaticComplexity = $cyclomaticComplexity; + $this->codeCoverage = $codeCoverage; + } + + public function asString(): string + { + if ($this->codeCoverage === 0.0) { + return (string) ($this->cyclomaticComplexity ** 2 + $this->cyclomaticComplexity); + } + + if ($this->codeCoverage >= 95) { + return (string) $this->cyclomaticComplexity; + } + + return sprintf( + '%01.2F', + $this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity, + ); + } +} diff --git a/src/Node/Directory.php b/src/Node/Directory.php new file mode 100644 index 000000000..2802f93ab --- /dev/null +++ b/src/Node/Directory.php @@ -0,0 +1,404 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use function array_merge; +use function assert; +use function count; +use IteratorAggregate; +use RecursiveIteratorIterator; +use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode; + +/** + * @template-implements IteratorAggregate + * + * @phpstan-import-type ProcessedFunctionType from File + * @phpstan-import-type ProcessedClassType from File + * @phpstan-import-type ProcessedTraitType from File + * + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class Directory extends AbstractNode implements IteratorAggregate +{ + /** + * @var list + */ + private array $children = []; + + /** + * @var list + */ + private array $directories = []; + + /** + * @var list + */ + private array $files = []; + + /** + * @var ?array + */ + private ?array $classes = null; + + /** + * @var ?array + */ + private ?array $traits = null; + + /** + * @var ?array + */ + private ?array $functions = null; + private ?LinesOfCode $linesOfCode = null; + private int $numFiles = -1; + private int $numExecutableLines = -1; + private int $numExecutedLines = -1; + private int $numExecutableBranches = -1; + private int $numExecutedBranches = -1; + private int $numExecutablePaths = -1; + private int $numExecutedPaths = -1; + private int $numClasses = -1; + private int $numTestedClasses = -1; + private int $numTraits = -1; + private int $numTestedTraits = -1; + private int $numMethods = -1; + private int $numTestedMethods = -1; + private int $numFunctions = -1; + private int $numTestedFunctions = -1; + + public function count(): int + { + if ($this->numFiles === -1) { + $this->numFiles = 0; + + foreach ($this->children as $child) { + $this->numFiles += count($child); + } + } + + return $this->numFiles; + } + + /** + * @return RecursiveIteratorIterator> + */ + public function getIterator(): RecursiveIteratorIterator + { + return new RecursiveIteratorIterator( + new Iterator($this), + RecursiveIteratorIterator::SELF_FIRST, + ); + } + + public function addDirectory(string $name): self + { + $directory = new self($name, $this); + + assert($directory instanceof self); + + $this->children[] = $directory; + $this->directories[] = &$this->children[count($this->children) - 1]; + + return $directory; + } + + public function addFile(File $file): void + { + $this->children[] = $file; + $this->files[] = &$this->children[count($this->children) - 1]; + + $this->numExecutableLines = -1; + $this->numExecutedLines = -1; + } + + /** + * @return list + */ + public function directories(): array + { + return $this->directories; + } + + /** + * @return list + */ + public function files(): array + { + return $this->files; + } + + /** + * @return list + */ + public function children(): array + { + return $this->children; + } + + /** + * @return array + */ + public function classes(): array + { + if ($this->classes === null) { + $this->classes = []; + + foreach ($this->children as $child) { + $this->classes = array_merge( + $this->classes, + $child->classes(), + ); + } + } + + return $this->classes; + } + + /** + * @return array + */ + public function traits(): array + { + if ($this->traits === null) { + $this->traits = []; + + foreach ($this->children as $child) { + $this->traits = array_merge( + $this->traits, + $child->traits(), + ); + } + } + + return $this->traits; + } + + /** + * @return array + */ + public function functions(): array + { + if ($this->functions === null) { + $this->functions = []; + + foreach ($this->children as $child) { + $this->functions = array_merge( + $this->functions, + $child->functions(), + ); + } + } + + return $this->functions; + } + + public function linesOfCode(): LinesOfCode + { + if ($this->linesOfCode === null) { + $linesOfCode = 0; + $commentLinesOfCode = 0; + $nonCommentLinesOfCode = 0; + + foreach ($this->children as $child) { + $childLinesOfCode = $child->linesOfCode(); + + $linesOfCode += $childLinesOfCode->linesOfCode(); + $commentLinesOfCode += $childLinesOfCode->commentLinesOfCode(); + $nonCommentLinesOfCode += $childLinesOfCode->nonCommentLinesOfCode(); + } + + $this->linesOfCode = new LinesOfCode($linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode); + } + + return $this->linesOfCode; + } + + public function numberOfExecutableLines(): int + { + if ($this->numExecutableLines === -1) { + $this->numExecutableLines = 0; + + foreach ($this->children as $child) { + $this->numExecutableLines += $child->numberOfExecutableLines(); + } + } + + return $this->numExecutableLines; + } + + public function numberOfExecutedLines(): int + { + if ($this->numExecutedLines === -1) { + $this->numExecutedLines = 0; + + foreach ($this->children as $child) { + $this->numExecutedLines += $child->numberOfExecutedLines(); + } + } + + return $this->numExecutedLines; + } + + public function numberOfExecutableBranches(): int + { + if ($this->numExecutableBranches === -1) { + $this->numExecutableBranches = 0; + + foreach ($this->children as $child) { + $this->numExecutableBranches += $child->numberOfExecutableBranches(); + } + } + + return $this->numExecutableBranches; + } + + public function numberOfExecutedBranches(): int + { + if ($this->numExecutedBranches === -1) { + $this->numExecutedBranches = 0; + + foreach ($this->children as $child) { + $this->numExecutedBranches += $child->numberOfExecutedBranches(); + } + } + + return $this->numExecutedBranches; + } + + public function numberOfExecutablePaths(): int + { + if ($this->numExecutablePaths === -1) { + $this->numExecutablePaths = 0; + + foreach ($this->children as $child) { + $this->numExecutablePaths += $child->numberOfExecutablePaths(); + } + } + + return $this->numExecutablePaths; + } + + public function numberOfExecutedPaths(): int + { + if ($this->numExecutedPaths === -1) { + $this->numExecutedPaths = 0; + + foreach ($this->children as $child) { + $this->numExecutedPaths += $child->numberOfExecutedPaths(); + } + } + + return $this->numExecutedPaths; + } + + public function numberOfClasses(): int + { + if ($this->numClasses === -1) { + $this->numClasses = 0; + + foreach ($this->children as $child) { + $this->numClasses += $child->numberOfClasses(); + } + } + + return $this->numClasses; + } + + public function numberOfTestedClasses(): int + { + if ($this->numTestedClasses === -1) { + $this->numTestedClasses = 0; + + foreach ($this->children as $child) { + $this->numTestedClasses += $child->numberOfTestedClasses(); + } + } + + return $this->numTestedClasses; + } + + public function numberOfTraits(): int + { + if ($this->numTraits === -1) { + $this->numTraits = 0; + + foreach ($this->children as $child) { + $this->numTraits += $child->numberOfTraits(); + } + } + + return $this->numTraits; + } + + public function numberOfTestedTraits(): int + { + if ($this->numTestedTraits === -1) { + $this->numTestedTraits = 0; + + foreach ($this->children as $child) { + $this->numTestedTraits += $child->numberOfTestedTraits(); + } + } + + return $this->numTestedTraits; + } + + public function numberOfMethods(): int + { + if ($this->numMethods === -1) { + $this->numMethods = 0; + + foreach ($this->children as $child) { + $this->numMethods += $child->numberOfMethods(); + } + } + + return $this->numMethods; + } + + public function numberOfTestedMethods(): int + { + if ($this->numTestedMethods === -1) { + $this->numTestedMethods = 0; + + foreach ($this->children as $child) { + $this->numTestedMethods += $child->numberOfTestedMethods(); + } + } + + return $this->numTestedMethods; + } + + public function numberOfFunctions(): int + { + if ($this->numFunctions === -1) { + $this->numFunctions = 0; + + foreach ($this->children as $child) { + $this->numFunctions += $child->numberOfFunctions(); + } + } + + return $this->numFunctions; + } + + public function numberOfTestedFunctions(): int + { + if ($this->numTestedFunctions === -1) { + $this->numTestedFunctions = 0; + + foreach ($this->children as $child) { + $this->numTestedFunctions += $child->numberOfTestedFunctions(); + } + } + + return $this->numTestedFunctions; + } +} diff --git a/src/Node/File.php b/src/Node/File.php new file mode 100644 index 000000000..54ee70b4a --- /dev/null +++ b/src/Node/File.php @@ -0,0 +1,701 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use function array_filter; +use function count; +use function range; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\StaticAnalysis\AnalysisResult; +use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_; +use SebastianBergmann\CodeCoverage\StaticAnalysis\Function_; +use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode; +use SebastianBergmann\CodeCoverage\StaticAnalysis\Method; +use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + * + * @phpstan-import-type TestType from CodeCoverage + * @phpstan-import-type LinesType from AnalysisResult + * + * @phpstan-type ProcessedFunctionType array{ + * functionName: string, + * namespace: string, + * signature: string, + * startLine: int, + * endLine: int, + * executableLines: int, + * executedLines: int, + * executableBranches: int, + * executedBranches: int, + * executablePaths: int, + * executedPaths: int, + * ccn: int, + * coverage: int|float, + * crap: int|string, + * link: string + * } + * @phpstan-type ProcessedMethodType array{ + * methodName: string, + * visibility: string, + * signature: string, + * startLine: int, + * endLine: int, + * executableLines: int, + * executedLines: int, + * executableBranches: int, + * executedBranches: int, + * executablePaths: int, + * executedPaths: int, + * ccn: int, + * coverage: float|int, + * crap: int|string, + * link: string + * } + * @phpstan-type ProcessedClassType array{ + * className: string, + * namespace: string, + * methods: array, + * startLine: int, + * executableLines: int, + * executedLines: int, + * executableBranches: int, + * executedBranches: int, + * executablePaths: int, + * executedPaths: int, + * ccn: int, + * coverage: int|float, + * crap: int|string, + * link: string + * } + * @phpstan-type ProcessedTraitType array{ + * traitName: string, + * namespace: string, + * methods: array, + * startLine: int, + * executableLines: int, + * executedLines: int, + * executableBranches: int, + * executedBranches: int, + * executablePaths: int, + * executedPaths: int, + * ccn: int, + * coverage: float|int, + * crap: int|string, + * link: string + * } + */ +final class File extends AbstractNode +{ + /** + * @var array> + */ + private array $lineCoverageData; + private array $functionCoverageData; + + /** + * @var array + */ + private readonly array $testData; + private int $numExecutableLines = 0; + private int $numExecutedLines = 0; + private int $numExecutableBranches = 0; + private int $numExecutedBranches = 0; + private int $numExecutablePaths = 0; + private int $numExecutedPaths = 0; + + /** + * @var array + */ + private array $classes = []; + + /** + * @var array + */ + private array $traits = []; + + /** + * @var array + */ + private array $functions = []; + private readonly LinesOfCode $linesOfCode; + private ?int $numClasses = null; + private int $numTestedClasses = 0; + private ?int $numTraits = null; + private int $numTestedTraits = 0; + private ?int $numMethods = null; + private ?int $numTestedMethods = null; + private ?int $numTestedFunctions = null; + + /** + * @var array + */ + private array $codeUnitsByLine = []; + + /** + * @param array> $lineCoverageData + * @param array $testData + * @param array $classes + * @param array $traits + * @param array $functions + */ + public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode) + { + parent::__construct($name, $parent); + + $this->lineCoverageData = $lineCoverageData; + $this->functionCoverageData = $functionCoverageData; + $this->testData = $testData; + $this->linesOfCode = $linesOfCode; + + $this->calculateStatistics($classes, $traits, $functions); + } + + public function count(): int + { + return 1; + } + + /** + * @return array> + */ + public function lineCoverageData(): array + { + return $this->lineCoverageData; + } + + public function functionCoverageData(): array + { + return $this->functionCoverageData; + } + + /** + * @return array + */ + public function testData(): array + { + return $this->testData; + } + + /** + * @return array + */ + public function classes(): array + { + return $this->classes; + } + + /** + * @return array + */ + public function traits(): array + { + return $this->traits; + } + + /** + * @return array + */ + public function functions(): array + { + return $this->functions; + } + + public function linesOfCode(): LinesOfCode + { + return $this->linesOfCode; + } + + public function numberOfExecutableLines(): int + { + return $this->numExecutableLines; + } + + public function numberOfExecutedLines(): int + { + return $this->numExecutedLines; + } + + public function numberOfExecutableBranches(): int + { + return $this->numExecutableBranches; + } + + public function numberOfExecutedBranches(): int + { + return $this->numExecutedBranches; + } + + public function numberOfExecutablePaths(): int + { + return $this->numExecutablePaths; + } + + public function numberOfExecutedPaths(): int + { + return $this->numExecutedPaths; + } + + public function numberOfClasses(): int + { + if ($this->numClasses === null) { + $this->numClasses = 0; + + foreach ($this->classes as $class) { + foreach ($class['methods'] as $method) { + if ($method['executableLines'] > 0) { + $this->numClasses++; + + continue 2; + } + } + } + } + + return $this->numClasses; + } + + public function numberOfTestedClasses(): int + { + return $this->numTestedClasses; + } + + public function numberOfTraits(): int + { + if ($this->numTraits === null) { + $this->numTraits = 0; + + foreach ($this->traits as $trait) { + foreach ($trait['methods'] as $method) { + if ($method['executableLines'] > 0) { + $this->numTraits++; + + continue 2; + } + } + } + } + + return $this->numTraits; + } + + public function numberOfTestedTraits(): int + { + return $this->numTestedTraits; + } + + public function numberOfMethods(): int + { + if ($this->numMethods === null) { + $this->numMethods = 0; + + foreach ($this->classes as $class) { + foreach ($class['methods'] as $method) { + if ($method['executableLines'] > 0) { + $this->numMethods++; + } + } + } + + foreach ($this->traits as $trait) { + foreach ($trait['methods'] as $method) { + if ($method['executableLines'] > 0) { + $this->numMethods++; + } + } + } + } + + return $this->numMethods; + } + + public function numberOfTestedMethods(): int + { + if ($this->numTestedMethods === null) { + $this->numTestedMethods = 0; + + foreach ($this->classes as $class) { + foreach ($class['methods'] as $method) { + if ($method['executableLines'] > 0 && + $method['coverage'] === 100) { + $this->numTestedMethods++; + } + } + } + + foreach ($this->traits as $trait) { + foreach ($trait['methods'] as $method) { + if ($method['executableLines'] > 0 && + $method['coverage'] === 100) { + $this->numTestedMethods++; + } + } + } + } + + return $this->numTestedMethods; + } + + public function numberOfFunctions(): int + { + return count($this->functions); + } + + public function numberOfTestedFunctions(): int + { + if ($this->numTestedFunctions === null) { + $this->numTestedFunctions = 0; + + foreach ($this->functions as $function) { + if ($function['executableLines'] > 0 && + $function['coverage'] === 100) { + $this->numTestedFunctions++; + } + } + } + + return $this->numTestedFunctions; + } + + /** + * @param array $classes + * @param array $traits + * @param array $functions + */ + private function calculateStatistics(array $classes, array $traits, array $functions): void + { + foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) { + $this->codeUnitsByLine[$lineNumber] = []; + } + + $this->processClasses($classes); + $this->processTraits($traits); + $this->processFunctions($functions); + + foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) { + if (isset($this->lineCoverageData[$lineNumber])) { + foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { + $codeUnit['executableLines']++; + } + + unset($codeUnit); + + $this->numExecutableLines++; + + if (count($this->lineCoverageData[$lineNumber]) > 0) { + foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { + $codeUnit['executedLines']++; + } + + unset($codeUnit); + + $this->numExecutedLines++; + } + } + } + + foreach ($this->traits as &$trait) { + foreach ($trait['methods'] as &$method) { + $methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100; + $methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0; + $methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0; + + $method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage; + $method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString(); + + $trait['ccn'] += $method['ccn']; + } + + unset($method); + + $traitLineCoverage = $trait['executableLines'] > 0 ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100; + $traitBranchCoverage = $trait['executableBranches'] > 0 ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0; + $traitPathCoverage = $trait['executablePaths'] > 0 ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0; + + $trait['coverage'] = $traitBranchCoverage > 0 ? $traitBranchCoverage : $traitLineCoverage; + $trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage > 0 ? $traitPathCoverage : $traitLineCoverage))->asString(); + + if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) { + $this->numTestedClasses++; + } + } + + unset($trait); + + foreach ($this->classes as &$class) { + foreach ($class['methods'] as &$method) { + $methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100; + $methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0; + $methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0; + + $method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage; + $method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString(); + + $class['ccn'] += $method['ccn']; + } + + unset($method); + + $classLineCoverage = $class['executableLines'] > 0 ? ($class['executedLines'] / $class['executableLines']) * 100 : 100; + $classBranchCoverage = $class['executableBranches'] > 0 ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0; + $classPathCoverage = $class['executablePaths'] > 0 ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0; + + $class['coverage'] = $classBranchCoverage > 0 ? $classBranchCoverage : $classLineCoverage; + $class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage > 0 ? $classPathCoverage : $classLineCoverage))->asString(); + + if ($class['executableLines'] > 0 && $class['coverage'] === 100) { + $this->numTestedClasses++; + } + } + + unset($class); + + foreach ($this->functions as &$function) { + $functionLineCoverage = $function['executableLines'] > 0 ? ($function['executedLines'] / $function['executableLines']) * 100 : 100; + $functionBranchCoverage = $function['executableBranches'] > 0 ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0; + $functionPathCoverage = $function['executablePaths'] > 0 ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0; + + $function['coverage'] = $functionBranchCoverage > 0 ? $functionBranchCoverage : $functionLineCoverage; + $function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage > 0 ? $functionPathCoverage : $functionLineCoverage))->asString(); + + if ($function['coverage'] === 100) { + $this->numTestedFunctions++; + } + } + } + + /** + * @param array $classes + */ + private function processClasses(array $classes): void + { + $link = $this->id() . '.html#'; + + foreach ($classes as $className => $class) { + $this->classes[$className] = [ + 'className' => $className, + 'namespace' => $class->namespace(), + 'methods' => [], + 'startLine' => $class->startLine(), + 'executableLines' => 0, + 'executedLines' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'ccn' => 0, + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $class->startLine(), + ]; + + foreach ($class->methods() as $methodName => $method) { + $methodData = $this->newMethod($className, $method, $link); + $this->classes[$className]['methods'][$methodName] = $methodData; + + $this->classes[$className]['executableBranches'] += $methodData['executableBranches']; + $this->classes[$className]['executedBranches'] += $methodData['executedBranches']; + $this->classes[$className]['executablePaths'] += $methodData['executablePaths']; + $this->classes[$className]['executedPaths'] += $methodData['executedPaths']; + + $this->numExecutableBranches += $methodData['executableBranches']; + $this->numExecutedBranches += $methodData['executedBranches']; + $this->numExecutablePaths += $methodData['executablePaths']; + $this->numExecutedPaths += $methodData['executedPaths']; + + foreach (range($method->startLine(), $method->endLine()) as $lineNumber) { + $this->codeUnitsByLine[$lineNumber] = [ + &$this->classes[$className], + &$this->classes[$className]['methods'][$methodName], + ]; + } + } + } + } + + /** + * @param array $traits + */ + private function processTraits(array $traits): void + { + $link = $this->id() . '.html#'; + + foreach ($traits as $traitName => $trait) { + $this->traits[$traitName] = [ + 'traitName' => $traitName, + 'namespace' => $trait->namespace(), + 'methods' => [], + 'startLine' => $trait->startLine(), + 'executableLines' => 0, + 'executedLines' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'ccn' => 0, + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $trait->startLine(), + ]; + + foreach ($trait->methods() as $methodName => $method) { + $methodData = $this->newMethod($traitName, $method, $link); + $this->traits[$traitName]['methods'][$methodName] = $methodData; + + $this->traits[$traitName]['executableBranches'] += $methodData['executableBranches']; + $this->traits[$traitName]['executedBranches'] += $methodData['executedBranches']; + $this->traits[$traitName]['executablePaths'] += $methodData['executablePaths']; + $this->traits[$traitName]['executedPaths'] += $methodData['executedPaths']; + + $this->numExecutableBranches += $methodData['executableBranches']; + $this->numExecutedBranches += $methodData['executedBranches']; + $this->numExecutablePaths += $methodData['executablePaths']; + $this->numExecutedPaths += $methodData['executedPaths']; + + foreach (range($method->startLine(), $method->endLine()) as $lineNumber) { + $this->codeUnitsByLine[$lineNumber] = [ + &$this->traits[$traitName], + &$this->traits[$traitName]['methods'][$methodName], + ]; + } + } + } + } + + /** + * @param array $functions + */ + private function processFunctions(array $functions): void + { + $link = $this->id() . '.html#'; + + foreach ($functions as $functionName => $function) { + $this->functions[$functionName] = [ + 'functionName' => $functionName, + 'namespace' => $function->namespace(), + 'signature' => $function->signature(), + 'startLine' => $function->startLine(), + 'endLine' => $function->endLine(), + 'executableLines' => 0, + 'executedLines' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'ccn' => $function->cyclomaticComplexity(), + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $function->startLine(), + ]; + + foreach (range($function->startLine(), $function->endLine()) as $lineNumber) { + $this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]]; + } + + if (isset($this->functionCoverageData[$functionName]['branches'])) { + $this->functions[$functionName]['executableBranches'] = count( + $this->functionCoverageData[$functionName]['branches'], + ); + + $this->functions[$functionName]['executedBranches'] = count( + array_filter( + $this->functionCoverageData[$functionName]['branches'], + static function (array $branch) + { + return (bool) $branch['hit']; + }, + ), + ); + } + + if (isset($this->functionCoverageData[$functionName]['paths'])) { + $this->functions[$functionName]['executablePaths'] = count( + $this->functionCoverageData[$functionName]['paths'], + ); + + $this->functions[$functionName]['executedPaths'] = count( + array_filter( + $this->functionCoverageData[$functionName]['paths'], + static function (array $path) + { + return (bool) $path['hit']; + }, + ), + ); + } + + $this->numExecutableBranches += $this->functions[$functionName]['executableBranches']; + $this->numExecutedBranches += $this->functions[$functionName]['executedBranches']; + $this->numExecutablePaths += $this->functions[$functionName]['executablePaths']; + $this->numExecutedPaths += $this->functions[$functionName]['executedPaths']; + } + } + + /** + * @return ProcessedMethodType + */ + private function newMethod(string $className, Method $method, string $link): array + { + $methodData = [ + 'methodName' => $method->name(), + 'visibility' => $method->visibility()->value, + 'signature' => $method->signature(), + 'startLine' => $method->startLine(), + 'endLine' => $method->endLine(), + 'executableLines' => 0, + 'executedLines' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'ccn' => $method->cyclomaticComplexity(), + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $method->startLine(), + ]; + + $key = $className . '->' . $method->name(); + + if (isset($this->functionCoverageData[$key]['branches'])) { + $methodData['executableBranches'] = count( + $this->functionCoverageData[$key]['branches'], + ); + + $methodData['executedBranches'] = count( + array_filter( + $this->functionCoverageData[$key]['branches'], + static function (array $branch) + { + return (bool) $branch['hit']; + }, + ), + ); + } + + if (isset($this->functionCoverageData[$key]['paths'])) { + $methodData['executablePaths'] = count( + $this->functionCoverageData[$key]['paths'], + ); + + $methodData['executedPaths'] = count( + array_filter( + $this->functionCoverageData[$key]['paths'], + static function (array $path) + { + return (bool) $path['hit']; + }, + ), + ); + } + + return $methodData; + } +} diff --git a/src/Node/Iterator.php b/src/Node/Iterator.php new file mode 100644 index 000000000..ab3c8eb98 --- /dev/null +++ b/src/Node/Iterator.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Node; + +use function assert; +use function count; +use RecursiveIterator; + +/** + * @template-implements RecursiveIterator + * + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class Iterator implements RecursiveIterator +{ + private int $position; + + /** + * @var list + */ + private readonly array $nodes; + + public function __construct(Directory $node) + { + $this->nodes = $node->children(); + } + + public function rewind(): void + { + $this->position = 0; + } + + public function valid(): bool + { + return $this->position < count($this->nodes); + } + + public function key(): int + { + return $this->position; + } + + public function current(): ?AbstractNode + { + return $this->valid() ? $this->nodes[$this->position] : null; + } + + public function next(): void + { + $this->position++; + } + + public function getChildren(): self + { + assert($this->nodes[$this->position] instanceof Directory); + + return new self($this->nodes[$this->position]); + } + + public function hasChildren(): bool + { + return $this->nodes[$this->position] instanceof Directory; + } +} diff --git a/src/Report/Clover.php b/src/Report/Clover.php new file mode 100644 index 000000000..a5f1c09e6 --- /dev/null +++ b/src/Report/Clover.php @@ -0,0 +1,233 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report; + +use function count; +use function dirname; +use function file_put_contents; +use function is_string; +use function ksort; +use function max; +use function range; +use function str_contains; +use function time; +use DOMDocument; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Node\File; +use SebastianBergmann\CodeCoverage\Util\Filesystem; +use SebastianBergmann\CodeCoverage\WriteOperationFailedException; + +final class Clover +{ + /** + * @throws WriteOperationFailedException + */ + public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string + { + $time = (string) time(); + + $xmlDocument = new DOMDocument('1.0', 'UTF-8'); + $xmlDocument->formatOutput = true; + + $xmlCoverage = $xmlDocument->createElement('coverage'); + $xmlCoverage->setAttribute('generated', $time); + $xmlDocument->appendChild($xmlCoverage); + + $xmlProject = $xmlDocument->createElement('project'); + $xmlProject->setAttribute('timestamp', $time); + + if (is_string($name)) { + $xmlProject->setAttribute('name', $name); + } + + $xmlCoverage->appendChild($xmlProject); + + $packages = []; + $report = $coverage->getReport(); + + foreach ($report as $item) { + if (!$item instanceof File) { + continue; + } + + /* @var File $item */ + + $xmlFile = $xmlDocument->createElement('file'); + $xmlFile->setAttribute('name', $item->pathAsString()); + + $classes = $item->classesAndTraits(); + $coverageData = $item->lineCoverageData(); + $lines = []; + $namespace = 'global'; + + foreach ($classes as $className => $class) { + $classStatements = 0; + $coveredClassStatements = 0; + $coveredMethods = 0; + $classMethods = 0; + + // Assumption: one namespace per file + if ($class['namespace'] !== '') { + $namespace = $class['namespace']; + } + + foreach ($class['methods'] as $methodName => $method) { + /** @phpstan-ignore equal.notAllowed */ + if ($method['executableLines'] == 0) { + continue; + } + + $classMethods++; + $classStatements += $method['executableLines']; + $coveredClassStatements += $method['executedLines']; + + /** @phpstan-ignore equal.notAllowed */ + if ($method['coverage'] == 100) { + $coveredMethods++; + } + + $methodCount = 0; + + foreach (range($method['startLine'], $method['endLine']) as $line) { + if (isset($coverageData[$line])) { + $methodCount = max($methodCount, count($coverageData[$line])); + } + } + + $lines[$method['startLine']] = [ + 'ccn' => $method['ccn'], + 'count' => $methodCount, + 'crap' => $method['crap'], + 'type' => 'method', + 'visibility' => $method['visibility'], + 'name' => $methodName, + ]; + } + + $xmlClass = $xmlDocument->createElement('class'); + $xmlClass->setAttribute('name', $className); + $xmlClass->setAttribute('namespace', $namespace); + + $xmlFile->appendChild($xmlClass); + + $xmlMetrics = $xmlDocument->createElement('metrics'); + $xmlMetrics->setAttribute('complexity', (string) $class['ccn']); + $xmlMetrics->setAttribute('methods', (string) $classMethods); + $xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods); + $xmlMetrics->setAttribute('conditionals', (string) $class['executableBranches']); + $xmlMetrics->setAttribute('coveredconditionals', (string) $class['executedBranches']); + $xmlMetrics->setAttribute('statements', (string) $classStatements); + $xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements); + $xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class['executableBranches'])); + $xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class['executedBranches'])); + $xmlClass->appendChild($xmlMetrics); + } + + foreach ($coverageData as $line => $data) { + if ($data === null || isset($lines[$line])) { + continue; + } + + $lines[$line] = [ + 'count' => count($data), 'type' => 'stmt', + ]; + } + + ksort($lines); + + foreach ($lines as $line => $data) { + $xmlLine = $xmlDocument->createElement('line'); + $xmlLine->setAttribute('num', (string) $line); + $xmlLine->setAttribute('type', $data['type']); + + if (isset($data['name'])) { + $xmlLine->setAttribute('name', $data['name']); + } + + if (isset($data['visibility'])) { + $xmlLine->setAttribute('visibility', $data['visibility']); + } + + if (isset($data['ccn'])) { + $xmlLine->setAttribute('complexity', (string) $data['ccn']); + } + + if (isset($data['crap'])) { + $xmlLine->setAttribute('crap', (string) $data['crap']); + } + + $xmlLine->setAttribute('count', (string) $data['count']); + $xmlFile->appendChild($xmlLine); + } + + $linesOfCode = $item->linesOfCode(); + + $xmlMetrics = $xmlDocument->createElement('metrics'); + $xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode()); + $xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode()); + $xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits()); + $xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods()); + $xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods()); + $xmlMetrics->setAttribute('conditionals', (string) $item->numberOfExecutableBranches()); + $xmlMetrics->setAttribute('coveredconditionals', (string) $item->numberOfExecutedBranches()); + $xmlMetrics->setAttribute('statements', (string) $item->numberOfExecutableLines()); + $xmlMetrics->setAttribute('coveredstatements', (string) $item->numberOfExecutedLines()); + $xmlMetrics->setAttribute('elements', (string) ($item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches())); + $xmlMetrics->setAttribute('coveredelements', (string) ($item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches())); + $xmlFile->appendChild($xmlMetrics); + + if ($namespace === 'global') { + $xmlProject->appendChild($xmlFile); + } else { + if (!isset($packages[$namespace])) { + $packages[$namespace] = $xmlDocument->createElement( + 'package', + ); + + $packages[$namespace]->setAttribute('name', $namespace); + $xmlProject->appendChild($packages[$namespace]); + } + + $packages[$namespace]->appendChild($xmlFile); + } + } + + $linesOfCode = $report->linesOfCode(); + + $xmlMetrics = $xmlDocument->createElement('metrics'); + $xmlMetrics->setAttribute('files', (string) count($report)); + $xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode()); + $xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode()); + $xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits()); + $xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods()); + $xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods()); + $xmlMetrics->setAttribute('conditionals', (string) $report->numberOfExecutableBranches()); + $xmlMetrics->setAttribute('coveredconditionals', (string) $report->numberOfExecutedBranches()); + $xmlMetrics->setAttribute('statements', (string) $report->numberOfExecutableLines()); + $xmlMetrics->setAttribute('coveredstatements', (string) $report->numberOfExecutedLines()); + $xmlMetrics->setAttribute('elements', (string) ($report->numberOfMethods() + $report->numberOfExecutableLines() + $report->numberOfExecutableBranches())); + $xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches())); + $xmlProject->appendChild($xmlMetrics); + + $buffer = $xmlDocument->saveXML(); + + if ($target !== null) { + if (!str_contains($target, '://')) { + Filesystem::createDirectory(dirname($target)); + } + + if (@file_put_contents($target, $buffer) === false) { + throw new WriteOperationFailedException($target); + } + } + + return $buffer; + } +} diff --git a/src/Report/Cobertura.php b/src/Report/Cobertura.php new file mode 100644 index 000000000..51786e5df --- /dev/null +++ b/src/Report/Cobertura.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report; + +use const DIRECTORY_SEPARATOR; +use function basename; +use function count; +use function dirname; +use function file_put_contents; +use function preg_match; +use function range; +use function str_contains; +use function str_replace; +use function time; +use DOMImplementation; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Node\File; +use SebastianBergmann\CodeCoverage\Util\Filesystem; +use SebastianBergmann\CodeCoverage\WriteOperationFailedException; + +final class Cobertura +{ + /** + * @throws WriteOperationFailedException + */ + public function process(CodeCoverage $coverage, ?string $target = null): string + { + $time = (string) time(); + + $report = $coverage->getReport(); + + $implementation = new DOMImplementation; + + $documentType = $implementation->createDocumentType( + 'coverage', + '', + 'http://cobertura.sourceforge.net/xml/coverage-04.dtd', + ); + + $document = $implementation->createDocument('', '', $documentType); + $document->xmlVersion = '1.0'; + $document->encoding = 'UTF-8'; + $document->formatOutput = true; + + $coverageElement = $document->createElement('coverage'); + + $linesValid = $report->numberOfExecutableLines(); + $linesCovered = $report->numberOfExecutedLines(); + $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); + $coverageElement->setAttribute('line-rate', (string) $lineRate); + + $branchesValid = $report->numberOfExecutableBranches(); + $branchesCovered = $report->numberOfExecutedBranches(); + $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); + $coverageElement->setAttribute('branch-rate', (string) $branchRate); + + $coverageElement->setAttribute('lines-covered', (string) $report->numberOfExecutedLines()); + $coverageElement->setAttribute('lines-valid', (string) $report->numberOfExecutableLines()); + $coverageElement->setAttribute('branches-covered', (string) $report->numberOfExecutedBranches()); + $coverageElement->setAttribute('branches-valid', (string) $report->numberOfExecutableBranches()); + $coverageElement->setAttribute('complexity', ''); + $coverageElement->setAttribute('version', '0.4'); + $coverageElement->setAttribute('timestamp', $time); + + $document->appendChild($coverageElement); + + $sourcesElement = $document->createElement('sources'); + $coverageElement->appendChild($sourcesElement); + + $sourceElement = $document->createElement('source', $report->pathAsString()); + $sourcesElement->appendChild($sourceElement); + + $packagesElement = $document->createElement('packages'); + $coverageElement->appendChild($packagesElement); + + $complexity = 0; + + foreach ($report as $item) { + if (!$item instanceof File) { + continue; + } + + $packageElement = $document->createElement('package'); + $packageComplexity = 0; + + $packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); + + $linesValid = $item->numberOfExecutableLines(); + $linesCovered = $item->numberOfExecutedLines(); + $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); + + $packageElement->setAttribute('line-rate', (string) $lineRate); + + $branchesValid = $item->numberOfExecutableBranches(); + $branchesCovered = $item->numberOfExecutedBranches(); + $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); + + $packageElement->setAttribute('branch-rate', (string) $branchRate); + + $packageElement->setAttribute('complexity', ''); + $packagesElement->appendChild($packageElement); + + $classesElement = $document->createElement('classes'); + + $packageElement->appendChild($classesElement); + + $classes = $item->classesAndTraits(); + $coverageData = $item->lineCoverageData(); + + foreach ($classes as $className => $class) { + $complexity += $class['ccn']; + $packageComplexity += $class['ccn']; + + $linesValid = $class['executableLines']; + $linesCovered = $class['executedLines']; + $lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid); + + $branchesValid = $class['executableBranches']; + $branchesCovered = $class['executedBranches']; + $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); + + $classElement = $document->createElement('class'); + + $classElement->setAttribute('name', $className); + $classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); + $classElement->setAttribute('line-rate', (string) $lineRate); + $classElement->setAttribute('branch-rate', (string) $branchRate); + $classElement->setAttribute('complexity', (string) $class['ccn']); + + $classesElement->appendChild($classElement); + + $methodsElement = $document->createElement('methods'); + + $classElement->appendChild($methodsElement); + + $classLinesElement = $document->createElement('lines'); + + $classElement->appendChild($classLinesElement); + + foreach ($class['methods'] as $methodName => $method) { + if ($method['executableLines'] === 0) { + continue; + } + + preg_match("/\((.*?)\)/", $method['signature'], $signature); + + $linesValid = $method['executableLines']; + $linesCovered = $method['executedLines']; + $lineRate = $linesCovered / $linesValid; + + $branchesValid = $method['executableBranches']; + $branchesCovered = $method['executedBranches']; + $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); + + $methodElement = $document->createElement('method'); + + $methodElement->setAttribute('name', $methodName); + $methodElement->setAttribute('signature', $signature[1]); + $methodElement->setAttribute('line-rate', (string) $lineRate); + $methodElement->setAttribute('branch-rate', (string) $branchRate); + $methodElement->setAttribute('complexity', (string) $method['ccn']); + + $methodLinesElement = $document->createElement('lines'); + + $methodElement->appendChild($methodLinesElement); + + foreach (range($method['startLine'], $method['endLine']) as $line) { + if (!isset($coverageData[$line])) { + continue; + } + $methodLineElement = $document->createElement('line'); + + $methodLineElement->setAttribute('number', (string) $line); + $methodLineElement->setAttribute('hits', (string) count($coverageData[$line])); + + $methodLinesElement->appendChild($methodLineElement); + + $classLineElement = $methodLineElement->cloneNode(); + + $classLinesElement->appendChild($classLineElement); + } + + $methodsElement->appendChild($methodElement); + } + } + + if ($item->numberOfFunctions() === 0) { + $packageElement->setAttribute('complexity', (string) $packageComplexity); + + continue; + } + + $functionsComplexity = 0; + $functionsLinesValid = 0; + $functionsLinesCovered = 0; + $functionsBranchesValid = 0; + $functionsBranchesCovered = 0; + + $classElement = $document->createElement('class'); + $classElement->setAttribute('name', basename($item->pathAsString())); + $classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString())); + + $methodsElement = $document->createElement('methods'); + + $classElement->appendChild($methodsElement); + + $classLinesElement = $document->createElement('lines'); + + $classElement->appendChild($classLinesElement); + + $functions = $item->functions(); + + foreach ($functions as $functionName => $function) { + if ($function['executableLines'] === 0) { + continue; + } + + $complexity += $function['ccn']; + $packageComplexity += $function['ccn']; + $functionsComplexity += $function['ccn']; + + $linesValid = $function['executableLines']; + $linesCovered = $function['executedLines']; + $lineRate = $linesCovered / $linesValid; + + $functionsLinesValid += $linesValid; + $functionsLinesCovered += $linesCovered; + + $branchesValid = $function['executableBranches']; + $branchesCovered = $function['executedBranches']; + $branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid); + + $functionsBranchesValid += $branchesValid; + $functionsBranchesCovered += $branchesValid; + + $methodElement = $document->createElement('method'); + + $methodElement->setAttribute('name', $functionName); + $methodElement->setAttribute('signature', $function['signature']); + $methodElement->setAttribute('line-rate', (string) $lineRate); + $methodElement->setAttribute('branch-rate', (string) $branchRate); + $methodElement->setAttribute('complexity', (string) $function['ccn']); + + $methodLinesElement = $document->createElement('lines'); + + $methodElement->appendChild($methodLinesElement); + + foreach (range($function['startLine'], $function['endLine']) as $line) { + if (!isset($coverageData[$line])) { + continue; + } + $methodLineElement = $document->createElement('line'); + + $methodLineElement->setAttribute('number', (string) $line); + $methodLineElement->setAttribute('hits', (string) count($coverageData[$line])); + + $methodLinesElement->appendChild($methodLineElement); + + $classLineElement = $methodLineElement->cloneNode(); + + $classLinesElement->appendChild($classLineElement); + } + + $methodsElement->appendChild($methodElement); + } + + $packageElement->setAttribute('complexity', (string) $packageComplexity); + + if ($functionsLinesValid === 0) { + continue; + } + + $lineRate = $functionsLinesCovered / $functionsLinesValid; + $branchRate = $functionsBranchesValid === 0 ? 0 : ($functionsBranchesCovered / $functionsBranchesValid); + + $classElement->setAttribute('line-rate', (string) $lineRate); + $classElement->setAttribute('branch-rate', (string) $branchRate); + $classElement->setAttribute('complexity', (string) $functionsComplexity); + + $classesElement->appendChild($classElement); + } + + $coverageElement->setAttribute('complexity', (string) $complexity); + + $buffer = $document->saveXML(); + + if ($target !== null) { + if (!str_contains($target, '://')) { + Filesystem::createDirectory(dirname($target)); + } + + if (@file_put_contents($target, $buffer) === false) { + throw new WriteOperationFailedException($target); + } + } + + return $buffer; + } +} diff --git a/src/Report/Crap4j.php b/src/Report/Crap4j.php new file mode 100644 index 000000000..57fabfce5 --- /dev/null +++ b/src/Report/Crap4j.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report; + +use function date; +use function dirname; +use function file_put_contents; +use function htmlspecialchars; +use function is_string; +use function round; +use function str_contains; +use DOMDocument; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Node\File; +use SebastianBergmann\CodeCoverage\Util\Filesystem; +use SebastianBergmann\CodeCoverage\WriteOperationFailedException; + +final readonly class Crap4j +{ + private int $threshold; + + public function __construct(int $threshold = 30) + { + $this->threshold = $threshold; + } + + /** + * @throws WriteOperationFailedException + */ + public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string + { + $document = new DOMDocument('1.0', 'UTF-8'); + $document->formatOutput = true; + + $root = $document->createElement('crap_result'); + $document->appendChild($root); + + $project = $document->createElement('project', is_string($name) ? $name : ''); + $root->appendChild($project); + $root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s'))); + + $stats = $document->createElement('stats'); + $methodsNode = $document->createElement('methods'); + + $report = $coverage->getReport(); + unset($coverage); + + $fullMethodCount = 0; + $fullCrapMethodCount = 0; + $fullCrapLoad = 0; + $fullCrap = 0; + + foreach ($report as $item) { + $namespace = 'global'; + + if (!$item instanceof File) { + continue; + } + + $file = $document->createElement('file'); + $file->setAttribute('name', $item->pathAsString()); + + $classes = $item->classesAndTraits(); + + foreach ($classes as $className => $class) { + foreach ($class['methods'] as $methodName => $method) { + $crapLoad = $this->crapLoad((float) $method['crap'], $method['ccn'], $method['coverage']); + + $fullCrap += $method['crap']; + $fullCrapLoad += $crapLoad; + $fullMethodCount++; + + if ($method['crap'] >= $this->threshold) { + $fullCrapMethodCount++; + } + + $methodNode = $document->createElement('method'); + + if ($class['namespace'] !== '') { + $namespace = $class['namespace']; + } + + $methodNode->appendChild($document->createElement('package', $namespace)); + $methodNode->appendChild($document->createElement('className', $className)); + $methodNode->appendChild($document->createElement('methodName', $methodName)); + $methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method['signature']))); + $methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method['signature']))); + $methodNode->appendChild($document->createElement('crap', (string) $this->roundValue((float) $method['crap']))); + $methodNode->appendChild($document->createElement('complexity', (string) $method['ccn'])); + $methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method['coverage']))); + $methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad))); + + $methodsNode->appendChild($methodNode); + } + } + } + + $stats->appendChild($document->createElement('name', 'Method Crap Stats')); + $stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount)); + $stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount)); + $stats->appendChild($document->createElement('crapLoad', (string) round($fullCrapLoad))); + $stats->appendChild($document->createElement('totalCrap', (string) $fullCrap)); + + $crapMethodPercent = 0; + + if ($fullMethodCount > 0) { + $crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount); + } + + $stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent)); + + $root->appendChild($stats); + $root->appendChild($methodsNode); + + $buffer = $document->saveXML(); + + if ($target !== null) { + if (!str_contains($target, '://')) { + Filesystem::createDirectory(dirname($target)); + } + + if (@file_put_contents($target, $buffer) === false) { + throw new WriteOperationFailedException($target); + } + } + + return $buffer; + } + + private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float + { + $crapLoad = 0; + + if ($crapValue >= $this->threshold) { + $crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100); + $crapLoad += $cyclomaticComplexity / $this->threshold; + } + + return $crapLoad; + } + + private function roundValue(float $value): float + { + return round($value, 2); + } +} diff --git a/src/Report/Html/Colors.php b/src/Report/Html/Colors.php new file mode 100644 index 000000000..c79bf9ee5 --- /dev/null +++ b/src/Report/Html/Colors.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +/** + * @immutable + */ +final readonly class Colors +{ + private string $successLow; + private string $successMedium; + private string $successHigh; + private string $warning; + private string $danger; + + public static function default(): self + { + return new self('#dff0d8', '#c3e3b5', '#99cb84', '#fcf8e3', '#f2dede'); + } + + public static function from(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger): self + { + return new self($successLow, $successMedium, $successHigh, $warning, $danger); + } + + private function __construct(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger) + { + $this->successLow = $successLow; + $this->successMedium = $successMedium; + $this->successHigh = $successHigh; + $this->warning = $warning; + $this->danger = $danger; + } + + public function successLow(): string + { + return $this->successLow; + } + + public function successMedium(): string + { + return $this->successMedium; + } + + public function successHigh(): string + { + return $this->successHigh; + } + + public function warning(): string + { + return $this->warning; + } + + public function danger(): string + { + return $this->danger; + } +} diff --git a/src/Report/Html/CustomCssFile.php b/src/Report/Html/CustomCssFile.php new file mode 100644 index 000000000..5c272a0bc --- /dev/null +++ b/src/Report/Html/CustomCssFile.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use function is_file; +use SebastianBergmann\CodeCoverage\InvalidArgumentException; + +/** + * @immutable + */ +final readonly class CustomCssFile +{ + private string $path; + + public static function default(): self + { + return new self(__DIR__ . '/Renderer/Template/css/custom.css'); + } + + /** + * @throws InvalidArgumentException + */ + public static function from(string $path): self + { + if (!is_file($path)) { + throw new InvalidArgumentException( + '$path does not exist', + ); + } + + return new self($path); + } + + private function __construct(string $path) + { + $this->path = $path; + } + + public function path(): string + { + return $this->path; + } +} diff --git a/src/Report/Html/Facade.php b/src/Report/Html/Facade.php new file mode 100644 index 000000000..0e8b230aa --- /dev/null +++ b/src/Report/Html/Facade.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use const DIRECTORY_SEPARATOR; +use function copy; +use function date; +use function dirname; +use function str_ends_with; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; +use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; +use SebastianBergmann\CodeCoverage\Report\Thresholds; +use SebastianBergmann\CodeCoverage\Util\Filesystem; +use SebastianBergmann\Template\Exception; +use SebastianBergmann\Template\Template; + +final readonly class Facade +{ + private string $templatePath; + private string $generator; + private Colors $colors; + private Thresholds $thresholds; + private CustomCssFile $customCssFile; + + public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null) + { + $this->generator = $generator; + $this->colors = $colors ?? Colors::default(); + $this->thresholds = $thresholds ?? Thresholds::default(); + $this->customCssFile = $customCssFile ?? CustomCssFile::default(); + $this->templatePath = __DIR__ . '/Renderer/Template/'; + } + + public function process(CodeCoverage $coverage, string $target): void + { + $target = $this->directory($target); + $report = $coverage->getReport(); + $date = date('D M j G:i:s T Y'); + + $dashboard = new Dashboard( + $this->templatePath, + $this->generator, + $date, + $this->thresholds, + $coverage->collectsBranchAndPathCoverage(), + ); + + $directory = new Directory( + $this->templatePath, + $this->generator, + $date, + $this->thresholds, + $coverage->collectsBranchAndPathCoverage(), + ); + + $file = new File( + $this->templatePath, + $this->generator, + $date, + $this->thresholds, + $coverage->collectsBranchAndPathCoverage(), + ); + + $directory->render($report, $target . 'index.html'); + $dashboard->render($report, $target . 'dashboard.html'); + + foreach ($report as $node) { + $id = $node->id(); + + if ($node instanceof DirectoryNode) { + Filesystem::createDirectory($target . $id); + + $directory->render($node, $target . $id . '/index.html'); + $dashboard->render($node, $target . $id . '/dashboard.html'); + } else { + $dir = dirname($target . $id); + + Filesystem::createDirectory($dir); + + $file->render($node, $target . $id); + } + } + + $this->copyFiles($target); + $this->renderCss($target); + } + + private function copyFiles(string $target): void + { + $dir = $this->directory($target . '_css'); + + copy($this->templatePath . 'css/billboard.min.css', $dir . 'billboard.min.css'); + copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css'); + copy($this->customCssFile->path(), $dir . 'custom.css'); + copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css'); + + $dir = $this->directory($target . '_icons'); + copy($this->templatePath . 'icons/file-code.svg', $dir . 'file-code.svg'); + copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg'); + + $dir = $this->directory($target . '_js'); + copy($this->templatePath . 'js/billboard.pkgd.min.js', $dir . 'billboard.pkgd.min.js'); + copy($this->templatePath . 'js/bootstrap.bundle.min.js', $dir . 'bootstrap.bundle.min.js'); + copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js'); + copy($this->templatePath . 'js/file.js', $dir . 'file.js'); + } + + private function renderCss(string $target): void + { + $template = new Template($this->templatePath . 'css/style.css', '{{', '}}'); + + $template->setVar( + [ + 'success-low' => $this->colors->successLow(), + 'success-medium' => $this->colors->successMedium(), + 'success-high' => $this->colors->successHigh(), + 'warning' => $this->colors->warning(), + 'danger' => $this->colors->danger(), + ], + ); + + try { + $template->renderTo($this->directory($target . '_css') . 'style.css'); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + // @codeCoverageIgnoreEnd + } + } + + private function directory(string $directory): string + { + if (!str_ends_with($directory, DIRECTORY_SEPARATOR)) { + $directory .= DIRECTORY_SEPARATOR; + } + + Filesystem::createDirectory($directory); + + return $directory; + } +} diff --git a/src/Report/Html/Renderer.php b/src/Report/Html/Renderer.php new file mode 100644 index 000000000..49a03e5cf --- /dev/null +++ b/src/Report/Html/Renderer.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use function array_pop; +use function count; +use function sprintf; +use function str_repeat; +use function substr_count; +use SebastianBergmann\CodeCoverage\Node\AbstractNode; +use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; +use SebastianBergmann\CodeCoverage\Node\File as FileNode; +use SebastianBergmann\CodeCoverage\Report\Thresholds; +use SebastianBergmann\CodeCoverage\Version; +use SebastianBergmann\Environment\Runtime; +use SebastianBergmann\Template\Template; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +abstract class Renderer +{ + protected string $templatePath; + protected string $generator; + protected string $date; + protected Thresholds $thresholds; + protected bool $hasBranchCoverage; + protected string $version; + + public function __construct(string $templatePath, string $generator, string $date, Thresholds $thresholds, bool $hasBranchCoverage) + { + $this->templatePath = $templatePath; + $this->generator = $generator; + $this->date = $date; + $this->thresholds = $thresholds; + $this->version = Version::id(); + $this->hasBranchCoverage = $hasBranchCoverage; + } + + /** + * @param array $data + */ + protected function renderItemTemplate(Template $template, array $data): string + { + $numSeparator = ' / '; + + if (isset($data['numClasses']) && $data['numClasses'] > 0) { + $classesLevel = $this->colorLevel($data['testedClassesPercent']); + + $classesNumber = $data['numTestedClasses'] . $numSeparator . + $data['numClasses']; + + $classesBar = $this->coverageBar( + $data['testedClassesPercent'], + ); + } else { + $classesLevel = ''; + $classesNumber = '0' . $numSeparator . '0'; + $classesBar = ''; + $data['testedClassesPercentAsString'] = 'n/a'; + } + + if ($data['numMethods'] > 0) { + $methodsLevel = $this->colorLevel($data['testedMethodsPercent']); + + $methodsNumber = $data['numTestedMethods'] . $numSeparator . + $data['numMethods']; + + $methodsBar = $this->coverageBar( + $data['testedMethodsPercent'], + ); + } else { + $methodsLevel = ''; + $methodsNumber = '0' . $numSeparator . '0'; + $methodsBar = ''; + $data['testedMethodsPercentAsString'] = 'n/a'; + } + + if ($data['numExecutableLines'] > 0) { + $linesLevel = $this->colorLevel($data['linesExecutedPercent']); + + $linesNumber = $data['numExecutedLines'] . $numSeparator . + $data['numExecutableLines']; + + $linesBar = $this->coverageBar( + $data['linesExecutedPercent'], + ); + } else { + $linesLevel = ''; + $linesNumber = '0' . $numSeparator . '0'; + $linesBar = ''; + $data['linesExecutedPercentAsString'] = 'n/a'; + } + + if ($data['numExecutablePaths'] > 0) { + $pathsLevel = $this->colorLevel($data['pathsExecutedPercent']); + + $pathsNumber = $data['numExecutedPaths'] . $numSeparator . + $data['numExecutablePaths']; + + $pathsBar = $this->coverageBar( + $data['pathsExecutedPercent'], + ); + } else { + $pathsLevel = ''; + $pathsNumber = '0' . $numSeparator . '0'; + $pathsBar = ''; + $data['pathsExecutedPercentAsString'] = 'n/a'; + } + + if ($data['numExecutableBranches'] > 0) { + $branchesLevel = $this->colorLevel($data['branchesExecutedPercent']); + + $branchesNumber = $data['numExecutedBranches'] . $numSeparator . + $data['numExecutableBranches']; + + $branchesBar = $this->coverageBar( + $data['branchesExecutedPercent'], + ); + } else { + $branchesLevel = ''; + $branchesNumber = '0' . $numSeparator . '0'; + $branchesBar = ''; + $data['branchesExecutedPercentAsString'] = 'n/a'; + } + + $template->setVar( + [ + 'icon' => $data['icon'] ?? '', + 'crap' => $data['crap'] ?? '', + 'name' => $data['name'], + 'lines_bar' => $linesBar, + 'lines_executed_percent' => $data['linesExecutedPercentAsString'], + 'lines_level' => $linesLevel, + 'lines_number' => $linesNumber, + 'paths_bar' => $pathsBar, + 'paths_executed_percent' => $data['pathsExecutedPercentAsString'], + 'paths_level' => $pathsLevel, + 'paths_number' => $pathsNumber, + 'branches_bar' => $branchesBar, + 'branches_executed_percent' => $data['branchesExecutedPercentAsString'], + 'branches_level' => $branchesLevel, + 'branches_number' => $branchesNumber, + 'methods_bar' => $methodsBar, + 'methods_tested_percent' => $data['testedMethodsPercentAsString'], + 'methods_level' => $methodsLevel, + 'methods_number' => $methodsNumber, + 'classes_bar' => $classesBar, + 'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '', + 'classes_level' => $classesLevel, + 'classes_number' => $classesNumber, + ], + ); + + return $template->render(); + } + + protected function setCommonTemplateVariables(Template $template, AbstractNode $node): void + { + $template->setVar( + [ + 'id' => $node->id(), + 'full_path' => $node->pathAsString(), + 'path_to_root' => $this->pathToRoot($node), + 'breadcrumbs' => $this->breadcrumbs($node), + 'date' => $this->date, + 'version' => $this->version, + 'runtime' => $this->runtimeString(), + 'generator' => $this->generator, + 'low_upper_bound' => (string) $this->thresholds->lowUpperBound(), + 'high_lower_bound' => (string) $this->thresholds->highLowerBound(), + ], + ); + } + + protected function breadcrumbs(AbstractNode $node): string + { + $breadcrumbs = ''; + $path = $node->pathAsArray(); + $pathToRoot = []; + $max = count($path); + + if ($node instanceof FileNode) { + $max--; + } + + for ($i = 0; $i < $max; $i++) { + $pathToRoot[] = str_repeat('../', $i); + } + + foreach ($path as $step) { + if ($step !== $node) { + $breadcrumbs .= $this->inactiveBreadcrumb( + $step, + array_pop($pathToRoot), + ); + } else { + $breadcrumbs .= $this->activeBreadcrumb($step); + } + } + + return $breadcrumbs; + } + + protected function activeBreadcrumb(AbstractNode $node): string + { + $buffer = sprintf( + ' ' . "\n", + $node->name(), + ); + + if ($node instanceof DirectoryNode) { + $buffer .= ' ' . "\n"; + } + + return $buffer; + } + + protected function inactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string + { + return sprintf( + ' ' . "\n", + $pathToRoot, + $node->name(), + ); + } + + protected function pathToRoot(AbstractNode $node): string + { + $id = $node->id(); + $depth = substr_count($id, '/'); + + if ($id !== 'index' && + $node instanceof DirectoryNode) { + $depth++; + } + + return str_repeat('../', $depth); + } + + protected function coverageBar(float $percent): string + { + $level = $this->colorLevel($percent); + + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'coverage_bar_branch.html' : 'coverage_bar.html'); + $template = new Template( + $templateName, + '{{', + '}}', + ); + + $template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]); + + return $template->render(); + } + + protected function colorLevel(float $percent): string + { + if ($percent <= $this->thresholds->lowUpperBound()) { + return 'danger'; + } + + if ($percent > $this->thresholds->lowUpperBound() && + $percent < $this->thresholds->highLowerBound()) { + return 'warning'; + } + + return 'success'; + } + + private function runtimeString(): string + { + $runtime = new Runtime; + + return sprintf( + '%s %s', + $runtime->getVendorUrl(), + $runtime->getName(), + $runtime->getVersion(), + ); + } +} diff --git a/src/Report/Html/Renderer/Dashboard.php b/src/Report/Html/Renderer/Dashboard.php new file mode 100644 index 000000000..305c7fa10 --- /dev/null +++ b/src/Report/Html/Renderer/Dashboard.php @@ -0,0 +1,331 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use function array_values; +use function asort; +use function assert; +use function count; +use function explode; +use function floor; +use function json_encode; +use function sprintf; +use function str_replace; +use function uasort; +use function usort; +use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; +use SebastianBergmann\CodeCoverage\Node\AbstractNode; +use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; +use SebastianBergmann\CodeCoverage\Node\File as FileNode; +use SebastianBergmann\Template\Exception; +use SebastianBergmann\Template\Template; + +/** + * @phpstan-import-type ProcessedClassType from FileNode + * @phpstan-import-type ProcessedTraitType from FileNode + * + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class Dashboard extends Renderer +{ + public function render(DirectoryNode $node, string $file): void + { + $classes = $node->classesAndTraits(); + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'dashboard_branch.html' : 'dashboard.html'); + $template = new Template( + $templateName, + '{{', + '}}', + ); + + $this->setCommonTemplateVariables($template, $node); + + $baseLink = $node->id() . '/'; + $complexity = $this->complexity($classes, $baseLink); + $coverageDistribution = $this->coverageDistribution($classes); + $insufficientCoverage = $this->insufficientCoverage($classes, $baseLink); + $projectRisks = $this->projectRisks($classes, $baseLink); + + $template->setVar( + [ + 'insufficient_coverage_classes' => $insufficientCoverage['class'], + 'insufficient_coverage_methods' => $insufficientCoverage['method'], + 'project_risks_classes' => $projectRisks['class'], + 'project_risks_methods' => $projectRisks['method'], + 'complexity_class' => $complexity['class'], + 'complexity_method' => $complexity['method'], + 'class_coverage_distribution' => $coverageDistribution['class'], + 'method_coverage_distribution' => $coverageDistribution['method'], + ], + ); + + try { + $template->renderTo($file); + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + } + + protected function activeBreadcrumb(AbstractNode $node): string + { + return sprintf( + ' ' . "\n" . + ' ' . "\n", + $node->name(), + ); + } + + /** + * @param array $classes + * + * @return array{class: non-empty-string, method: non-empty-string} + */ + private function complexity(array $classes, string $baseLink): array + { + $result = ['class' => [], 'method' => []]; + + foreach ($classes as $className => $class) { + foreach ($class['methods'] as $methodName => $method) { + if ($className !== '*') { + $methodName = $className . '::' . $methodName; + } + + $result['method'][] = [ + $method['coverage'], + $method['ccn'], + str_replace($baseLink, '', $method['link']), + $methodName, + $method['crap'], + ]; + } + + $result['class'][] = [ + $class['coverage'], + $class['ccn'], + str_replace($baseLink, '', $class['link']), + $className, + $class['crap'], + ]; + } + + usort($result['class'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0])); + usort($result['method'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0])); + + $class = json_encode($result['class']); + + assert($class !== false); + + $method = json_encode($result['method']); + + assert($method !== false); + + return ['class' => $class, 'method' => $method]; + } + + /** + * @param array $classes + * + * @return array{class: non-empty-string, method: non-empty-string} + */ + private function coverageDistribution(array $classes): array + { + $result = [ + 'class' => [ + '0%' => 0, + '0-10%' => 0, + '10-20%' => 0, + '20-30%' => 0, + '30-40%' => 0, + '40-50%' => 0, + '50-60%' => 0, + '60-70%' => 0, + '70-80%' => 0, + '80-90%' => 0, + '90-100%' => 0, + '100%' => 0, + ], + 'method' => [ + '0%' => 0, + '0-10%' => 0, + '10-20%' => 0, + '20-30%' => 0, + '30-40%' => 0, + '40-50%' => 0, + '50-60%' => 0, + '60-70%' => 0, + '70-80%' => 0, + '80-90%' => 0, + '90-100%' => 0, + '100%' => 0, + ], + ]; + + foreach ($classes as $class) { + foreach ($class['methods'] as $methodName => $method) { + if ($method['coverage'] === 0) { + $result['method']['0%']++; + } elseif ($method['coverage'] === 100) { + $result['method']['100%']++; + } else { + $key = floor($method['coverage'] / 10) * 10; + $key = $key . '-' . ($key + 10) . '%'; + $result['method'][$key]++; + } + } + + if ($class['coverage'] === 0) { + $result['class']['0%']++; + } elseif ($class['coverage'] === 100) { + $result['class']['100%']++; + } else { + $key = floor($class['coverage'] / 10) * 10; + $key = $key . '-' . ($key + 10) . '%'; + $result['class'][$key]++; + } + } + + $class = json_encode(array_values($result['class'])); + + assert($class !== false); + + $method = json_encode(array_values($result['method'])); + + assert($method !== false); + + return ['class' => $class, 'method' => $method]; + } + + /** + * @param array $classes + * + * @return array{class: string, method: string} + */ + private function insufficientCoverage(array $classes, string $baseLink): array + { + $leastTestedClasses = []; + $leastTestedMethods = []; + $result = ['class' => '', 'method' => '']; + + foreach ($classes as $className => $class) { + foreach ($class['methods'] as $methodName => $method) { + if ($method['coverage'] < $this->thresholds->highLowerBound()) { + $key = $methodName; + + if ($className !== '*') { + $key = $className . '::' . $methodName; + } + + $leastTestedMethods[$key] = $method['coverage']; + } + } + + if ($class['coverage'] < $this->thresholds->highLowerBound()) { + $leastTestedClasses[$className] = $class['coverage']; + } + } + + asort($leastTestedClasses); + asort($leastTestedMethods); + + foreach ($leastTestedClasses as $className => $coverage) { + $result['class'] .= sprintf( + ' %s%d%%' . "\n", + str_replace($baseLink, '', $classes[$className]['link']), + $className, + $coverage, + ); + } + + foreach ($leastTestedMethods as $methodName => $coverage) { + [$class, $method] = explode('::', $methodName); + + $result['method'] .= sprintf( + ' %s%d%%' . "\n", + str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), + $methodName, + $method, + $coverage, + ); + } + + return $result; + } + + /** + * @param array $classes + * + * @return array{class: string, method: string} + */ + private function projectRisks(array $classes, string $baseLink): array + { + $classRisks = []; + $methodRisks = []; + $result = ['class' => '', 'method' => '']; + + foreach ($classes as $className => $class) { + foreach ($class['methods'] as $methodName => $method) { + if ($method['coverage'] < $this->thresholds->highLowerBound() && $method['ccn'] > 1) { + $key = $methodName; + + if ($className !== '*') { + $key = $className . '::' . $methodName; + } + + $methodRisks[$key] = $method; + } + } + + if ($class['coverage'] < $this->thresholds->highLowerBound() && + $class['ccn'] > count($class['methods'])) { + $classRisks[$className] = $class; + } + } + + uasort($classRisks, static function (array $a, array $b) + { + return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1; + }); + uasort($methodRisks, static function (array $a, array $b) + { + return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1; + }); + + foreach ($classRisks as $className => $class) { + $result['class'] .= sprintf( + ' %s%.1f%%%d%d' . "\n", + str_replace($baseLink, '', $classes[$className]['link']), + $className, + $class['coverage'], + $class['ccn'], + $class['crap'], + ); + } + + foreach ($methodRisks as $methodName => $methodVals) { + [$class, $method] = explode('::', $methodName); + + $result['method'] .= sprintf( + ' %s%.1f%%%d%d' . "\n", + str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), + $methodName, + $method, + $methodVals['coverage'], + $methodVals['ccn'], + $methodVals['crap'], + ); + } + + return $result; + } +} diff --git a/src/Report/Html/Renderer/Directory.php b/src/Report/Html/Renderer/Directory.php new file mode 100644 index 000000000..1d7334b3a --- /dev/null +++ b/src/Report/Html/Renderer/Directory.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use function count; +use function sprintf; +use function str_repeat; +use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; +use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node; +use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; +use SebastianBergmann\Template\Exception; +use SebastianBergmann\Template\Template; + +/** + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class Directory extends Renderer +{ + public function render(DirectoryNode $node, string $file): void + { + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_branch.html' : 'directory.html'); + $template = new Template($templateName, '{{', '}}'); + + $this->setCommonTemplateVariables($template, $node); + + $items = $this->renderItem($node, true); + + foreach ($node->directories() as $item) { + $items .= $this->renderItem($item); + } + + foreach ($node->files() as $item) { + $items .= $this->renderItem($item); + } + + $template->setVar( + [ + 'id' => $node->id(), + 'items' => $items, + ], + ); + + try { + $template->renderTo($file); + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + } + + private function renderItem(Node $node, bool $total = false): string + { + $data = [ + 'numClasses' => $node->numberOfClassesAndTraits(), + 'numTestedClasses' => $node->numberOfTestedClassesAndTraits(), + 'numMethods' => $node->numberOfFunctionsAndMethods(), + 'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(), + 'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(), + 'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(), + 'numExecutedLines' => $node->numberOfExecutedLines(), + 'numExecutableLines' => $node->numberOfExecutableLines(), + 'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(), + 'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(), + 'numExecutedBranches' => $node->numberOfExecutedBranches(), + 'numExecutableBranches' => $node->numberOfExecutableBranches(), + 'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(), + 'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(), + 'numExecutedPaths' => $node->numberOfExecutedPaths(), + 'numExecutablePaths' => $node->numberOfExecutablePaths(), + 'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(), + 'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(), + 'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(), + 'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(), + ]; + + if ($total) { + $data['name'] = 'Total'; + } else { + $up = str_repeat('../', count($node->pathAsArray()) - 2); + $data['icon'] = sprintf('', $up); + + if ($node instanceof DirectoryNode) { + $data['name'] = sprintf( + '%s', + $node->name(), + $node->name(), + ); + $data['icon'] = sprintf('', $up); + } elseif ($this->hasBranchCoverage) { + $data['name'] = sprintf( + '%s [line] [branch] [path]', + $node->name(), + $node->name(), + $node->name(), + $node->name(), + ); + } else { + $data['name'] = sprintf( + '%s', + $node->name(), + $node->name(), + ); + } + } + + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html'); + + return $this->renderItemTemplate( + new Template($templateName, '{{', '}}'), + $data, + ); + } +} diff --git a/src/Report/Html/Renderer/File.php b/src/Report/Html/Renderer/File.php new file mode 100644 index 000000000..09dbe31fe --- /dev/null +++ b/src/Report/Html/Renderer/File.php @@ -0,0 +1,1163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report\Html; + +use const ENT_COMPAT; +use const ENT_HTML401; +use const ENT_SUBSTITUTE; +use const T_ABSTRACT; +use const T_ARRAY; +use const T_AS; +use const T_BREAK; +use const T_CALLABLE; +use const T_CASE; +use const T_CATCH; +use const T_CLASS; +use const T_CLONE; +use const T_COMMENT; +use const T_CONST; +use const T_CONTINUE; +use const T_DECLARE; +use const T_DEFAULT; +use const T_DO; +use const T_DOC_COMMENT; +use const T_ECHO; +use const T_ELSE; +use const T_ELSEIF; +use const T_EMPTY; +use const T_ENDDECLARE; +use const T_ENDFOR; +use const T_ENDFOREACH; +use const T_ENDIF; +use const T_ENDSWITCH; +use const T_ENDWHILE; +use const T_ENUM; +use const T_EVAL; +use const T_EXIT; +use const T_EXTENDS; +use const T_FINAL; +use const T_FINALLY; +use const T_FN; +use const T_FOR; +use const T_FOREACH; +use const T_FUNCTION; +use const T_GLOBAL; +use const T_GOTO; +use const T_HALT_COMPILER; +use const T_IF; +use const T_IMPLEMENTS; +use const T_INCLUDE; +use const T_INCLUDE_ONCE; +use const T_INLINE_HTML; +use const T_INSTANCEOF; +use const T_INSTEADOF; +use const T_INTERFACE; +use const T_ISSET; +use const T_LIST; +use const T_MATCH; +use const T_NAMESPACE; +use const T_NEW; +use const T_PRINT; +use const T_PRIVATE; +use const T_PROTECTED; +use const T_PUBLIC; +use const T_READONLY; +use const T_REQUIRE; +use const T_REQUIRE_ONCE; +use const T_RETURN; +use const T_STATIC; +use const T_SWITCH; +use const T_THROW; +use const T_TRAIT; +use const T_TRY; +use const T_UNSET; +use const T_USE; +use const T_VAR; +use const T_WHILE; +use const T_YIELD; +use const T_YIELD_FROM; +use const TOKEN_PARSE; +use function array_key_exists; +use function array_keys; +use function array_merge; +use function array_pop; +use function array_unique; +use function count; +use function explode; +use function file_get_contents; +use function htmlspecialchars; +use function is_string; +use function ksort; +use function range; +use function sort; +use function sprintf; +use function str_ends_with; +use function str_replace; +use function token_get_all; +use function trim; +use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; +use SebastianBergmann\CodeCoverage\Node\File as FileNode; +use SebastianBergmann\CodeCoverage\Util\Percentage; +use SebastianBergmann\Template\Exception; +use SebastianBergmann\Template\Template; + +/** + * @phpstan-import-type ProcessedClassType from FileNode + * @phpstan-import-type ProcessedTraitType from FileNode + * @phpstan-import-type ProcessedMethodType from FileNode + * @phpstan-import-type ProcessedFunctionType from FileNode + * + * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage + */ +final class File extends Renderer +{ + /** + * @var array + */ + private const array KEYWORD_TOKENS = [ + T_ABSTRACT => true, + T_ARRAY => true, + T_AS => true, + T_BREAK => true, + T_CALLABLE => true, + T_CASE => true, + T_CATCH => true, + T_CLASS => true, + T_CLONE => true, + T_CONST => true, + T_CONTINUE => true, + T_DECLARE => true, + T_DEFAULT => true, + T_DO => true, + T_ECHO => true, + T_ELSE => true, + T_ELSEIF => true, + T_EMPTY => true, + T_ENDDECLARE => true, + T_ENDFOR => true, + T_ENDFOREACH => true, + T_ENDIF => true, + T_ENDSWITCH => true, + T_ENDWHILE => true, + T_ENUM => true, + T_EVAL => true, + T_EXIT => true, + T_EXTENDS => true, + T_FINAL => true, + T_FINALLY => true, + T_FN => true, + T_FOR => true, + T_FOREACH => true, + T_FUNCTION => true, + T_GLOBAL => true, + T_GOTO => true, + T_HALT_COMPILER => true, + T_IF => true, + T_IMPLEMENTS => true, + T_INCLUDE => true, + T_INCLUDE_ONCE => true, + T_INSTANCEOF => true, + T_INSTEADOF => true, + T_INTERFACE => true, + T_ISSET => true, + T_LIST => true, + T_MATCH => true, + T_NAMESPACE => true, + T_NEW => true, + T_PRINT => true, + T_PRIVATE => true, + T_PROTECTED => true, + T_PUBLIC => true, + T_READONLY => true, + T_REQUIRE => true, + T_REQUIRE_ONCE => true, + T_RETURN => true, + T_STATIC => true, + T_SWITCH => true, + T_THROW => true, + T_TRAIT => true, + T_TRY => true, + T_UNSET => true, + T_USE => true, + T_VAR => true, + T_WHILE => true, + T_YIELD => true, + T_YIELD_FROM => true, + ]; + + private const int HTML_SPECIAL_CHARS_FLAGS = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE; + + /** + * @var array> + */ + private static array $formattedSourceCache = []; + + public function render(FileNode $node, string $file): void + { + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_branch.html' : 'file.html'); + $template = new Template($templateName, '{{', '}}'); + $this->setCommonTemplateVariables($template, $node); + + $template->setVar( + [ + 'items' => $this->renderItems($node), + 'lines' => $this->renderSourceWithLineCoverage($node), + 'legend' => '

    Covered by small (and larger) testsCovered by medium (and large) testsCovered by large tests (and tests of unknown size)Not coveredNot coverable

    ', + 'structure' => '', + ], + ); + + try { + $template->renderTo($file . '.html'); + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + + if ($this->hasBranchCoverage) { + $template->setVar( + [ + 'items' => $this->renderItems($node), + 'lines' => $this->renderSourceWithBranchCoverage($node), + 'legend' => '

    Fully coveredPartially coveredNot covered

    ', + 'structure' => $this->renderBranchStructure($node), + ], + ); + + try { + $template->renderTo($file . '_branch.html'); + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + + $template->setVar( + [ + 'items' => $this->renderItems($node), + 'lines' => $this->renderSourceWithPathCoverage($node), + 'legend' => '

    Fully coveredPartially coveredNot covered

    ', + 'structure' => $this->renderPathStructure($node), + ], + ); + + try { + $template->renderTo($file . '_path.html'); + } catch (Exception $e) { + throw new FileCouldNotBeWrittenException( + $e->getMessage(), + $e->getCode(), + $e, + ); + } + } + } + + private function renderItems(FileNode $node): string + { + $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_item_branch.html' : 'file_item.html'); + $template = new Template($templateName, '{{', '}}'); + + $methodTemplateName = $this->templatePath . ($this->hasBranchCoverage ? 'method_item_branch.html' : 'method_item.html'); + $methodItemTemplate = new Template( + $methodTemplateName, + '{{', + '}}', + ); + + $items = $this->renderItemTemplate( + $template, + [ + 'name' => 'Total', + 'numClasses' => $node->numberOfClassesAndTraits(), + 'numTestedClasses' => $node->numberOfTestedClassesAndTraits(), + 'numMethods' => $node->numberOfFunctionsAndMethods(), + 'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(), + 'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(), + 'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(), + 'numExecutedLines' => $node->numberOfExecutedLines(), + 'numExecutableLines' => $node->numberOfExecutableLines(), + 'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(), + 'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(), + 'numExecutedBranches' => $node->numberOfExecutedBranches(), + 'numExecutableBranches' => $node->numberOfExecutableBranches(), + 'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(), + 'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(), + 'numExecutedPaths' => $node->numberOfExecutedPaths(), + 'numExecutablePaths' => $node->numberOfExecutablePaths(), + 'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(), + 'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(), + 'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(), + 'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(), + 'crap' => 'CRAP', + ], + ); + + $items .= $this->renderFunctionItems( + $node->functions(), + $methodItemTemplate, + ); + + $items .= $this->renderTraitOrClassItems( + $node->traits(), + $template, + $methodItemTemplate, + ); + + $items .= $this->renderTraitOrClassItems( + $node->classes(), + $template, + $methodItemTemplate, + ); + + return $items; + } + + /** + * @param array $items + */ + private function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string + { + $buffer = ''; + + if ($items === []) { + return $buffer; + } + + foreach ($items as $name => $item) { + $numMethods = 0; + $numTestedMethods = 0; + + foreach ($item['methods'] as $method) { + if ($method['executableLines'] > 0) { + $numMethods++; + + if ($method['executedLines'] === $method['executableLines']) { + $numTestedMethods++; + } + } + } + + if ($item['executableLines'] > 0) { + $numClasses = 1; + $numTestedClasses = $numTestedMethods === $numMethods ? 1 : 0; + $linesExecutedPercentAsString = Percentage::fromFractionAndTotal( + $item['executedLines'], + $item['executableLines'], + )->asString(); + $branchesExecutedPercentAsString = Percentage::fromFractionAndTotal( + $item['executedBranches'], + $item['executableBranches'], + )->asString(); + $pathsExecutedPercentAsString = Percentage::fromFractionAndTotal( + $item['executedPaths'], + $item['executablePaths'], + )->asString(); + } else { + $numClasses = 0; + $numTestedClasses = 0; + $linesExecutedPercentAsString = 'n/a'; + $branchesExecutedPercentAsString = 'n/a'; + $pathsExecutedPercentAsString = 'n/a'; + } + + $testedMethodsPercentage = Percentage::fromFractionAndTotal( + $numTestedMethods, + $numMethods, + ); + + $testedClassesPercentage = Percentage::fromFractionAndTotal( + $numTestedMethods === $numMethods ? 1 : 0, + 1, + ); + + $buffer .= $this->renderItemTemplate( + $template, + [ + 'name' => $this->abbreviateClassName($name), + 'numClasses' => $numClasses, + 'numTestedClasses' => $numTestedClasses, + 'numMethods' => $numMethods, + 'numTestedMethods' => $numTestedMethods, + 'linesExecutedPercent' => Percentage::fromFractionAndTotal( + $item['executedLines'], + $item['executableLines'], + )->asFloat(), + 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, + 'numExecutedLines' => $item['executedLines'], + 'numExecutableLines' => $item['executableLines'], + 'branchesExecutedPercent' => Percentage::fromFractionAndTotal( + $item['executedBranches'], + $item['executableBranches'], + )->asFloat(), + 'branchesExecutedPercentAsString' => $branchesExecutedPercentAsString, + 'numExecutedBranches' => $item['executedBranches'], + 'numExecutableBranches' => $item['executableBranches'], + 'pathsExecutedPercent' => Percentage::fromFractionAndTotal( + $item['executedPaths'], + $item['executablePaths'], + )->asFloat(), + 'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString, + 'numExecutedPaths' => $item['executedPaths'], + 'numExecutablePaths' => $item['executablePaths'], + 'testedMethodsPercent' => $testedMethodsPercentage->asFloat(), + 'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(), + 'testedClassesPercent' => $testedClassesPercentage->asFloat(), + 'testedClassesPercentAsString' => $testedClassesPercentage->asString(), + 'crap' => $item['crap'], + ], + ); + + foreach ($item['methods'] as $method) { + $buffer .= $this->renderFunctionOrMethodItem( + $methodItemTemplate, + $method, + ' ', + ); + } + } + + return $buffer; + } + + /** + * @param array $functions + */ + private function renderFunctionItems(array $functions, Template $template): string + { + if ($functions === []) { + return ''; + } + + $buffer = ''; + + foreach ($functions as $function) { + $buffer .= $this->renderFunctionOrMethodItem( + $template, + $function, + ); + } + + return $buffer; + } + + /** + * @param ProcessedFunctionType|ProcessedMethodType $item + */ + private function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string + { + $numMethods = 0; + $numTestedMethods = 0; + + if ($item['executableLines'] > 0) { + $numMethods = 1; + + if ($item['executedLines'] === $item['executableLines']) { + $numTestedMethods = 1; + } + } + + $executedLinesPercentage = Percentage::fromFractionAndTotal( + $item['executedLines'], + $item['executableLines'], + ); + + $executedBranchesPercentage = Percentage::fromFractionAndTotal( + $item['executedBranches'], + $item['executableBranches'], + ); + + $executedPathsPercentage = Percentage::fromFractionAndTotal( + $item['executedPaths'], + $item['executablePaths'], + ); + + $testedMethodsPercentage = Percentage::fromFractionAndTotal( + $numTestedMethods, + 1, + ); + + return $this->renderItemTemplate( + $template, + [ + 'name' => sprintf( + '%s%s', + $indent, + $item['startLine'], + htmlspecialchars($item['signature'], self::HTML_SPECIAL_CHARS_FLAGS), + $item['functionName'] ?? $item['methodName'], + ), + 'numMethods' => $numMethods, + 'numTestedMethods' => $numTestedMethods, + 'linesExecutedPercent' => $executedLinesPercentage->asFloat(), + 'linesExecutedPercentAsString' => $executedLinesPercentage->asString(), + 'numExecutedLines' => $item['executedLines'], + 'numExecutableLines' => $item['executableLines'], + 'branchesExecutedPercent' => $executedBranchesPercentage->asFloat(), + 'branchesExecutedPercentAsString' => $executedBranchesPercentage->asString(), + 'numExecutedBranches' => $item['executedBranches'], + 'numExecutableBranches' => $item['executableBranches'], + 'pathsExecutedPercent' => $executedPathsPercentage->asFloat(), + 'pathsExecutedPercentAsString' => $executedPathsPercentage->asString(), + 'numExecutedPaths' => $item['executedPaths'], + 'numExecutablePaths' => $item['executablePaths'], + 'testedMethodsPercent' => $testedMethodsPercentage->asFloat(), + 'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(), + 'crap' => $item['crap'], + ], + ); + } + + private function renderSourceWithLineCoverage(FileNode $node): string + { + $linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}'); + $singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}'); + + $coverageData = $node->lineCoverageData(); + $testData = $node->testData(); + $codeLines = $this->loadFile($node->pathAsString()); + $lines = ''; + $i = 1; + + foreach ($codeLines as $line) { + $trClass = ''; + $popoverContent = ''; + $popoverTitle = ''; + + if (array_key_exists($i, $coverageData)) { + $numTests = ($coverageData[$i] !== null ? count($coverageData[$i]) : 0); + + if ($coverageData[$i] === null) { + $trClass = 'warning'; + } elseif ($numTests === 0) { + $trClass = 'danger'; + } else { + if ($numTests > 1) { + $popoverTitle = $numTests . ' tests cover line ' . $i; + } else { + $popoverTitle = '1 test covers line ' . $i; + } + + $lineCss = 'covered-by-large-tests'; + $popoverContent = '
      '; + + foreach ($coverageData[$i] as $test) { + if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') { + $lineCss = 'covered-by-medium-tests'; + } elseif ($testData[$test]['size'] === 'small') { + $lineCss = 'covered-by-small-tests'; + } + + $popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]); + } + + $popoverContent .= '
    '; + $trClass = $lineCss . ' popin'; + } + } + + $popover = ''; + + if ($popoverTitle !== '') { + $popover = sprintf( + ' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"', + $popoverTitle, + htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS), + ); + } + + $lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover); + + $i++; + } + + $linesTemplate->setVar(['lines' => $lines]); + + return $linesTemplate->render(); + } + + private function renderSourceWithBranchCoverage(FileNode $node): string + { + $linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}'); + $singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}'); + + $functionCoverageData = $node->functionCoverageData(); + $testData = $node->testData(); + $codeLines = $this->loadFile($node->pathAsString()); + + $lineData = []; + + foreach (array_keys($codeLines) as $line) { + $lineData[$line + 1] = [ + 'includedInBranches' => 0, + 'includedInHitBranches' => 0, + 'tests' => [], + ]; + } + + foreach ($functionCoverageData as $method) { + foreach ($method['branches'] as $branch) { + foreach (range($branch['line_start'], $branch['line_end']) as $line) { + if (!isset($lineData[$line])) { // blank line at end of file is sometimes included here + continue; + } + + $lineData[$line]['includedInBranches']++; + + if ($branch['hit']) { + $lineData[$line]['includedInHitBranches']++; + $lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $branch['hit'])); + } + } + } + } + + $lines = ''; + $i = 1; + + /** @var string $line */ + foreach ($codeLines as $line) { + $trClass = ''; + $popover = ''; + + if ($lineData[$i]['includedInBranches'] > 0) { + $lineCss = 'success'; + + if ($lineData[$i]['includedInHitBranches'] === 0) { + $lineCss = 'danger'; + } elseif ($lineData[$i]['includedInHitBranches'] !== $lineData[$i]['includedInBranches']) { + $lineCss = 'warning'; + } + + $popoverContent = '
      '; + + if (count($lineData[$i]['tests']) === 1) { + $popoverTitle = '1 test covers line ' . $i; + } else { + $popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i; + } + $popoverTitle .= '. These are covering ' . $lineData[$i]['includedInHitBranches'] . ' out of the ' . $lineData[$i]['includedInBranches'] . ' code branches.'; + + foreach ($lineData[$i]['tests'] as $test) { + $popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]); + } + + $popoverContent .= '
    '; + $trClass = $lineCss . ' popin'; + + $popover = sprintf( + ' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"', + $popoverTitle, + htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS), + ); + } + + $lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover); + + $i++; + } + + $linesTemplate->setVar(['lines' => $lines]); + + return $linesTemplate->render(); + } + + private function renderSourceWithPathCoverage(FileNode $node): string + { + $linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}'); + $singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}'); + + $functionCoverageData = $node->functionCoverageData(); + $testData = $node->testData(); + $codeLines = $this->loadFile($node->pathAsString()); + + $lineData = []; + + foreach (array_keys($codeLines) as $line) { + $lineData[$line + 1] = [ + 'includedInPaths' => [], + 'includedInHitPaths' => [], + 'tests' => [], + ]; + } + + foreach ($functionCoverageData as $method) { + foreach ($method['paths'] as $pathId => $path) { + foreach ($path['path'] as $branchTaken) { + foreach (range($method['branches'][$branchTaken]['line_start'], $method['branches'][$branchTaken]['line_end']) as $line) { + if (!isset($lineData[$line])) { + continue; + } + $lineData[$line]['includedInPaths'][] = $pathId; + + if ($path['hit']) { + $lineData[$line]['includedInHitPaths'][] = $pathId; + $lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $path['hit'])); + } + } + } + } + } + + $lines = ''; + $i = 1; + + /** @var string $line */ + foreach ($codeLines as $line) { + $trClass = ''; + $popover = ''; + $includedInPathsCount = count(array_unique($lineData[$i]['includedInPaths'])); + $includedInHitPathsCount = count(array_unique($lineData[$i]['includedInHitPaths'])); + + if ($includedInPathsCount > 0) { + $lineCss = 'success'; + + if ($includedInHitPathsCount === 0) { + $lineCss = 'danger'; + } elseif ($includedInHitPathsCount !== $includedInPathsCount) { + $lineCss = 'warning'; + } + + $popoverContent = '
      '; + + if (count($lineData[$i]['tests']) === 1) { + $popoverTitle = '1 test covers line ' . $i; + } else { + $popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i; + } + $popoverTitle .= '. These are covering ' . $includedInHitPathsCount . ' out of the ' . $includedInPathsCount . ' code paths.'; + + foreach ($lineData[$i]['tests'] as $test) { + $popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]); + } + + $popoverContent .= '
    '; + $trClass = $lineCss . ' popin'; + + $popover = sprintf( + ' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"', + $popoverTitle, + htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS), + ); + } + + $lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover); + + $i++; + } + + $linesTemplate->setVar(['lines' => $lines]); + + return $linesTemplate->render(); + } + + private function renderBranchStructure(FileNode $node): string + { + $branchesTemplate = new Template($this->templatePath . 'branches.html.dist', '{{', '}}'); + + $coverageData = $node->functionCoverageData(); + $testData = $node->testData(); + $codeLines = $this->loadFile($node->pathAsString()); + $branches = ''; + + ksort($coverageData); + + foreach ($coverageData as $methodName => $methodData) { + if (!$methodData['branches']) { + continue; + } + + $branchStructure = ''; + + foreach ($methodData['branches'] as $branch) { + $branchStructure .= $this->renderBranchLines($branch, $codeLines, $testData); + } + + if ($branchStructure !== '') { // don't show empty branches + $branches .= '
    ' . $this->abbreviateMethodName($methodName) . '
    ' . "\n"; + $branches .= $branchStructure; + } + } + + $branchesTemplate->setVar(['branches' => $branches]); + + return $branchesTemplate->render(); + } + + /** + * @param list $codeLines + */ + private function renderBranchLines(array $branch, array $codeLines, array $testData): string + { + $linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}'); + $singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}'); + + $lines = ''; + + $branchLines = range($branch['line_start'], $branch['line_end']); + sort($branchLines); // sometimes end_line < start_line + + /** @var int $line */ + foreach ($branchLines as $line) { + if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included here + continue; + } + + $popoverContent = ''; + $popoverTitle = ''; + + $numTests = count($branch['hit']); + + if ($numTests === 0) { + $trClass = 'danger'; + } else { + $lineCss = 'covered-by-large-tests'; + $popoverContent = '