From 7c7fe5ca803b744f610a84c667219a8badf927d0 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 30 Apr 2026 09:56:57 -0500 Subject: [PATCH] Initial import from aternosorg/codex-minecraft --- .github/workflows/tests.yaml | 43 + .gitignore | 3 + LICENSE | 19 + README.md | 115 ++ composer.json | 32 + composer.lock | 1695 +++++++++++++++++ phpunit.xml | 19 + src/Analyser/Analyser.php | 27 + src/Analyser/AnalyserInterface.php | 29 + src/Analyser/PatternAnalyser.php | 159 ++ src/Analysis/Analysis.php | 238 +++ src/Analysis/AnalysisInterface.php | 77 + src/Analysis/AutomatableSolutionInterface.php | 17 + src/Analysis/Information.php | 90 + src/Analysis/InformationInterface.php | 33 + src/Analysis/Insight.php | 119 ++ src/Analysis/InsightInterface.php | 78 + src/Analysis/PatternInsightInterface.php | 29 + src/Analysis/Problem.php | 168 ++ src/Analysis/ProblemInterface.php | 38 + src/Analysis/Solution.php | 29 + src/Analysis/SolutionInterface.php | 21 + src/Detective/Detective.php | 149 ++ src/Detective/DetectiveInterface.php | 56 + src/Detective/Detector.php | 47 + src/Detective/DetectorInterface.php | 38 + src/Detective/LinePatternDetector.php | 37 + src/Detective/MultiPatternDetector.php | 41 + src/Detective/PatternDetector.php | 25 + src/Detective/SinglePatternDetector.php | 29 + .../WeightedSinglePatternDetector.php | 43 + src/Log/AnalysableLog.php | 50 + src/Log/AnalysableLogInterface.php | 33 + src/Log/DetectableLogInterface.php | 20 + src/Log/Entry.php | 244 +++ src/Log/EntryInterface.php | 67 + src/Log/File/LogFile.php | 23 + src/Log/File/LogFileInterface.php | 18 + src/Log/File/PathLogFile.php | 27 + src/Log/File/StreamLogFile.php | 30 + src/Log/File/StringLogFile.php | 21 + src/Log/Level.php | 66 + src/Log/LevelInterface.php | 24 + src/Log/Line.php | 84 + src/Log/LineInterface.php | 48 + src/Log/Log.php | 253 +++ src/Log/LogInterface.php | 110 ++ src/Parser/DefaultParser.php | 26 + src/Parser/Parser.php | 47 + src/Parser/ParserInterface.php | 26 + src/Parser/PatternParser.php | 183 ++ src/Printer/DefaultPrinter.php | 24 + src/Printer/ModifiableDefaultPrinter.php | 24 + src/Printer/ModifiablePrinter.php | 59 + src/Printer/ModifiablePrinterInterface.php | 27 + src/Printer/Modification.php | 13 + src/Printer/ModificationInterface.php | 19 + src/Printer/PatternModification.php | 60 + src/Printer/Printer.php | 99 + src/Printer/PrinterInterface.php | 37 + test/data/problem.log | 8 + test/data/simple.log | 7 + test/src/Analysis/TestInformation.php | 13 + test/src/Analysis/TestInsight.php | 33 + test/src/Analysis/TestPatternInformation.php | 43 + test/src/Analysis/TestPatternProblem.php | 70 + test/src/Analysis/TestProblem.php | 33 + test/src/Analysis/TestSolution.php | 21 + test/src/Log/TestAlwaysDetectableLog.php | 24 + test/src/Log/TestLessDetectableLog.php | 24 + test/src/Log/TestMoreDetectableLog.php | 24 + test/src/Log/TestNeverDetectableLog.php | 24 + test/src/Log/TestPatternLog.php | 40 + test/src/Printer/TestModification.php | 22 + test/tests/Analyser/PatternAnalyserTest.php | 177 ++ test/tests/Analysis/AnalysisTest.php | 157 ++ test/tests/Analysis/InformationTest.php | 61 + test/tests/Analysis/ProblemTest.php | 113 ++ test/tests/Detective/DetectiveTest.php | 122 ++ .../Detective/LinePatternDetectorTest.php | 30 + .../Detective/MultiPatternDetectorTest.php | 40 + .../Detective/SinglePatternDetectorTest.php | 25 + .../WeightedSinglePatternDetectorTest.php | 40 + test/tests/Log/EntryTest.php | 138 ++ test/tests/Log/File/PathLogFileTest.php | 17 + test/tests/Log/File/StreamLogFileTest.php | 18 + test/tests/Log/File/StringLogFileTest.php | 17 + test/tests/Log/LineTest.php | 33 + test/tests/Log/LogTest.php | 103 + test/tests/Parser/DefaultParserTest.php | 47 + test/tests/Parser/PatternParserTest.php | 76 + test/tests/Printer/DefaultPrinterTest.php | 35 + .../Printer/ModifiableDefaultPrinterTest.php | 38 + .../tests/Printer/PatternModificationTest.php | 25 + 94 files changed, 7003 insertions(+) create mode 100644 .github/workflows/tests.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 phpunit.xml create mode 100644 src/Analyser/Analyser.php create mode 100644 src/Analyser/AnalyserInterface.php create mode 100644 src/Analyser/PatternAnalyser.php create mode 100644 src/Analysis/Analysis.php create mode 100644 src/Analysis/AnalysisInterface.php create mode 100644 src/Analysis/AutomatableSolutionInterface.php create mode 100644 src/Analysis/Information.php create mode 100644 src/Analysis/InformationInterface.php create mode 100644 src/Analysis/Insight.php create mode 100644 src/Analysis/InsightInterface.php create mode 100644 src/Analysis/PatternInsightInterface.php create mode 100644 src/Analysis/Problem.php create mode 100644 src/Analysis/ProblemInterface.php create mode 100644 src/Analysis/Solution.php create mode 100644 src/Analysis/SolutionInterface.php create mode 100644 src/Detective/Detective.php create mode 100644 src/Detective/DetectiveInterface.php create mode 100644 src/Detective/Detector.php create mode 100644 src/Detective/DetectorInterface.php create mode 100644 src/Detective/LinePatternDetector.php create mode 100644 src/Detective/MultiPatternDetector.php create mode 100644 src/Detective/PatternDetector.php create mode 100644 src/Detective/SinglePatternDetector.php create mode 100644 src/Detective/WeightedSinglePatternDetector.php create mode 100644 src/Log/AnalysableLog.php create mode 100644 src/Log/AnalysableLogInterface.php create mode 100644 src/Log/DetectableLogInterface.php create mode 100644 src/Log/Entry.php create mode 100644 src/Log/EntryInterface.php create mode 100644 src/Log/File/LogFile.php create mode 100644 src/Log/File/LogFileInterface.php create mode 100644 src/Log/File/PathLogFile.php create mode 100644 src/Log/File/StreamLogFile.php create mode 100644 src/Log/File/StringLogFile.php create mode 100644 src/Log/Level.php create mode 100644 src/Log/LevelInterface.php create mode 100644 src/Log/Line.php create mode 100644 src/Log/LineInterface.php create mode 100644 src/Log/Log.php create mode 100644 src/Log/LogInterface.php create mode 100644 src/Parser/DefaultParser.php create mode 100644 src/Parser/Parser.php create mode 100644 src/Parser/ParserInterface.php create mode 100644 src/Parser/PatternParser.php create mode 100644 src/Printer/DefaultPrinter.php create mode 100644 src/Printer/ModifiableDefaultPrinter.php create mode 100644 src/Printer/ModifiablePrinter.php create mode 100644 src/Printer/ModifiablePrinterInterface.php create mode 100644 src/Printer/Modification.php create mode 100644 src/Printer/ModificationInterface.php create mode 100644 src/Printer/PatternModification.php create mode 100644 src/Printer/Printer.php create mode 100644 src/Printer/PrinterInterface.php create mode 100644 test/data/problem.log create mode 100644 test/data/simple.log create mode 100644 test/src/Analysis/TestInformation.php create mode 100644 test/src/Analysis/TestInsight.php create mode 100644 test/src/Analysis/TestPatternInformation.php create mode 100644 test/src/Analysis/TestPatternProblem.php create mode 100644 test/src/Analysis/TestProblem.php create mode 100644 test/src/Analysis/TestSolution.php create mode 100644 test/src/Log/TestAlwaysDetectableLog.php create mode 100644 test/src/Log/TestLessDetectableLog.php create mode 100644 test/src/Log/TestMoreDetectableLog.php create mode 100644 test/src/Log/TestNeverDetectableLog.php create mode 100644 test/src/Log/TestPatternLog.php create mode 100644 test/src/Printer/TestModification.php create mode 100644 test/tests/Analyser/PatternAnalyserTest.php create mode 100644 test/tests/Analysis/AnalysisTest.php create mode 100644 test/tests/Analysis/InformationTest.php create mode 100644 test/tests/Analysis/ProblemTest.php create mode 100644 test/tests/Detective/DetectiveTest.php create mode 100644 test/tests/Detective/LinePatternDetectorTest.php create mode 100644 test/tests/Detective/MultiPatternDetectorTest.php create mode 100644 test/tests/Detective/SinglePatternDetectorTest.php create mode 100644 test/tests/Detective/WeightedSinglePatternDetectorTest.php create mode 100644 test/tests/Log/EntryTest.php create mode 100644 test/tests/Log/File/PathLogFileTest.php create mode 100644 test/tests/Log/File/StreamLogFileTest.php create mode 100644 test/tests/Log/File/StringLogFileTest.php create mode 100644 test/tests/Log/LineTest.php create mode 100644 test/tests/Log/LogTest.php create mode 100644 test/tests/Parser/DefaultParserTest.php create mode 100644 test/tests/Parser/PatternParserTest.php create mode 100644 test/tests/Printer/DefaultPrinterTest.php create mode 100644 test/tests/Printer/ModifiableDefaultPrinterTest.php create mode 100644 test/tests/Printer/PatternModificationTest.php diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..0dde0a1 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,43 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: [ '8.4', '8.5' ] + + name: Run tests on PHP v${{ matrix.php-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - name: Set composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Restore composer from cache + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Run phpunit tests + run: vendor/bin/phpunit --testsuite tests --colors=always --testdox diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24265e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +vendor +.phpunit.result.cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64966c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-2026 Aternos GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..34b1ee5 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Codex + +### About + +Codex (*lat. roughly for "log"*) is a PHP library to read, parse, print and analyse log files to find problems and suggest possible +solutions. It was created mainly for Minecraft server logs but could be used for any other logs as well. This library provides a set +up for a structured log parsing implementation and provides some useful basic implementation, mainly based on RegEx. Every part +of this library can or even must be extended/overwritten while still following the interfaces, which ensure interoperability between +the different parts of this library. + +### Installation + +``` +composer require aternos/codex +``` + +## Usage + +This is a short introduction to the idea of Codex, for some more examples check the [test](test) folder +and/or read the [code](src). + +### Logfile + +A [`LogFile`](src/Log/File/LogFile.php) object implementing the [`LogFileInterface`](src/Log/File/LogFileInterface.php) object is required +to start reading a log. There are currently three different log file classes in this library. + +```php +setLogFile($logFile); +``` + +### Detection + +If the log type (specifically the class name of the log type) is unknown you can use the [`Detective`](src/Detective/Detective.php) class +to automatically detect the log type. The `Detective` class gets a list of possible log class names and executes +their given [Detectors](src/Detective/DetectorInterface.php). + +```php +addPossibleLogClass(\Aternos\Codex\Log\Log::class); +$log = $detective->detect(); +``` + +The `detect()` function always returns a log object, if necessary it defaults to [`Log`](src/Log/Log.php). + +### Parsing + +Parsing reads the entire log and creates the [`Entry`](src/Log/EntryInterface.php) and [`Line`](src/Log/LineInterface.php) objects which +are parts of a [`Log`](src/Log/LogInterface.php) object. Different log types can use different parsers by overwriting the +`LogInterface::getDefaultParser()` function or by passing a parser object to the parse function. + +```php +parse(); +``` + +### Analysing + +An analysis is performed by an [`Analyser`](src/Analyser/AnalyserInterface.php) on an [`AnalysableLog`](src/Log/AnalysableLogInterface.php) and returns +an [`Analysis`](src/Analysis/AnalysisInterface.php) object containing various [`Insight`](src/Analysis/InsightInterface.php) objects, e.g. a [`Problem`](src/Analysis/ProblemInterface.php) +or an [`Information`](src/Analysis/InformationInterface.php) object. Different log types can use different analysers by overwriting +the `AnalysableLogInterface::getDefaultAnalyser()` function or by passing an analyser object to the analyse function. + +```php +analyse(); +``` + +### Printing + +The entire [`Log`](src/Log/LogInterface.php) or just an [`Entry`](src/Log/EntryInterface.php) can be printed through a [`Printer`](src/Printer/PrinterInterface.php). The basic +[`DefaultPrinter`](src/Printer/DefaultPrinter.php) only prints the plain content line by line. The [`ModifiableDefaultPrinter`](src/Printer/ModifiableDefaultPrinter.php) +allows [`Modification`](src/Printer/ModificationInterface.php), e.g. to highlight certain characters/words. + +```php +setLog($log); +$printer->print(); + +$printer = new \Aternos\Codex\Printer\DefaultPrinter(); +$printer->setEntry($entry); +$printer->print(); + +$printer = new \Aternos\Codex\Printer\ModifiableDefaultPrinter(); +$printer->setLog($log); +$modification = new \Aternos\Codex\Printer\PatternModification(); +$modification->setPattern('/foo/'); +$modification->setReplacement('bar'); +$printer->addModification($modification); +$printer->print(); +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..05f5fc3 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "aternos/codex", + "description": "PHP library to read, parse, print and analyse log files.", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Matthias Neid", + "email": "matthias@aternos.org" + } + ], + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^12" + }, + "autoload": { + "psr-4": { + "Aternos\\Codex\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Aternos\\Codex\\Test\\Src\\": "test/src/", + "Aternos\\Codex\\Test\\Tests\\": "test/tests/" + } + }, + "scripts": { + "test": "phpunit test/tests" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..508eee4 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1695 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ed32720a5cfbfa3bbba1d5c565bb2966", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.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" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:03:04+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ab8e4374264bc65523d1458d14bf80261577e01f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ab8e4374264bc65523d1458d14bf80261577e01f", + "reference": "ab8e4374264bc65523d1458d14bf80261577e01f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.2", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-16T16:28:10+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-20T11:27:00+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-08-12T14:11:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..fe58448 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + test/tests/ + + + + + src + + + diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php new file mode 100644 index 0000000..800ea38 --- /dev/null +++ b/src/Analyser/Analyser.php @@ -0,0 +1,27 @@ +log = $log; + return $this; + } +} \ No newline at end of file diff --git a/src/Analyser/AnalyserInterface.php b/src/Analyser/AnalyserInterface.php new file mode 100644 index 0000000..f4f2dda --- /dev/null +++ b/src/Analyser/AnalyserInterface.php @@ -0,0 +1,29 @@ +[] + */ + protected array $possibleInsightClasses = []; + + /** + * Set possible insight classes + * + * Every class must implement PatternInsightInterface + * + * @param class-string[] $insightClasses + * @return $this + */ + public function setPossibleInsightClasses(array $insightClasses): static + { + $this->possibleInsightClasses = []; + foreach ($insightClasses as $insightClass) { + $this->addPossibleInsightClass($insightClass); + } + + return $this; + } + + /** + * Add a possible insight class + * + * The class must implement PatternInsightInterface + * + * @param class-string $insightClass + * @return $this + */ + public function addPossibleInsightClass(string $insightClass): static + { + if (!is_subclass_of($insightClass, PatternInsightInterface::class)) { + throw new InvalidArgumentException("Class " . $insightClass . " does not implement " . PatternInsightInterface::class . "."); + } + + $this->possibleInsightClasses[] = $insightClass; + return $this; + } + + /** + * Find a possible insight class + * + * @param class-string $insightClass + * @return int + */ + protected function findPossibleInsightClass(string $insightClass): int + { + $index = array_search($insightClass, $this->possibleInsightClasses); + if ($index === false) { + throw new InvalidArgumentException("Class " . $insightClass . " not found in possible insight classes."); + } + return $index; + } + + /** + * Remove a possible insight class + * + * @param class-string $insightClass + */ + public function removePossibleInsightClass(string $insightClass): void + { + $index = $this->findPossibleInsightClass($insightClass); + unset($this->possibleInsightClasses[$index]); + } + + /** + * Override a possible insight class with a child class + * + * The $childInsightClass has to extend $parentInsightClass + * + * @param class-string $parentInsightClass + * @param class-string $childInsightClass + */ + public function overridePossibleInsightClass(string $parentInsightClass, string $childInsightClass): void + { + if (!is_subclass_of($childInsightClass, $parentInsightClass)) { + throw new InvalidArgumentException("Class " . $childInsightClass . " does not extend " . $parentInsightClass . "."); + } + + $index = $this->findPossibleInsightClass($parentInsightClass); + $this->possibleInsightClasses[$index] = $childInsightClass; + } + + /** + * Analyse a log and return an Analysis + * + * @return AnalysisInterface + */ + public function analyse(): AnalysisInterface + { + $analysis = new Analysis(); + $analysis->setLog($this->log); + + foreach ($this->log as $entry) { + foreach ($this->possibleInsightClasses as $possibleInsightClass) { + /** @var PatternInsightInterface $possibleInsightClass */ + $patterns = $possibleInsightClass::getPatterns(); + foreach ($patterns as $patternKey => $pattern) { + $insights = $this->analyseEntry($entry, $possibleInsightClass, $patternKey, $pattern); + if ($insights) { + foreach ($insights as $insight) { + $analysis->addInsight($insight); + } + } + } + } + } + + return $analysis; + } + + /** + * Compare the entry against the given pattern and create an insight object if it matches + * + * @param EntryInterface $entry + * @param string $possibleInsightClass + * @param mixed $patternKey + * @param string $pattern + * @return null|PatternInsightInterface[] + */ + protected function analyseEntry(EntryInterface $entry, string $possibleInsightClass, mixed $patternKey, string $pattern): ?array + { + $result = preg_match_all($pattern, $entry, $matches, PREG_SET_ORDER); + if ($result === false || $result === 0) { + return null; + } + + $return = []; + foreach ($matches as $match) { + /** @var PatternInsightInterface $insight */ + $insight = new $possibleInsightClass(); + $insight->setMatches($match, $patternKey); + $insight->setEntry($entry); + + $return[] = $insight; + } + + return $return; + } +} \ No newline at end of file diff --git a/src/Analysis/Analysis.php b/src/Analysis/Analysis.php new file mode 100644 index 0000000..ab3e7d1 --- /dev/null +++ b/src/Analysis/Analysis.php @@ -0,0 +1,238 @@ +setAnalysis($this); + } + $this->insights = $insights; + return $this; + } + + /** + * Add an insight. + * If the insight already exists, we increase its counter. + * + * @param InsightInterface $insight + * @return $this + */ + public function addInsight(InsightInterface $insight): static + { + $insight->setAnalysis($this); + + foreach ($this as $existingInsight) { + if (get_class($insight) === get_class($existingInsight) && $existingInsight->isEqual($insight)) { + $existingInsight->increaseCounter(); + return $this; + } + } + + $this->insights[] = $insight; + return $this; + } + + /** + * Get all insights + * + * @return InsightInterface[] + */ + public function getInsights(): array + { + return $this->insights; + } + + /** + * Get all insights that are extended from $extendedFrom (class name) + * + * @param class-string $extendedFrom + * @return InsightInterface[] + */ + public function getFilteredInsights(string $extendedFrom): array + { + $returnInsights = []; + foreach ($this->getInsights() as $insight) { + if ($insight instanceof $extendedFrom) { + $returnInsights[] = $insight; + } + } + + return $returnInsights; + } + + /** + * Get all problem insights + * + * @return ProblemInterface[] + */ + public function getProblems(): array + { + return $this->getFilteredInsights(ProblemInterface::class); + } + + /** + * Get all information insights + * + * @return InformationInterface[] + */ + public function getInformation(): array + { + return $this->getFilteredInsights(InformationInterface::class); + } + + /** + * Return the current element + * + * @return InsightInterface + */ + public function current(): InsightInterface + { + return $this->insights[$this->iterator]; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next(): void + { + $this->iterator++; + } + + /** + * Return the key of the current element + * + * @return int + */ + public function key(): int + { + return $this->iterator; + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid(): bool + { + return array_key_exists($this->iterator, $this->insights); + } + + /** + * Rewind the Iterator to the first element + * + * @return void + */ + public function rewind(): void + { + $this->iterator = 0; + } + + /** + * Count elements of an object + * + * @return int + */ + public function count(): int + { + return count($this->insights); + } + + /** + * Whether an offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->insights[$offset]); + } + + /** + * Offset to retrieve + * + * @param mixed $offset + * @return InsightInterface + */ + public function offsetGet(mixed $offset): InsightInterface + { + return $this->insights[$offset]; + } + + /** + * Offset to set + * + * @param mixed $offset + * @param InsightInterface $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $value->setAnalysis($this); + $this->insights[$offset] = $value; + } + + /** + * Offset to unset + * + * @param mixed $offset + */ + public function offsetUnset(mixed $offset): void + { + unset($this->insights[$offset]); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + "problems" => $this->getProblems(), + "information" => $this->getInformation() + ]; + } + + /** + * @param LogInterface $log + * @return $this + */ + public function setLog(LogInterface $log): static + { + $this->log = $log; + return $this; + } + + /** + * @return LogInterface|null + */ + public function getLog(): ?LogInterface + { + return $this->log; + } +} \ No newline at end of file diff --git a/src/Analysis/AnalysisInterface.php b/src/Analysis/AnalysisInterface.php new file mode 100644 index 0000000..730f3bd --- /dev/null +++ b/src/Analysis/AnalysisInterface.php @@ -0,0 +1,77 @@ + $extendedFrom + * @return InsightInterface[] + */ + public function getFilteredInsights(string $extendedFrom): array; +} \ No newline at end of file diff --git a/src/Analysis/AutomatableSolutionInterface.php b/src/Analysis/AutomatableSolutionInterface.php new file mode 100644 index 0000000..daf530b --- /dev/null +++ b/src/Analysis/AutomatableSolutionInterface.php @@ -0,0 +1,17 @@ +label; + } + + /** + * Set the information label + * + * @param string $label + * @return $this + */ + protected function setLabel(string $label): static + { + $this->label = $label; + return $this; + } + + /** + * Get the information value + * + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * Set the information value + * + * @param mixed $value + * @return $this + */ + public function setValue(mixed $value): static + { + $this->value = $value; + return $this; + } + + /** + * Get a human-readable message + * + * @return string + */ + public function getMessage(): string + { + return $this->getLabel() . ": " . $this->getValue(); + } + + /** + * Check if the $insight object is equal with the current object + * + * @param InsightInterface $insight + * @return bool + */ + public function isEqual(InsightInterface $insight): bool + { + return $insight instanceof InformationInterface && $this->getLabel() === $insight->getLabel() && $this->getValue() === $insight->getValue(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return array_merge(parent::jsonSerialize(), [ + "label" => $this->getLabel(), + "value" => $this->getValue() + ]); + } +} \ No newline at end of file diff --git a/src/Analysis/InformationInterface.php b/src/Analysis/InformationInterface.php new file mode 100644 index 0000000..b270020 --- /dev/null +++ b/src/Analysis/InformationInterface.php @@ -0,0 +1,33 @@ +entry = $entry; + return $this; + } + + /** + * Get the related entry + * + * @return EntryInterface + */ + public function getEntry(): EntryInterface + { + return $this->entry; + } + + /** + * Increase the counter for this insight + * + * @return $this + */ + public function increaseCounter(): static + { + $this->counter++; + return $this; + } + + /** + * Get the current counter value + * + * @return int + */ + public function getCounterValue(): int + { + return $this->counter; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->getMessage(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'message' => $this->getMessage(), + 'counter' => $this->getCounterValue(), + 'entry' => $this->getEntry() + ]; + } + + /** + * Set the related analysis + * + * @param AnalysisInterface $analysis + * @return $this + */ + public function setAnalysis(AnalysisInterface $analysis): static + { + $this->analysis = $analysis; + return $this; + } + + /** + * Get the related analysis + * + * @return AnalysisInterface|null + */ + public function getAnalysis(): ?AnalysisInterface + { + return $this->analysis; + } + + /** + * @return LogInterface|null + */ + protected function getLog(): ?LogInterface + { + return $this->getAnalysis()?->getLog(); + } + + /** + * @return string|null + */ + protected function getLogContent(): ?string + { + return $this->getLog()?->getLogFile()?->getContent(); + } +} \ No newline at end of file diff --git a/src/Analysis/InsightInterface.php b/src/Analysis/InsightInterface.php new file mode 100644 index 0000000..5ee82b0 --- /dev/null +++ b/src/Analysis/InsightInterface.php @@ -0,0 +1,78 @@ +solutions = $solutions; + return $this; + } + + /** + * Add a solution + * + * @param SolutionInterface $solution + * @return $this + */ + public function addSolution(SolutionInterface $solution): static + { + $this->solutions[] = $solution; + return $this; + } + + /** + * Get all solutions + * + * @return array + */ + public function getSolutions(): array + { + return $this->solutions; + } + + /** + * Return the current element + * + * @return SolutionInterface + */ + public function current(): SolutionInterface + { + return $this->solutions[$this->iterator]; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next(): void + { + $this->iterator++; + } + + /** + * Return the key of the current element + * + * @return int + */ + public function key(): int + { + return $this->iterator; + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid(): bool + { + return array_key_exists($this->iterator, $this->solutions); + } + + /** + * Rewind the Iterator to the first element + * + * @return void + */ + public function rewind(): void + { + $this->iterator = 0; + } + + /** + * Count elements of an object + * + * @return int + */ + public function count(): int + { + return count($this->solutions); + } + + /** + * Whether an offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->solutions[$offset]); + } + + /** + * Offset to retrieve + * + * @param mixed $offset + * @return SolutionInterface + */ + public function offsetGet(mixed $offset): SolutionInterface + { + return $this->solutions[$offset]; + } + + /** + * Offset to set + * + * @param mixed $offset + * @param SolutionInterface $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->solutions[$offset] = $value; + } + + /** + * Offset to unset + * + * @param mixed $offset + */ + public function offsetUnset(mixed $offset): void + { + unset($this->solutions[$offset]); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return array_merge(parent::jsonSerialize(), [ + "solutions" => $this->getSolutions() + ]); + } +} \ No newline at end of file diff --git a/src/Analysis/ProblemInterface.php b/src/Analysis/ProblemInterface.php new file mode 100644 index 0000000..bbf6710 --- /dev/null +++ b/src/Analysis/ProblemInterface.php @@ -0,0 +1,38 @@ +getMessage(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'message' => $this->getMessage() + ]; + } +} \ No newline at end of file diff --git a/src/Analysis/SolutionInterface.php b/src/Analysis/SolutionInterface.php new file mode 100644 index 0000000..dce523d --- /dev/null +++ b/src/Analysis/SolutionInterface.php @@ -0,0 +1,21 @@ +[] + */ + protected array $possibleLogClasses = []; + + /** + * @var class-string + */ + protected string $defaultLogClass = Log::class; + + protected ?LogFileInterface $logFile = null; + + /** + * Set possible log classes + * + * Every class must implement DetectableLogInterface + * + * @param class-string[] $logClasses + * @return $this + */ + public function setPossibleLogClasses(array $logClasses): static + { + $this->possibleLogClasses = []; + foreach ($logClasses as $logClass) { + $this->addPossibleLogClass($logClass); + } + + return $this; + } + + /** + * Add a possible insight class + * + * The class must implement DetectableLogInterface + * + * @param class-string $logClass + * @return $this + */ + public function addPossibleLogClass(string $logClass): static + { + if (!is_subclass_of($logClass, DetectableLogInterface::class)) { + throw new InvalidArgumentException("Class " . $logClass . " does not implement " . DetectableLogInterface::class . "."); + } + + $this->possibleLogClasses[] = $logClass; + return $this; + } + + /** + * Add all possible log classes from another detective + * + * @param DetectiveInterface $detective + * @return $this + */ + public function addDetective(DetectiveInterface $detective): static + { + foreach ($detective->getPossibleLogClasses() as $logClass) { + $this->addPossibleLogClass($logClass); + } + return $this; + } + + /** + * @inheritDoc + */ + public function getPossibleLogClasses(): array + { + return $this->possibleLogClasses; + } + + /** + * Set the log file + * + * @param LogFileInterface $logFile + * @return $this + */ + public function setLogFile(LogFileInterface $logFile): static + { + $this->logFile = $logFile; + return $this; + } + + /** + * Detect a log type out of possible classes by using detector + * + * @return LogInterface + */ + public function detect(): LogInterface + { + $detectionResults = []; + foreach ($this->possibleLogClasses as $possibleLogClass) { + /** @var DetectableLogInterface $possibleLogClass */ + $detectors = $possibleLogClass::getDetectors(); + foreach ($detectors as $detector) { + if (!$detector instanceof DetectorInterface) { + throw new InvalidArgumentException("Class " . get_class($detector) . " does not implement " . DetectorInterface::class . "."); + } + + $detector->setLogFile($this->logFile); + $result = $detector->detect(); + if ($result === true) { + return (new $possibleLogClass())->setLogFile($this->logFile); + } + if ($result === false) { + continue; + } + if (!is_numeric($result) || $result < 0 || $result > 1) { + throw new InvalidArgumentException("Detector " . get_class($detector) . " returned " . var_export($result)); + } + $detectionResults[] = ["class" => $possibleLogClass, "result" => $result]; + } + } + + if (count($detectionResults) === 0) { + return (new $this->defaultLogClass())->setLogFile($this->logFile); + } + + usort($detectionResults, function ($a, $b) { + if ($a["result"] < $b["result"]) { + return 1; + } + + if ($a["result"] > $b["result"]) { + return -1; + } + + return 0; + }); + + return (new $detectionResults[0]["class"]())->setLogFile($this->logFile); + } +} \ No newline at end of file diff --git a/src/Detective/DetectiveInterface.php b/src/Detective/DetectiveInterface.php new file mode 100644 index 0000000..7a78368 --- /dev/null +++ b/src/Detective/DetectiveInterface.php @@ -0,0 +1,56 @@ +[] $logClasses + * @return $this + */ + public function setPossibleLogClasses(array $logClasses): static; + + /** + * Add a possible log class + * + * The class must implement DetectableLogInterface + * + * @param class-string $logClass + * @return $this + */ + public function addPossibleLogClass(string $logClass): static; + + /** + * Get all possible log classes + * + * @return class-string[] + */ + public function getPossibleLogClasses(): array; + + /** + * Set the log file + * + * @param LogFileInterface $logFile + * @return $this + */ + public function setLogFile(LogFileInterface $logFile): static; + + /** + * Detect a log type out of possible classes by using detector + * + * @return LogInterface + */ + public function detect(): LogInterface; +} \ No newline at end of file diff --git a/src/Detective/Detector.php b/src/Detective/Detector.php new file mode 100644 index 0000000..f46f722 --- /dev/null +++ b/src/Detective/Detector.php @@ -0,0 +1,47 @@ +logFile = $logFile; + return $this; + } + + /** + * Get the log content as string + * + * @return string + */ + protected function getLogContent(): string + { + return $this->logFile->getContent(); + } + + /** + * Get the log content as array split by line + * + * @return string[] + */ + protected function getLogContentAsArray(): array + { + return explode(PHP_EOL, $this->getLogContent()); + } +} \ No newline at end of file diff --git a/src/Detective/DetectorInterface.php b/src/Detective/DetectorInterface.php new file mode 100644 index 0000000..2fea8ce --- /dev/null +++ b/src/Detective/DetectorInterface.php @@ -0,0 +1,38 @@ +getLogContentAsArray(); + $matchingCounter = 0; + foreach ($lines as $line) { + if (preg_match($this->pattern, $line) === 1) { + $matchingCounter++; + } + } + + if ($matchingCounter === 0) { + return false; + } + + return $matchingCounter / count($lines); + } +} \ No newline at end of file diff --git a/src/Detective/MultiPatternDetector.php b/src/Detective/MultiPatternDetector.php new file mode 100644 index 0000000..04ea3dd --- /dev/null +++ b/src/Detective/MultiPatternDetector.php @@ -0,0 +1,41 @@ +patterns[] = $pattern; + return $this; + } + + /** + * Detects if the log matches all patterns + * + * Returns true if all patterns are found, false otherwise + * + * @return bool|float + */ + public function detect(): bool|float + { + foreach ($this->patterns as $pattern) { + if (preg_match($pattern, $this->getLogContent()) !== 1) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Detective/PatternDetector.php b/src/Detective/PatternDetector.php new file mode 100644 index 0000000..29e8ceb --- /dev/null +++ b/src/Detective/PatternDetector.php @@ -0,0 +1,25 @@ +pattern = $pattern; + return $this; + } +} \ No newline at end of file diff --git a/src/Detective/SinglePatternDetector.php b/src/Detective/SinglePatternDetector.php new file mode 100644 index 0000000..28ffc0a --- /dev/null +++ b/src/Detective/SinglePatternDetector.php @@ -0,0 +1,29 @@ +pattern, $this->getLogContent()) === 1) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/src/Detective/WeightedSinglePatternDetector.php b/src/Detective/WeightedSinglePatternDetector.php new file mode 100644 index 0000000..cc1da68 --- /dev/null +++ b/src/Detective/WeightedSinglePatternDetector.php @@ -0,0 +1,43 @@ +weight = $weight; + return $this; + } + + /** + * Detect if the log matches + * + * Checks if the pattern matches anywhere in the log file + * + * Returns either true or false + * + * @return bool|float + */ + public function detect(): bool|float + { + if (parent::detect()) { + return $this->weight; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/src/Log/AnalysableLog.php b/src/Log/AnalysableLog.php new file mode 100644 index 0000000..92ea131 --- /dev/null +++ b/src/Log/AnalysableLog.php @@ -0,0 +1,50 @@ +analysis !== null) { + return $this->analysis; + } + + if ($analyser === null) { + $analyser = static::getDefaultAnalyser(); + } + + $analyser->setLog($this); + return $this->analysis = $analyser->analyse(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return array_merge(parent::jsonSerialize(), [ + 'analysis' => $this->analyse() + ]); + } +} \ No newline at end of file diff --git a/src/Log/AnalysableLogInterface.php b/src/Log/AnalysableLogInterface.php new file mode 100644 index 0000000..34e9931 --- /dev/null +++ b/src/Log/AnalysableLogInterface.php @@ -0,0 +1,33 @@ +lines = $lines; + return $this; + } + + /** + * Add a line + * + * @param LineInterface $line + * @return $this + */ + public function addLine(LineInterface $line): static + { + $this->lines[] = $line; + return $this; + } + + /** + * Get all lines + * + * @return array + */ + public function getLines(): array + { + return $this->lines; + } + + /** + * Set the log level of the entry + * + * @param LevelInterface $level + * @return $this + */ + public function setLevel(LevelInterface $level): static + { + $this->level = $level; + return $this; + } + + /** + * Get the log level of the entry + * + * @return LevelInterface + */ + public function getLevel(): LevelInterface + { + return $this->level ?? Level::INFO; + } + + /** + * Set the timestamp of the entry + * + * @param int $time + * @return $this + */ + public function setTime(int $time): static + { + $this->time = $time; + return $this; + } + + /** + * Get the timestamp of the entry + * + * @return int|null + */ + public function getTime(): ?int + { + return $this->time; + } + + /** + * Set the prefix + * + * @param string $prefix + * @return $this + */ + public function setPrefix(string $prefix): static + { + $this->prefix = $prefix; + return $this; + } + + /** + * Get the prefix + * + * @return string|null + */ + public function getPrefix(): ?string + { + return $this->prefix; + } + + /** + * Return the current element + * + * @return Line + */ + public function current(): Line + { + return $this->lines[$this->iterator]; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next(): void + { + $this->iterator++; + } + + /** + * Return the key of the current element + * + * @return int + */ + public function key(): int + { + return $this->iterator; + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid(): bool + { + return array_key_exists($this->iterator, $this->lines); + } + + /** + * Rewind the Iterator to the first element + * + * @return void + */ + public function rewind(): void + { + $this->iterator = 0; + } + + /** + * Count elements of an object + * + * @return int + */ + public function count(): int + { + return count($this->lines); + } + + /** + * Whether an offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->lines[$offset]); + } + + /** + * Offset to retrieve + * + * @param mixed $offset + * @return LineInterface + */ + public function offsetGet(mixed $offset): LineInterface + { + return $this->lines[$offset]; + } + + /** + * Offset to set + * + * @param mixed $offset + * @param LineInterface $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->lines[$offset] = $value; + } + + /** + * Offset to unset + * + * @param mixed $offset + */ + public function offsetUnset(mixed $offset): void + { + unset($this->lines[$offset]); + } + + /** + * @return string + */ + public function __toString(): string + { + return implode("\n", $this->getLines()); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'level' => $this->getLevel(), + 'time' => $this->getTime(), + 'prefix' => $this->getPrefix(), + 'lines' => $this->getLines() + ]; + } +} \ No newline at end of file diff --git a/src/Log/EntryInterface.php b/src/Log/EntryInterface.php new file mode 100644 index 0000000..887b8fe --- /dev/null +++ b/src/Log/EntryInterface.php @@ -0,0 +1,67 @@ +content; + } +} \ No newline at end of file diff --git a/src/Log/File/LogFileInterface.php b/src/Log/File/LogFileInterface.php new file mode 100644 index 0000000..618809a --- /dev/null +++ b/src/Log/File/LogFileInterface.php @@ -0,0 +1,18 @@ +content = file_get_contents($path); + } +} \ No newline at end of file diff --git a/src/Log/File/StreamLogFile.php b/src/Log/File/StreamLogFile.php new file mode 100644 index 0000000..af65060 --- /dev/null +++ b/src/Log/File/StreamLogFile.php @@ -0,0 +1,30 @@ +content = ''; + while (!feof($streamResource)) { + $this->content .= fread($streamResource, 8192); + } + } +} \ No newline at end of file diff --git a/src/Log/File/StringLogFile.php b/src/Log/File/StringLogFile.php new file mode 100644 index 0000000..447d1d4 --- /dev/null +++ b/src/Log/File/StringLogFile.php @@ -0,0 +1,21 @@ +content = $string; + } +} \ No newline at end of file diff --git a/src/Log/Level.php b/src/Log/Level.php new file mode 100644 index 0000000..0dda093 --- /dev/null +++ b/src/Log/Level.php @@ -0,0 +1,66 @@ + Level::EMERGENCY, + "alert" => Level::ALERT, + "critical", "severe", "fatal" => Level::CRITICAL, + "error", "stderr" => Level::ERROR, + "warning", "warn" => Level::WARNING, + "notice", "fine" => Level::NOTICE, + "debug", "finer", "finest" => Level::DEBUG, + default => Level::INFO + }; + } + + /** + * @return string + */ + public function asString(): string + { + return match ($this) { + Level::EMERGENCY => "emergency", + Level::ALERT => "alert", + Level::CRITICAL => "critical", + Level::ERROR => "error", + Level::WARNING => "warning", + Level::NOTICE => "notice", + Level::INFO => "info", + Level::DEBUG => "debug" + }; + } + + /** + * @return int + */ + public function asInt(): int + { + return $this->value; + } + + /** + * @return int + */ + public function jsonSerialize(): int + { + return $this->value; + } +} diff --git a/src/Log/LevelInterface.php b/src/Log/LevelInterface.php new file mode 100644 index 0000000..8df993b --- /dev/null +++ b/src/Log/LevelInterface.php @@ -0,0 +1,24 @@ +text = $text; + return $this; + } + + /** + * Get the text of the line + * + * @return string + */ + public function getText(): string + { + return $this->text; + } + + /** + * Set the line number + * + * @param int $number + * @return $this + */ + public function setNumber(int $number): static + { + $this->number = $number; + return $this; + } + + /** + * Get the line number + * + * @return int + */ + public function getNumber(): int + { + return $this->number; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->getText(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'number' => $this->getNumber(), + 'content' => $this->getText() + ]; + } +} \ No newline at end of file diff --git a/src/Log/LineInterface.php b/src/Log/LineInterface.php new file mode 100644 index 0000000..12ac207 --- /dev/null +++ b/src/Log/LineInterface.php @@ -0,0 +1,48 @@ +logFile = $logFile; + return $this; + } + + /** + * Get the log file + * + * @return LogFileInterface + */ + public function getLogfile(): LogFileInterface + { + return $this->logFile; + } + + /** + * Parse a log file with a parser + * + * Every log type should have a default parser, + * but the $parser argument can be used to override + * the default parser + * + * @param ParserInterface|null $parser + * @return $this + */ + public function parse(?ParserInterface $parser = null): static + { + if ($parser === null) { + $parser = static::getDefaultParser(); + } + + $parser->setLog($this)->parse(); + + return $this; + } + + /** + * Set all entries of the log at once replacing the current entries + * + * @param EntryInterface[] $entries + * @return $this + */ + public function setEntries(array $entries = []): static + { + $this->entries = $entries; + return $this; + } + + /** + * Add an entry to the log + * + * @param EntryInterface $entry + * @return $this + */ + public function addEntry(EntryInterface $entry): static + { + $this->entries[] = $entry; + return $this; + } + + /** + * Get all entries of the log + * + * @return EntryInterface[] + */ + public function getEntries(): array + { + return $this->entries; + } + + /** + * Return the current element + * + * @return EntryInterface + */ + public function current(): EntryInterface + { + return $this->entries[$this->iterator]; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next(): void + { + $this->iterator++; + } + + /** + * Return the key of the current element + * + * @return int + */ + public function key(): int + { + return $this->iterator; + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid(): bool + { + return array_key_exists($this->iterator, $this->entries); + } + + /** + * Rewind the Iterator to the first element + * + * @return void + */ + public function rewind(): void + { + $this->iterator = 0; + } + + /** + * Count elements of an object + * + * @return int + */ + public function count(): int + { + return count($this->entries); + } + + /** + * Whether an offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return isset($this->entries[$offset]); + } + + /** + * Offset to retrieve + * + * @param mixed $offset + * @return EntryInterface + */ + public function offsetGet(mixed $offset): EntryInterface + { + return $this->entries[$offset]; + } + + /** + * Offset to set + * + * @param mixed $offset + * @param EntryInterface $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->entries[$offset] = $value; + } + + /** + * Offset to unset + * + * @param mixed $offset + */ + public function offsetUnset(mixed $offset): void + { + unset($this->entries[$offset]); + } + + /** + * @return string + */ + public function __toString(): string + { + return implode("\n", $this->getEntries()); + } + + /** + * @param bool $includeEntries + * @return $this + */ + public function setIncludeEntries(bool $includeEntries): static + { + $this->includeEntries = $includeEntries; + return $this; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + if (!$this->includeEntries) { + return []; + } + + return [ + "entries" => $this->getEntries() + ]; + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return "Log"; + } +} \ No newline at end of file diff --git a/src/Log/LogInterface.php b/src/Log/LogInterface.php new file mode 100644 index 0000000..c8e91b1 --- /dev/null +++ b/src/Log/LogInterface.php @@ -0,0 +1,110 @@ +getLogContentAsArray() as $number => $logLineString) { + $this->log->addEntry((new Entry()) + ->addLine(new Line($number + 1, $logLineString)) + ); + } + } +} \ No newline at end of file diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php new file mode 100644 index 0000000..ab0a850 --- /dev/null +++ b/src/Parser/Parser.php @@ -0,0 +1,47 @@ +log = $log; + return $this; + } + + /** + * Get the log content as string + * + * @return string + */ + protected function getLogContent(): string + { + return $this->log->getLogFile()->getContent(); + } + + /** + * Get the log content as array split by line + * + * @return string[] + */ + protected function getLogContentAsArray(): array + { + return explode(PHP_EOL, $this->getLogContent()); + } +} \ No newline at end of file diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 0000000..ee42b99 --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,26 @@ + + */ + protected string $entryClass = Entry::class; + + /** + * @noinspection PhpDocFieldTypeMismatchInspection + * @var class-string|LevelInterface + */ + protected string $levelClass = Level::class; + + protected ?string $pattern = null; + protected array $matches = []; + protected ?string $timeFormat = null; + protected ?DateTimeZone $timeZone = null; + + /** + * Set the entry pattern + * + * Every line matching this pattern is defined as + * new entry, all other lines are added to the + * previous entry + * + * @param string $pattern + * @return $this + */ + public function setPattern(string $pattern): static + { + $this->pattern = $pattern; + return $this; + } + + /** + * Get the entry pattern + * + * @return string + */ + public function getPattern(): string + { + return $this->pattern; + } + + /** + * Set the array of match constants + * + * The position/key in the array defines + * the position of the matching capturing + * group in the $pattern + * + * @param array $matches + * @return $this + */ + public function setMatches(array $matches): static + { + $this->matches = $matches; + return $this; + } + + /** + * Set the time format + * + * Time is parsed with the DateTime::createFromFormat() function, + * see this for format information: + * + * http://php.net/manual/en/datetime.createfromformat.php + * + * @param string $timeFormat + * @return $this + */ + public function setTimeFormat(string $timeFormat): static + { + $this->timeFormat = $timeFormat; + return $this; + } + + /** + * Set the time zone + * + * Optional, uses OS timezone otherwise + * + * @param DateTimeZone $timeZone + * @return $this + */ + public function setTimezone(DateTimeZone $timeZone): static + { + $this->timeZone = $timeZone; + return $this; + } + + /** + * Parse a log from resource to Log object + */ + public function parse(): void + { + foreach ($this->getLogContentAsArray() as $number => $lineString) { + $line = new Line($number + 1, $lineString); + $result = preg_match($this->pattern, $lineString, $matches); + if ($result !== 1) { + if (!isset($entry)) { + /** @var Entry $entry */ + $entry = new $this->entryClass(); + $this->log->addEntry($entry); + } + $entry->addLine($line); + continue; + } + + /** @var Entry $entry */ + $entry = new $this->entryClass(); + $this->log->addEntry($entry); + foreach ($matches as $key => $match) { + if ($key === 0) { + continue; + } + $matchKey = $key - 1; + if (!isset($this->matches[$matchKey])) { + throw new InvalidArgumentException("More matches found in string than defined in PatternParser::setMatches()."); + } + + $this->parseEntryMatch($entry, $this->matches[$matchKey], $match); + } + $entry->addLine($line); + } + } + + /** + * Parse an entry match + * + * Overwrite this function to add more different + * match types and call the parent function (this function) + * if you don't know the match type (default in a switch) + * + * @param Entry $entry + * @param string $matchType One of the match constants + * @param string $matchString + */ + protected function parseEntryMatch(Entry $entry, string $matchType, string $matchString): void + { + switch ($matchType) { + case static::TIME: + $date = DateTime::createFromFormat($this->timeFormat, $matchString, $this->timeZone); + if ($date) { + $entry->setTime($date->getTimestamp()); + } + break; + case static::LEVEL: + $entry->setLevel($this->levelClass::fromString($matchString)); + break; + case static::PREFIX: + $entry->setPrefix($matchString); + break; + default: + throw new InvalidArgumentException("Match type '" . $matchType . "' is not defined."); + } + } +} \ No newline at end of file diff --git a/src/Printer/DefaultPrinter.php b/src/Printer/DefaultPrinter.php new file mode 100644 index 0000000..8a623b3 --- /dev/null +++ b/src/Printer/DefaultPrinter.php @@ -0,0 +1,24 @@ +getText() . PHP_EOL; + } +} \ No newline at end of file diff --git a/src/Printer/ModifiableDefaultPrinter.php b/src/Printer/ModifiableDefaultPrinter.php new file mode 100644 index 0000000..d07ea8a --- /dev/null +++ b/src/Printer/ModifiableDefaultPrinter.php @@ -0,0 +1,24 @@ +runModifications($line->getText()) . PHP_EOL; + } +} \ No newline at end of file diff --git a/src/Printer/ModifiablePrinter.php b/src/Printer/ModifiablePrinter.php new file mode 100644 index 0000000..98ea1da --- /dev/null +++ b/src/Printer/ModifiablePrinter.php @@ -0,0 +1,59 @@ +modifications = []; + foreach ($modifications as $modification) { + $this->addModification($modification); + } + + return $this; + } + + /** + * Add a modification + * + * @param ModificationInterface $modification + * @return $this + */ + public function addModification(ModificationInterface $modification): static + { + $this->modifications[] = $modification; + return $this; + } + + /** + * Run the set modifications for a string + * + * @param string $text + * @return string + */ + protected function runModifications(string $text): string + { + foreach ($this->modifications as $modification) { + $text = $modification->modify($text); + } + + return $text; + } +} \ No newline at end of file diff --git a/src/Printer/ModifiablePrinterInterface.php b/src/Printer/ModifiablePrinterInterface.php new file mode 100644 index 0000000..84eb6dc --- /dev/null +++ b/src/Printer/ModifiablePrinterInterface.php @@ -0,0 +1,27 @@ +pattern = $pattern; + return $this; + } + + /** + * Set the replacement string + * + * See http://php.net/manual/de/function.preg-replace.php + * + * @param string $replacement + * @return $this + */ + public function setReplacement(string $replacement): PatternModification + { + $this->replacement = $replacement; + return $this; + } + + /** + * Modify the given string and return it + * + * @param string $text + * @return string + */ + public function modify(string $text): string + { + return preg_replace($this->pattern, $this->replacement, $text); + } +} \ No newline at end of file diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php new file mode 100644 index 0000000..423d20b --- /dev/null +++ b/src/Printer/Printer.php @@ -0,0 +1,99 @@ +log = $log; + return $this; + } + + /** + * Set the entry + * + * @param EntryInterface $entry + * @return $this + */ + public function setEntry(EntryInterface $entry): static + { + $this->entry = $entry; + return $this; + } + + /** + * Print the log + * + * @return string + */ + public function print(): string + { + if ($this->entry) { + return $this->printEntry(); + } else { + return $this->printLog(); + } + } + + /** + * Print a log + * + * @return string + */ + protected function printLog(): string + { + $return = ""; + foreach ($this->log as $entry) { + $return .= $this->printEntry($entry); + } + + return $return; + } + + /** + * Print an entry + * + * @param EntryInterface|null $entry + * @return string + */ + protected function printEntry(?EntryInterface $entry = null): string + { + if ($entry === null) { + $entry = $this->entry; + } + + $return = ""; + foreach ($entry as $line) { + $return .= $this->printLine($line); + } + + return $return; + } + + /** + * Print a line + * + * @param LineInterface $line + * @return string + */ + abstract protected function printLine(LineInterface $line): string; +} \ No newline at end of file diff --git a/src/Printer/PrinterInterface.php b/src/Printer/PrinterInterface.php new file mode 100644 index 0000000..ede5493 --- /dev/null +++ b/src/Printer/PrinterInterface.php @@ -0,0 +1,37 @@ +value = $matches[1]; + } + + public function getLogContent(): ?string + { + return parent::getLogContent(); + } +} \ No newline at end of file diff --git a/test/src/Analysis/TestPatternProblem.php b/test/src/Analysis/TestPatternProblem.php new file mode 100644 index 0000000..86256cf --- /dev/null +++ b/test/src/Analysis/TestPatternProblem.php @@ -0,0 +1,70 @@ +cause = $cause; + return $this; + } + + /** + * Get an array of possible patterns + * + * The array key of the pattern will be passed to setMatches() + * + * @return array + */ + public static function getPatterns(): array + { + return ['/I have a problem with (\w+)/']; + } + + /** + * Apply the matches from the pattern + * + * @param array $matches + * @param mixed $patternKey + * @return void + */ + public function setMatches(array $matches, mixed $patternKey): void + { + $this->cause = $matches[1]; + } + + /** + * Get the problem as human-readable message + * + * @return string + */ + public function getMessage(): string + { + return "There is a problem with " . $this->cause; + } + + /** + * Check if the $insight object is equal with the current object + * + * @param InsightInterface $insight + * @return bool + */ + public function isEqual(InsightInterface $insight): bool + { + return $insight instanceof static && $this->cause === $insight->cause; + } +} \ No newline at end of file diff --git a/test/src/Analysis/TestProblem.php b/test/src/Analysis/TestProblem.php new file mode 100644 index 0000000..f47ca21 --- /dev/null +++ b/test/src/Analysis/TestProblem.php @@ -0,0 +1,33 @@ +setPattern('/information/')]; + } +} \ No newline at end of file diff --git a/test/src/Log/TestLessDetectableLog.php b/test/src/Log/TestLessDetectableLog.php new file mode 100644 index 0000000..13c7b28 --- /dev/null +++ b/test/src/Log/TestLessDetectableLog.php @@ -0,0 +1,24 @@ +setPattern('/information/')]; + } +} \ No newline at end of file diff --git a/test/src/Log/TestMoreDetectableLog.php b/test/src/Log/TestMoreDetectableLog.php new file mode 100644 index 0000000..1f9d531 --- /dev/null +++ b/test/src/Log/TestMoreDetectableLog.php @@ -0,0 +1,24 @@ +setPattern('/This/')]; + } +} \ No newline at end of file diff --git a/test/src/Log/TestNeverDetectableLog.php b/test/src/Log/TestNeverDetectableLog.php new file mode 100644 index 0000000..6b44f69 --- /dev/null +++ b/test/src/Log/TestNeverDetectableLog.php @@ -0,0 +1,24 @@ +setPattern('/missing/')]; + } +} \ No newline at end of file diff --git a/test/src/Log/TestPatternLog.php b/test/src/Log/TestPatternLog.php new file mode 100644 index 0000000..43a82fa --- /dev/null +++ b/test/src/Log/TestPatternLog.php @@ -0,0 +1,40 @@ +setPattern('/(\[([^\]]+)\] \[[^\/]+\/([^\]]+)\]).*/') + ->setMatches([PatternParser::PREFIX, PatternParser::TIME, PatternParser::LEVEL]) + ->setTimeFormat('d.m.Y H:i:s'); + } + + /** + * Get the default analyser + * + * @return PatternAnalyser + */ + public static function getDefaultAnalyser(): PatternAnalyser + { + return (new PatternAnalyser()) + ->addPossibleInsightClass(TestPatternProblem::class) + ->addPossibleInsightClass(TestPatternInformation::class); + } +} \ No newline at end of file diff --git a/test/src/Printer/TestModification.php b/test/src/Printer/TestModification.php new file mode 100644 index 0000000..e49bded --- /dev/null +++ b/test/src/Printer/TestModification.php @@ -0,0 +1,22 @@ +addInsight((new TestPatternProblem()) + ->setCause("ABC") + ->increaseCounter() + ->setEntry((new Entry())->setTime(2)->setLevel(Level::ERROR)->setPrefix("[01.01.1970 00:00:02] [Log/ERROR]") + ->addLine(new Line(2, "[01.01.1970 00:00:02] [Log/ERROR] I have a problem with ABC")) + ) + ) + ->addInsight((new TestPatternProblem()) + ->setCause("XYZ") + ->setEntry((new Entry())->setTime(4)->setLevel(Level::ERROR)->setPrefix("[01.01.1970 00:00:04] [Log/ERROR]") + ->addLine(new Line(4, "[01.01.1970 00:00:04] [Log/ERROR] I have a problem with XYZ")) + ) + ) + ->addInsight((new TestPatternProblem()) + ->setCause("DEF") + ->setEntry((new Entry())->setTime(6)->setLevel(Level::ERROR)->setPrefix("[01.01.1970 00:00:06] [Log/ERROR]") + ->addLine(new Line(6, "[01.01.1970 00:00:06] [Log/ERROR] I have a problem with DEF")) + ->addLine(new Line(7, "I have a problem with GHI")) + ) + ) + ->addInsight((new TestPatternProblem()) + ->setCause("GHI") + ->setEntry((new Entry())->setTime(6)->setLevel(Level::ERROR)->setPrefix("[01.01.1970 00:00:06] [Log/ERROR]") + ->addLine(new Line(6, "[01.01.1970 00:00:06] [Log/ERROR] I have a problem with DEF")) + ->addLine(new Line(7, "I have a problem with GHI")) + ) + ) + ->addInsight((new TestPatternInformation()) + ->setValue("v1.2.3") + ->setEntry((new Entry())->setTime(7)->setLevel(Level::INFO)->setPrefix("[01.01.1970 00:00:07] [Log/INFO]") + ->addLine(new Line(8, "[01.01.1970 00:00:07] [Log/INFO] This log was generated by software v1.2.3")) + ) + ); + } + + public function testAnalyse(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/problem.log'); + $log = (new TestPatternLog())->setLogFile($logFile); + $log->parse(); + + $analysis = $log->analyse(); + $this->assertJsonStringEqualsJsonString(json_encode($this->getExpectedAnalysis()->getInsights()), json_encode($analysis->getInsights())); + } + + public function testAnalyseWithPossibleInsightClasses(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/problem.log'); + $log = (new TestPatternLog())->setLogFile($logFile); + $log->parse(); + + $analyser = (new PatternAnalyser()) + ->setPossibleInsightClasses([ + TestPatternInformation::class, + TestPatternProblem::class + ]); + + $analysis = $log->analyse($analyser); + $this->assertJsonStringEqualsJsonString(json_encode($this->getExpectedAnalysis()->getInsights()), json_encode($analysis->getInsights())); + } + + public function testAddPossibleInsightClassThrowsExceptionIfPossibleInsightClassDoesNotImplementPatternInsightInterface(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Class " . TestSolution::class . " does not implement " . PatternInsightInterface::class . "."); + (new PatternAnalyser())->addPossibleInsightClass(TestSolution::class); + } + + public function testRemovePossibleInsightClass(): void + { + $analyser = (new PatternAnalyser()) + ->setPossibleInsightClasses([ + TestPatternInformation::class, + TestPatternProblem::class + ]); + + $reflector = new ReflectionClass(PatternAnalyser::class); + $possibleInsightClassesProperty = $reflector->getProperty('possibleInsightClasses'); + + $this->assertEquals([TestPatternInformation::class, TestPatternProblem::class], $possibleInsightClassesProperty->getValue($analyser)); + + $analyser->removePossibleInsightClass(TestPatternProblem::class); + + $this->assertEquals([TestPatternInformation::class], $possibleInsightClassesProperty->getValue($analyser)); + } + + public function testRemovePossibleInsightClassThrowsExceptionIfPossibleInsightClassIsNotAdded(): void + { + // Set TestPatternProblem class + $analyser = (new PatternAnalyser()) + ->setPossibleInsightClasses([ + TestPatternProblem::class + ]); + + $reflector = new ReflectionClass(PatternAnalyser::class); + $possibleInsightClassesProperty = $reflector->getProperty('possibleInsightClasses'); + + $this->assertEquals([TestPatternProblem::class], $possibleInsightClassesProperty->getValue($analyser)); + + // Remove TestPatternInformation class -> not found + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Class " . TestPatternInformation::class . " not found in possible insight classes."); + $analyser->removePossibleInsightClass(TestPatternInformation::class); + + $this->assertEquals([TestPatternInformation::class], $possibleInsightClassesProperty->getValue($analyser)); + } + + public function testOverridePossibleInsightClass(): void + { + $analyser = (new PatternAnalyser()) + ->setPossibleInsightClasses([ + TestPatternProblem::class + ]); + + $reflector = new ReflectionClass(PatternAnalyser::class); + $possibleInsightClassesProperty = $reflector->getProperty('possibleInsightClasses'); + + $this->assertEquals([TestPatternProblem::class], $possibleInsightClassesProperty->getValue($analyser)); + + $childInsightClass = new class extends TestPatternProblem { + // Is empty child class + }; + + $analyser->overridePossibleInsightClass(TestPatternProblem::class, get_class($childInsightClass)); + + $this->assertEquals([get_class($childInsightClass)], $possibleInsightClassesProperty->getValue($analyser)); + } + + public function testOverridePossibleInsightClassThrowsExceptionIfClassDoesNotExtendParent(): void + { + $analyser = (new PatternAnalyser()) + ->setPossibleInsightClasses([ + TestPatternProblem::class + ]); + + $reflector = new ReflectionClass(PatternAnalyser::class); + $possibleInsightClassesProperty = $reflector->getProperty('possibleInsightClasses'); + + $this->assertEquals([TestPatternProblem::class], $possibleInsightClassesProperty->getValue($analyser)); + + $childInsightClass = new class { + // Is empty and not a child class + }; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Class " . get_class($childInsightClass) . " does not extend " . TestPatternProblem::class . "."); + $analyser->overridePossibleInsightClass(TestPatternProblem::class, get_class($childInsightClass)); + + $this->assertEquals([TestPatternProblem::class], $possibleInsightClassesProperty->getValue($analyser)); + } +} diff --git a/test/tests/Analysis/AnalysisTest.php b/test/tests/Analysis/AnalysisTest.php new file mode 100644 index 0000000..e082391 --- /dev/null +++ b/test/tests/Analysis/AnalysisTest.php @@ -0,0 +1,157 @@ +assertSame($analysis, $analysis->setInsights([$insight])); + $this->assertSame([$insight], $analysis->getInsights()); + } + + public function testAddInsight(): void + { + $analysis = new Analysis(); + $insight = new TestInsight(); + $this->assertSame($analysis, $analysis->addInsight($insight)); + $this->assertSame([$insight], $analysis->getInsights()); + } + + public function testGetProblems(): void + { + $analysis = new Analysis(); + $problem = new TestProblem(); + $information = new TestInformation(); + + $analysis->addInsight($problem); + $analysis->addInsight($information); + + $this->assertEquals([$problem], $analysis->getProblems()); + } + + public function testGetInformation(): void + { + $analysis = new Analysis(); + $problem = new TestProblem(); + $information = new TestInformation(); + + $analysis->addInsight($problem); + $analysis->addInsight($information); + + $this->assertEquals([$information], $analysis->getInformation()); + } + + public function testKey(): void + { + $analysis = new Analysis(); + $problem = new TestProblem(); + $information = new TestInformation(); + + $analysis->addInsight($problem); + $this->assertEquals(0, $analysis->key()); + $analysis->addInsight($information); + $this->assertEquals(1, $analysis->key()); + } + + + public function testCount(): void + { + $analysis = new Analysis(); + $problem = new TestProblem(); + $information = new TestInformation(); + + $this->assertEquals(0, $analysis->count()); + $analysis->addInsight($problem); + $this->assertEquals(1, $analysis->count()); + $analysis->addInsight($information); + $this->assertEquals(2, $analysis->count()); + } + + public function testAddingTheSameInsightIncreasesInternalCounter(): void + { + // Adding the same insight to an analysis does not add it to the insights, and therefore it + // does not increase the counter of the analysis, but the internal counter of the insight. + // See Analysis->addInsight() + + $analysis = new Analysis(); + $problem = new TestPatternProblem(); + $problem2 = new TestPatternProblem(); + + $analysis->addInsight($problem); + $this->assertEquals(1, $analysis->count()); + $this->assertEquals(1, $problem->getCounterValue()); + + $analysis->addInsight($problem2); + $this->assertEquals(1, $analysis->count()); + $this->assertEquals(2, $problem->getCounterValue()); + } + + public function testOffsetExists(): void + { + $analysis = new Analysis(); + $information = new TestInformation(); + + $this->assertArrayNotHasKey(0, $analysis); + $this->assertEquals(0, $analysis->count()); + $analysis->addInsight($information); + $this->assertArrayHasKey(0, $analysis); + $this->assertEquals($information, $analysis[0]); + } + + public function testOffsetGet(): void + { + $analysis = new Analysis(); + $information = new TestInformation(); + $analysis->addInsight($information); + + // Exists + $this->assertEquals($information, $analysis[0]); + + // Does not exist -> "undefined array key" error + $this->assertArrayNotHasKey(1, $analysis); + } + + public function testOffsetSet(): void + { + $analysis = new Analysis(); + $information = new TestInformation(); + + $this->assertArrayNotHasKey(0, $analysis); + $this->assertEquals(0, $analysis->count()); + $analysis->addInsight($information); + $this->assertArrayHasKey(0, $analysis); + $this->assertEquals($information, $analysis[0]); + + // Overwrite $information on $analysis[0] using the offsetSet + $problem = new TestProblem(); + $analysis[0] = $problem; + $this->assertEquals($problem, $analysis[0]); + } + + public function testOffsetUnset(): void + { + $analysis = new Analysis(); + $information = new TestInformation(); + + $this->assertArrayNotHasKey(0, $analysis); + $this->assertEquals(0, $analysis->count()); + $analysis->addInsight($information); + $this->assertArrayHasKey(0, $analysis); + $this->assertEquals($information, $analysis[0]); + + // Unset $information on $analysis[0] using the offsetUnset + unset($analysis[0]); + $this->assertArrayNotHasKey(0, $analysis); + $this->assertArrayNotHasKey(1, $analysis); + } +} diff --git a/test/tests/Analysis/InformationTest.php b/test/tests/Analysis/InformationTest.php new file mode 100644 index 0000000..1195e66 --- /dev/null +++ b/test/tests/Analysis/InformationTest.php @@ -0,0 +1,61 @@ +setValue($value); + $this->assertEquals($value, $information->getValue()); + } + + public function testGetLabel(): void + { + $this->assertEquals("Label", (new TestInformation())->getLabel()); + } + + public function testGetMessage(): void + { + $value = uniqid(); + $information = new TestInformation(); + $information->setValue($value); + $this->assertEquals("Label: " . $value, $information->getMessage()); + $this->assertEquals("Label: " . $value, (string)$information); + } + + public function testIsEqual(): void + { + $value = uniqid(); + $informationA = new TestInformation(); + $informationA->setValue($value); + $informationB = new TestInformation(); + $informationB->setValue($value); + + $this->assertTrue($informationA->isEqual($informationB)); + $this->assertTrue($informationA->isEqual($informationA)); + } + + public function testGetLogContent(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/problem.log'); + $log = (new TestPatternLog())->setLogFile($logFile); + $log->parse(); + + $analysis = $log->analyse(); + foreach ($analysis->getInformation() as $information) { + /** @var TestPatternInformation $information */ + $this->assertNotNull($information->getLogContent()); + $this->assertEquals($logFile->getContent(), $information->getLogContent()); + } + } +} diff --git a/test/tests/Analysis/ProblemTest.php b/test/tests/Analysis/ProblemTest.php new file mode 100644 index 0000000..068b00f --- /dev/null +++ b/test/tests/Analysis/ProblemTest.php @@ -0,0 +1,113 @@ +assertSame($problem, $problem->setSolutions([$solution])); + $this->assertEquals([$solution], $problem->getSolutions()); + } + + public function testAddSolutions(): void + { + $problem = new TestProblem(); + $solution = new TestSolution(); + $this->assertSame($problem, $problem->addSolution($solution)); + $this->assertEquals([$solution], $problem->getSolutions()); + } + + public function testKey(): void + { + $problem = new TestProblem(); + $solution = new TestSolution(); + + $problem->addSolution($solution); + /** @noinspection PhpStatementHasEmptyBodyInspection */ + foreach ($problem as $ignored) { + // do nothing + } + $this->assertEquals(1, $problem->key()); + } + + + public function testCount(): void + { + $problem = new TestProblem(); + $solution1 = new TestSolution(); + $solution2 = new TestSolution(); + + $this->assertEquals(0, $problem->count()); + $problem->addSolution($solution1); + $this->assertEquals(1, $problem->count()); + $problem->addSolution($solution2); + $this->assertEquals(2, $problem->count()); + } + + public function testOffsetExists(): void + { + $problem = new TestProblem(); + $solution = new TestSolution(); + + $this->assertArrayNotHasKey(0, $problem); + $this->assertEquals(0, $problem->count()); + $problem->addSolution($solution); + $this->assertArrayHasKey(0, $problem); + $this->assertEquals($solution, $problem[0]); + } + + public function testOffsetGet(): void + { + $problem = new TestProblem(); + $solution = new TestSolution(); + $problem->addSolution($solution); + + // Exists + $this->assertEquals($solution, $problem[0]); + + // Does not exist -> "undefined array key" error + $this->assertArrayNotHasKey(1, $problem); + } + + public function testOffsetSet(): void + { + $problem = new TestProblem(); + $solution1 = new TestSolution(); + + $this->assertArrayNotHasKey(0, $problem); + $this->assertEquals(0, $problem->count()); + $problem->addSolution($solution1); + $this->assertArrayHasKey(0, $problem); + $this->assertEquals($solution1, $problem[0]); + + // Overwrite $solution1 on $problem[0] using the offsetSet + $TestSolution2 = new TestSolution(); + $problem[0] = $TestSolution2; + $this->assertEquals($TestSolution2, $problem[0]); + } + + public function testOffsetUnset(): void + { + $problem = new TestProblem(); + $solution = new TestSolution(); + + $this->assertArrayNotHasKey(0, $problem); + $this->assertEquals(0, $problem->count()); + $problem->addSolution($solution); + $this->assertArrayHasKey(0, $problem); + $this->assertEquals($solution, $problem[0]); + + // Unset $solution on $problem[0] using the offsetUnset + unset($problem[0]); + $this->assertArrayNotHasKey(0, $problem); + $this->assertArrayNotHasKey(1, $problem); + } +} diff --git a/test/tests/Detective/DetectiveTest.php b/test/tests/Detective/DetectiveTest.php new file mode 100644 index 0000000..e8a99cc --- /dev/null +++ b/test/tests/Detective/DetectiveTest.php @@ -0,0 +1,122 @@ +setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')); + $detective->setPossibleLogClasses($possibleLogClasses); + return $detective; + } + + public function testDetect(): void + { + $this->assertEquals(TestAlwaysDetectableLog::class, + get_class($this->getDetective([ + TestAlwaysDetectableLog::class, + TestLessDetectableLog::class, + TestMoreDetectableLog::class, + TestNeverDetectableLog::class])->detect())); + + $this->assertEquals(TestMoreDetectableLog::class, + get_class($this->getDetective([ + TestLessDetectableLog::class, + TestMoreDetectableLog::class, + TestNeverDetectableLog::class])->detect())); + + $this->assertEquals(TestLessDetectableLog::class, + get_class($this->getDetective([ + TestLessDetectableLog::class, + TestNeverDetectableLog::class])->detect())); + + $this->assertEquals(Log::class, + get_class($this->getDetective([ + TestNeverDetectableLog::class])->detect())); + } + + public function testAddPossibleLogClassThrowsExceptionIfPossibleClassDoesNotImplementDetectableLogInterface(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Class " . TestSolution::class . " does not implement " . DetectableLogInterface::class . "."); + (new Detective())->addPossibleLogClass(TestSolution::class); + } + + public function testDetectThrowsExceptionIfDetectorClassDoesNotImplementDetectorInterface(): void + { + $invalidDetectorClass = new class { + // Is empty and not a child class of DetectorInterface + }; + + $customLogClass = new class() extends Log implements DetectableLogInterface { + private static array $detectors = []; + + public static function setDetectors($detectors): void + { + self::$detectors = $detectors; + } + + public static function getDetectors(): array + { + return self::$detectors; + } + }; + + $customLogClass::setDetectors([$invalidDetectorClass]); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Class " . get_class($invalidDetectorClass) . " does not implement " . DetectorInterface::class . "."); + $this->getDetective([get_class($customLogClass)])->detect(); + } + + public function testGetPossibleLogClasses(): void + { + $possibleLogClasses = [ + TestAlwaysDetectableLog::class, + TestLessDetectableLog::class, + TestMoreDetectableLog::class, + TestNeverDetectableLog::class + ]; + + $detective = $this->getDetective($possibleLogClasses); + + $this->assertEquals($possibleLogClasses, $detective->getPossibleLogClasses()); + } + + public function testAddDetective(): void + { + $possibleLogClasses1 = [ + TestAlwaysDetectableLog::class, + TestLessDetectableLog::class + ]; + + $possibleLogClasses2 = [ + TestMoreDetectableLog::class, + TestNeverDetectableLog::class + ]; + + $detective1 = $this->getDetective($possibleLogClasses1); + $detective2 = $this->getDetective($possibleLogClasses2); + + $detective1->addDetective($detective2); + + $this->assertEquals(array_merge($possibleLogClasses1, $possibleLogClasses2), $detective1->getPossibleLogClasses()); + } +} diff --git a/test/tests/Detective/LinePatternDetectorTest.php b/test/tests/Detective/LinePatternDetectorTest.php new file mode 100644 index 0000000..24456b3 --- /dev/null +++ b/test/tests/Detective/LinePatternDetectorTest.php @@ -0,0 +1,30 @@ +assertEquals(5 / 7, (new LinePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/information/') + ->detect() + ); + + $this->assertFalse((new LinePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/missing/') + ->detect() + ); + + $this->assertEquals(1, (new LinePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/This/') + ->detect() + ); + } +} diff --git a/test/tests/Detective/MultiPatternDetectorTest.php b/test/tests/Detective/MultiPatternDetectorTest.php new file mode 100644 index 0000000..5d240a7 --- /dev/null +++ b/test/tests/Detective/MultiPatternDetectorTest.php @@ -0,0 +1,40 @@ +assertTrue((new MultiPatternDetector()) + ->setLogFile(new StringLogFile("You can detect this.")) + ->addPattern('/detect/') + ->detect() + ); + } + + public function testDetectMultiplePatterns(): void + { + $this->assertTrue((new MultiPatternDetector()) + ->setLogFile(new StringLogFile("You can detect this and this.")) + ->addPattern('/detect/') + ->addPattern('/and this/') + ->detect() + ); + } + + public function testNotDetectMissingFromMultiplePatterns(): void + { + $this->assertFalse((new MultiPatternDetector()) + ->setLogFile(new StringLogFile("You can detect this and this.")) + ->addPattern('/detect/') + ->addPattern('/and this/') + ->addPattern('/but not this/') + ->detect() + ); + } +} diff --git a/test/tests/Detective/SinglePatternDetectorTest.php b/test/tests/Detective/SinglePatternDetectorTest.php new file mode 100644 index 0000000..6f82ca3 --- /dev/null +++ b/test/tests/Detective/SinglePatternDetectorTest.php @@ -0,0 +1,25 @@ +assertTrue((new SinglePatternDetector()) + ->setLogFile(new StringLogFile("You can detect this.")) + ->setPattern('/detect/') + ->detect() + ); + + $this->assertFalse((new SinglePatternDetector()) + ->setLogFile(new StringLogFile("You cannot detect this.")) + ->setPattern('/missing/') + ->detect() + ); + } +} diff --git a/test/tests/Detective/WeightedSinglePatternDetectorTest.php b/test/tests/Detective/WeightedSinglePatternDetectorTest.php new file mode 100644 index 0000000..39e6049 --- /dev/null +++ b/test/tests/Detective/WeightedSinglePatternDetectorTest.php @@ -0,0 +1,40 @@ +assertEquals(1, (new WeightedSinglePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/This/') + ->setWeight(1) + ->detect() + ); + + $this->assertEquals(0.5, (new WeightedSinglePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/This/') + ->setWeight(0.5) + ->detect() + ); + + $this->assertEquals(0, (new WeightedSinglePatternDetector()) + ->setLogFile(new \Aternos\Codex\Log\File\PathLogFile(__DIR__ . '/../../data/simple.log')) + ->setPattern('/This/') + ->setWeight(0) + ->detect() + ); + + $this->assertFalse((new WeightedSinglePatternDetector()) + ->setLogFile(new StringLogFile("You cannot detect this.")) + ->setPattern('/missing/') + ->detect() + ); + } +} diff --git a/test/tests/Log/EntryTest.php b/test/tests/Log/EntryTest.php new file mode 100644 index 0000000..34481d5 --- /dev/null +++ b/test/tests/Log/EntryTest.php @@ -0,0 +1,138 @@ +assertSame($entry, $entry->addLine($line)); + $this->assertEquals([$line], $entry->getLines()); + } + + public function testSetGetLines(): void + { + $entry = new Entry(); + $line = new Line(1, uniqid()); + $this->assertSame($entry, $entry->setLines([$line])); + $this->assertEquals([$line], $entry->getLines()); + } + + public function testSetGetLevel(): void + { + $entry = new Entry(); + $level = Level::CRITICAL; + $this->assertSame($entry, $entry->setLevel($level)); + $this->assertEquals($level, $entry->getLevel()); + } + + public function testSetGetTime(): void + { + $entry = new Entry(); + $time = time(); + $this->assertSame($entry, $entry->setTime($time)); + $this->assertEquals($time, $entry->getTime()); + } + + public function testSetGetPrefix(): void + { + $entry = new Entry(); + $prefix = uniqid(); + $this->assertSame($entry, $entry->setPrefix($prefix)); + $this->assertEquals($prefix, $entry->getPrefix()); + } + + public function testKey(): void + { + $entry = new Entry(); + $line = new Line(1, uniqid()); + + $entry->addLine($line); + /** @noinspection PhpStatementHasEmptyBodyInspection */ + foreach ($entry as $ignored) { + // do nothing + } + $this->assertEquals(1, $entry->key()); + } + + + public function testCount(): void + { + $entry = new Entry(); + $line1 = new Line(1, uniqid()); + $line2 = new Line(2, uniqid()); + + $this->assertEquals(0, $entry->count()); + $entry->addLine($line1); + $this->assertEquals(1, $entry->count()); + $entry->addLine($line2); + $this->assertEquals(2, $entry->count()); + } + + public function testOffsetExists(): void + { + $entry = new Entry(); + $line = new Line(1, uniqid()); + + $this->assertArrayNotHasKey(0, $entry); + $this->assertEquals(0, $entry->count()); + $entry->addLine($line); + $this->assertArrayHasKey(0, $entry); + $this->assertEquals($line, $entry[0]); + } + + public function testOffsetGet(): void + { + $entry = new Entry(); + $line = new Line(1, uniqid()); + $entry->addLine($line); + + // Exists + $this->assertEquals($line, $entry[0]); + + // Does not exist -> "undefined array key" error + $this->assertArrayNotHasKey(1, $entry); + } + + public function testOffsetSet(): void + { + $entry = new Entry(); + $line1 = new Line(1, uniqid()); + + $this->assertArrayNotHasKey(0, $entry); + $this->assertEquals(0, $entry->count()); + $entry->addLine($line1); + $this->assertArrayHasKey(0, $entry); + $this->assertEquals($line1, $entry[0]); + + // Overwrite $line1 on $entry[0] using the offsetSet + $line2 = new Line(2, uniqid()); + $entry[0] = $line2; + $this->assertEquals($line2, $entry[0]); + } + + public function testOffsetUnset(): void + { + $entry = new Entry(); + $line = new Line(1, uniqid()); + + $this->assertArrayNotHasKey(0, $entry); + $this->assertEquals(0, $entry->count()); + $entry->addLine($line); + $this->assertArrayHasKey(0, $entry); + $this->assertEquals($line, $entry[0]); + + // Unset $line1 on $entry[0] using the offsetUnset + unset($entry[0]); + $this->assertArrayNotHasKey(0, $entry); + $this->assertArrayNotHasKey(1, $entry); + } +} diff --git a/test/tests/Log/File/PathLogFileTest.php b/test/tests/Log/File/PathLogFileTest.php new file mode 100644 index 0000000..acbd00a --- /dev/null +++ b/test/tests/Log/File/PathLogFileTest.php @@ -0,0 +1,17 @@ +assertStringEqualsFile($path, $logFile->getContent()); + } +} diff --git a/test/tests/Log/File/StreamLogFileTest.php b/test/tests/Log/File/StreamLogFileTest.php new file mode 100644 index 0000000..ea5b0d5 --- /dev/null +++ b/test/tests/Log/File/StreamLogFileTest.php @@ -0,0 +1,18 @@ +assertStringEqualsFile($path, $logFile->getContent()); + } +} diff --git a/test/tests/Log/File/StringLogFileTest.php b/test/tests/Log/File/StringLogFileTest.php new file mode 100644 index 0000000..2e31b20 --- /dev/null +++ b/test/tests/Log/File/StringLogFileTest.php @@ -0,0 +1,17 @@ +assertEquals($content, $logFile->getContent()); + } +} diff --git a/test/tests/Log/LineTest.php b/test/tests/Log/LineTest.php new file mode 100644 index 0000000..415ff31 --- /dev/null +++ b/test/tests/Log/LineTest.php @@ -0,0 +1,33 @@ +assertSame($line, $line->setText($text)); + $this->assertEquals($text, $line->getText()); + } + + public function testSetGetNumber(): void + { + $number = rand(0, 100); + $line = new Line(999, ""); + $this->assertSame($line, $line->setNumber($number)); + $this->assertEquals($number, $line->getNumber()); + } + + public function test__toString(): void + { + $text = uniqid(); + $line = new Line(1, $text); + $this->assertSame($line, $line->setText($text)); + $this->assertEquals($text, (string)$line); + } +} diff --git a/test/tests/Log/LogTest.php b/test/tests/Log/LogTest.php new file mode 100644 index 0000000..e693f5e --- /dev/null +++ b/test/tests/Log/LogTest.php @@ -0,0 +1,103 @@ +assertSame($log, $log->addEntry($entry)); + $this->assertEquals([$entry], $log->getEntries()); + } + + public function testSetGetEntries(): void + { + $log = new Log(); + $entry = new Entry(); + $this->assertSame($log, $log->setEntries([$entry])); + $this->assertEquals([$entry], $log->getEntries()); + } + + + public function testKey(): void + { + $log = new Log(); + $entry = new Entry(); + + $log->addEntry($entry); + /** @noinspection PhpStatementHasEmptyBodyInspection */ + foreach ($log as $ignored) { + // do nothing + } + $this->assertEquals(1, $log->key()); + } + + + public function testCount(): void + { + $log = new Log(); + $entry1 = new Entry(); + $entry2 = new Entry(); + + $this->assertEquals(0, $log->count()); + $log->addEntry($entry1); + $this->assertEquals(1, $log->count()); + $log->addEntry($entry2); + $this->assertEquals(2, $log->count()); + } + + public function testOffsetExists(): void + { + $log = new Log(); + $entry = new Entry(); + + $this->assertArrayNotHasKey(0, $log); + $this->assertEquals(0, $log->count()); + $log->addEntry($entry); + $this->assertArrayHasKey(0, $log); + $this->assertEquals($entry, $log[0]); + } + + public function testOffsetGet(): void + { + $log = new Log(); + $entry = new Entry(); + $log->addEntry($entry); + + // Exists + $this->assertEquals($entry, $log[0]); + + // Does not exist -> "undefined array key" error + $this->assertArrayNotHasKey(1, $log); + } + + public function testOffsetSet(): void + { + $log = new Log(); + $entry1 = new Entry(); + $log->addEntry($entry1); + + // Overwrite $entry1 on $log[0] using the offsetSet + $entry2 = new Entry(); + $log[0] = $entry2; + $this->assertEquals($entry2, $log[0]); + } + + public function testOffsetUnset(): void + { + $log = new Log(); + $entry = new Entry(); + $log->addEntry($entry); + + // Unset $entry on $log[0] using the offsetUnset + unset($log[0]); + $this->assertArrayNotHasKey(0, $log); + $this->assertArrayNotHasKey(1, $log); + } +} diff --git a/test/tests/Parser/DefaultParserTest.php b/test/tests/Parser/DefaultParserTest.php new file mode 100644 index 0000000..e24b63c --- /dev/null +++ b/test/tests/Parser/DefaultParserTest.php @@ -0,0 +1,47 @@ +setLogFile(new PathLogFile(__DIR__ . '/../../data/simple.log')) + ->addEntry((new Entry()) + ->addLine(new Line(1, "[01.01.1970 00:00:01] [Log/INFO] This is the first message containing information."))) + ->addEntry((new Entry()) + ->addLine(new Line(2, "[01.01.1970 00:00:02] [Log/DEBUG] This is the second message containing a debug information."))) + ->addEntry((new Entry()) + ->addLine(new Line(3, "[01.01.1970 00:00:03] [Log/WARN] This is the third message containing a warning information."))) + ->addEntry((new Entry()) + ->addLine(new Line(4, "[01.01.1970 00:00:04] [Log/ERROR] This is the third message containing an error information."))) + ->addEntry((new Entry()) + ->addLine(new Line(5, "This line continues the error entry to add even more information."))) + ->addEntry((new Entry()) + ->addLine(new Line(6, "This line is also part of the error entry."))) + ->addEntry((new Entry()) + ->addLine(new Line(7, "[01.01.1970 00:00:05] [Log/INFO] This is the last message of the log."))); + } + + + public function testParse(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/simple.log'); + $log = (new Log())->setLogFile($logFile); + $log->parse(); + + $this->assertEquals($this->getSimpleExpectedLog(), $log); + } +} diff --git a/test/tests/Parser/PatternParserTest.php b/test/tests/Parser/PatternParserTest.php new file mode 100644 index 0000000..fb06b5f --- /dev/null +++ b/test/tests/Parser/PatternParserTest.php @@ -0,0 +1,76 @@ +setLogFile(new PathLogFile(__DIR__ . '/../../data/simple.log')) + ->addEntry((new Entry())->setLevel(Level::INFO)->setTime(1)->setPrefix("[01.01.1970 00:00:01] [Log/INFO]") + ->addLine(new Line(1, "[01.01.1970 00:00:01] [Log/INFO] This is the first message containing information."))) + ->addEntry((new Entry())->setLevel(Level::DEBUG)->setTime(2)->setPrefix("[01.01.1970 00:00:02] [Log/DEBUG]") + ->addLine(new Line(2, "[01.01.1970 00:00:02] [Log/DEBUG] This is the second message containing a debug information."))) + ->addEntry((new Entry())->setLevel(Level::WARNING)->setTime(3)->setPrefix("[01.01.1970 00:00:03] [Log/WARN]") + ->addLine(new Line(3, "[01.01.1970 00:00:03] [Log/WARN] This is the third message containing a warning information."))) + ->addEntry((new Entry())->setLevel(Level::ERROR)->setTime(4)->setPrefix("[01.01.1970 00:00:04] [Log/ERROR]") + ->addLine(new Line(4, "[01.01.1970 00:00:04] [Log/ERROR] This is the third message containing an error information.")) + ->addLine(new Line(5, "This line continues the error entry to add even more information.")) + ->addLine(new Line(6, "This line is also part of the error entry."))) + ->addEntry((new Entry())->setLevel(Level::INFO)->setTime(5)->setPrefix("[01.01.1970 00:00:05] [Log/INFO]") + ->addLine(new Line(7, "[01.01.1970 00:00:05] [Log/INFO] This is the last message of the log."))); + } + + + public function testParse(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/simple.log'); + $log = (new TestPatternLog())->setLogFile($logFile); + $log->parse(); + + $this->assertEquals($this->getSimpleExpectedLog(), $log); + } + + public function testParseWithCustomParser(): void + { + $logFile = new PathLogFile(__DIR__ . '/../../data/simple.log'); + $log = (new TestPatternLog())->setLogFile($logFile); + + $patternParser = (new PatternParser()) + ->setPattern('/(\[([^\]]+)\] \[[^\/]+\/([^\]]+)\]).*/') + ->setMatches([PatternParser::PREFIX, PatternParser::TIME, PatternParser::LEVEL]) + ->setTimeFormat('d.m.Y H:i:s'); + $log->parse($patternParser); + + $this->assertEquals($this->getSimpleExpectedLog(), $log); + } + + public function testGetPattern(): void + { + $pattern = '/\[([^\]]+)\] \[[^\/]+\/([^\]]+)\].*/'; + + $patternParser = (new PatternParser()) + ->setPattern($pattern) + ->setMatches([PatternParser::TIME, PatternParser::LEVEL]) + ->setTimezone(new \DateTimeZone('Europe/Berlin')) + ->setTimeFormat('d.m.Y H:i:s'); + + $this->assertEquals($pattern, $patternParser->getPattern()); + } + +} diff --git a/test/tests/Printer/DefaultPrinterTest.php b/test/tests/Printer/DefaultPrinterTest.php new file mode 100644 index 0000000..e636241 --- /dev/null +++ b/test/tests/Printer/DefaultPrinterTest.php @@ -0,0 +1,35 @@ +setLogFile($logFile); + $log->parse(); + + $printer = new DefaultPrinter(); + $printer->setLog($log); + $this->assertEquals($logFile->getContent(), trim($printer->print())); + } + + public function testPrintEntry(): void + { + $text = uniqid(); + $entry = (new Entry())->addLine(new Line(1, $text)); + + $printer = new DefaultPrinter(); + $printer->setEntry($entry); + $this->assertEquals($text, trim($printer->print())); + } +} diff --git a/test/tests/Printer/ModifiableDefaultPrinterTest.php b/test/tests/Printer/ModifiableDefaultPrinterTest.php new file mode 100644 index 0000000..d49957d --- /dev/null +++ b/test/tests/Printer/ModifiableDefaultPrinterTest.php @@ -0,0 +1,38 @@ +setLogFile($logFile); + $log->parse(); + + $printer = new ModifiableDefaultPrinter(); + $printer->addModification(new TestModification()); + $printer->setLog($log); + $this->assertEquals("This is bar!", trim($printer->print())); + } + + public function testPrintEntry(): void + { + $entry = (new Entry())->addLine(new Line(1, "This is foo!")); + + $printer = new ModifiableDefaultPrinter(); + $printer->setModifications([new TestModification()]); + $printer->setEntry($entry); + $this->assertEquals("This is bar!", trim($printer->print())); + } +} diff --git a/test/tests/Printer/PatternModificationTest.php b/test/tests/Printer/PatternModificationTest.php new file mode 100644 index 0000000..34f6690 --- /dev/null +++ b/test/tests/Printer/PatternModificationTest.php @@ -0,0 +1,25 @@ +setLogFile($logFile); + $log->parse(); + + $printer = new ModifiableDefaultPrinter(); + $printer->addModification(new PatternModification('/foo/', 'bar')); + $printer->setLog($log); + $this->assertEquals("This is bar!", trim($printer->print())); + } +}