Initial import from aternosorg/codex-minecraft
Some checks failed
Tests / Run tests on PHP v8.4 (push) Failing after 32s
Tests / Run tests on PHP v8.5 (push) Failing after 2s

This commit is contained in:
2026-04-30 09:56:57 -05:00
commit 7c7fe5ca80
94 changed files with 7003 additions and 0 deletions

43
.github/workflows/tests.yaml vendored Normal file
View File

@@ -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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
vendor
.phpunit.result.cache

19
LICENSE Normal file
View File

@@ -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.

115
README.md Normal file
View File

@@ -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
<?php
$logFile = new \Aternos\Codex\Log\File\StringLogFile("This is the log content");
$logFile = new \Aternos\Codex\Log\File\PathLogFile("/path/to/log");
$logFile = new \Aternos\Codex\Log\File\StreamLogFile(fopen("/path/to/log", "r"));
```
### Log
A [`Log`](src/Log/Log.php) object implementing the [`LogInterface`](src/Log/LogInterface.php) is the most important object
for the different operations. It represents the log content, which is split in [Entries](src/Log/EntryInterface.php) and [Lines](src/Log/LineInterface.php).
And it offers quick access to the detection, parsing and analysing functions and can define which classes are used
for those functions. If you know which log type you have or just want to test the default [Log](src/Log/Log.php) class, you can
directly create a new instance, otherwise you can use detection as described below.
```php
<?php
$log = new \Aternos\Codex\Log\Log();
$log->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
<?php
$detective = new \Aternos\Codex\Detective\Detective();
$detective->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
<?php
$log->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
<?php
$analysis = $log->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
<?php
$printer = new \Aternos\Codex\Printer\DefaultPrinter();
$printer->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();
```

32
composer.json Normal file
View File

@@ -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"
}
}

1695
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
phpunit.xml Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true">
<testsuites>
<testsuite name="tests">
<directory>test/tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

27
src/Analyser/Analyser.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace Aternos\Codex\Analyser;
use Aternos\Codex\Log\AnalysableLogInterface;
/**
* Class Analyser
*
* @package Aternos\Codex\Analyser
*/
abstract class Analyser implements AnalyserInterface
{
protected ?AnalysableLogInterface $log = null;
/**
* Set the log
*
* @param AnalysableLogInterface $log
* @return $this
*/
public function setLog(AnalysableLogInterface $log): static
{
$this->log = $log;
return $this;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Aternos\Codex\Analyser;
use Aternos\Codex\Analysis\AnalysisInterface;
use Aternos\Codex\Log\AnalysableLogInterface;
/**
* Interface AnalyserInterface
*
* @package Aternos\Codex\Analyser
*/
interface AnalyserInterface
{
/**
* Set the log
*
* @param AnalysableLogInterface $log
* @return $this
*/
public function setLog(AnalysableLogInterface $log): static;
/**
* Analyse a log and return an Analysis
*
* @return AnalysisInterface
*/
public function analyse(): AnalysisInterface;
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Aternos\Codex\Analyser;
use Aternos\Codex\Analysis\Analysis;
use Aternos\Codex\Analysis\AnalysisInterface;
use Aternos\Codex\Analysis\PatternInsightInterface;
use Aternos\Codex\Log\EntryInterface;
use InvalidArgumentException;
/**
* Class PatternAnalyser
*
* @package Aternos\Codex\Analyser
*/
class PatternAnalyser extends Analyser
{
/**
* @var class-string<PatternInsightInterface>[]
*/
protected array $possibleInsightClasses = [];
/**
* Set possible insight classes
*
* Every class must implement PatternInsightInterface
*
* @param class-string<PatternInsightInterface>[] $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<PatternInsightInterface> $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<PatternInsightInterface> $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<PatternInsightInterface> $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<PatternInsightInterface> $parentInsightClass
* @param class-string<PatternInsightInterface> $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;
}
}

238
src/Analysis/Analysis.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
namespace Aternos\Codex\Analysis;
use Aternos\Codex\Log\LogInterface;
/**
* Class Analysis
*
* @package Aternos\Codex\Analysis
*/
class Analysis implements AnalysisInterface
{
/**
* @var InsightInterface[]
*/
protected array $insights = [];
protected int $iterator = 0;
protected ?LogInterface $log = null;
/**
* Set all insights at once in an array replacing the current insights
*
* @param InsightInterface[] $insights
* @return $this
*/
public function setInsights(array $insights = []): static
{
foreach ($insights as $insight) {
$insight->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<InsightInterface> $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;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Aternos\Codex\Analysis;
use ArrayAccess;
use Aternos\Codex\Log\LogInterface;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface AnalysisInterface
*
* @package Aternos\Codex\Analysis
*/
interface AnalysisInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
{
/**
* Set the log
*
* @param LogInterface $log
* @return $this
*/
public function setLog(LogInterface $log): static;
/**
* Get the log
*
* @return LogInterface|null
*/
public function getLog(): ?LogInterface;
/**
* Set all insights at once in an array replacing the current insights
*
* @param InsightInterface[] $insights
* @return $this
*/
public function setInsights(array $insights = []): static;
/**
* Add an insight
*
* @param InsightInterface $insight
* @return $this
*/
public function addInsight(InsightInterface $insight): static;
/**
* Get all insights
*
* @return InsightInterface[]
*/
public function getInsights(): array;
/**
* Get all problem insights
*
* @return ProblemInterface[]
*/
public function getProblems(): array;
/**
* Get all information insights
*
* @return InformationInterface[]
*/
public function getInformation(): array;
/**
* Get all insights that are extended from $extendedFrom (class name)
*
* @param class-string<InsightInterface> $extendedFrom
* @return InsightInterface[]
*/
public function getFilteredInsights(string $extendedFrom): array;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Interface AutomatableSolutionInterface
*
* This interface should be used to indicate
* that a solution can be solved automatically
* e.g. deletion/creation/modification of files
*
* @package Aternos\Codex\Analysis
*/
interface AutomatableSolutionInterface extends SolutionInterface
{
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Class Information
*
* @package Aternos\Codex\Analysis
*/
abstract class Information extends Insight implements InformationInterface
{
protected ?string $label = null;
protected mixed $value = null;
/**
* Get the information label
*
* @return string
*/
public function getLabel(): string
{
return $this->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()
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Interface InformationInterface
*
* @package Aternos\Codex\Analysis
*/
interface InformationInterface extends InsightInterface
{
/**
* Get the information label
*
* @return string
*/
public function getLabel(): string;
/**
* Set the information value
*
* @param mixed $value
* @return $this
*/
public function setValue(mixed $value): static;
/**
* Get the information value
*
* @return mixed
*/
public function getValue(): mixed;
}

119
src/Analysis/Insight.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
namespace Aternos\Codex\Analysis;
use Aternos\Codex\Log\EntryInterface;
use Aternos\Codex\Log\LogInterface;
/**
* Class Insight
*
* @package Aternos\Codex\Analysis
*/
abstract class Insight implements InsightInterface
{
protected ?AnalysisInterface $analysis = null;
protected ?EntryInterface $entry = null;
protected int $counter = 1;
/**
* Set the related entry
*
* @param EntryInterface $entry
* @return $this
*/
public function setEntry(EntryInterface $entry): static
{
$this->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();
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Aternos\Codex\Analysis;
use Aternos\Codex\Log\EntryInterface;
use JsonSerializable;
/**
* Interface InsightInterface
*
* @package Aternos\Codex\Analysis
*/
interface InsightInterface extends JsonSerializable
{
/**
* Get a human-readable message
*
* @return string
*/
public function getMessage(): string;
/**
* @return string
*/
public function __toString(): string;
/**
* Set the related entry
*
* @param EntryInterface $entry
* @return $this
*/
public function setEntry(EntryInterface $entry): static;
/**
* Get the related entry
*
* @return EntryInterface
*/
public function getEntry(): EntryInterface;
/**
* Check if the $insight object is equal with the current object
*
* @param InsightInterface $insight
* @return bool
*/
public function isEqual(InsightInterface $insight): bool;
/**
* Increase the counter for this insight
*
* @return $this
*/
public function increaseCounter(): static;
/**
* Get the current counter value
*
* @return int
*/
public function getCounterValue(): int;
/**
* Set the related analysis
*
* @param AnalysisInterface $analysis
* @return $this
*/
public function setAnalysis(AnalysisInterface $analysis): static;
/**
* Get the related analysis
*
* @return AnalysisInterface|null
*/
public function getAnalysis(): ?AnalysisInterface;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Interface PatternInsightInterface
*
* @package Aternos\Codex\Analysis
*/
interface PatternInsightInterface extends InsightInterface
{
/**
* Get an array of possible patterns
*
* The array key of the pattern will be passed to setMatches()
*
* @return string[]
*/
public static function getPatterns(): array;
/**
* Apply the matches from the pattern
*
* @param array $matches
* @param mixed $patternKey
* @return void
*/
public function setMatches(array $matches, mixed $patternKey): void;
}

168
src/Analysis/Problem.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Class Problem
*
* @package Aternos\Codex\Analysis
*/
abstract class Problem extends Insight implements ProblemInterface
{
/**
* @var SolutionInterface[]
*/
protected array $solutions = [];
/**
* @var int
*/
protected int $iterator = 0;
/**
* Set all solutions at once in an array replacing the current solutions
*
* @param SolutionInterface[] $solutions
* @return $this
*/
public function setSolutions(array $solutions = []): static
{
$this->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()
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Aternos\Codex\Analysis;
use ArrayAccess;
use Countable;
use Iterator;
/**
* Interface ProblemInterface
*
* @package Aternos\Codex\Analysis
*/
interface ProblemInterface extends Iterator, Countable, ArrayAccess, InsightInterface
{
/**
* Set all solutions at once in an array replacing the current solutions
*
* @param SolutionInterface[] $solutions
* @return $this
*/
public function setSolutions(array $solutions = []): static;
/**
* Add a solution
*
* @param SolutionInterface $solution
* @return $this
*/
public function addSolution(SolutionInterface $solution): static;
/**
* Get all solutions
*
* @return SolutionInterface[]
*/
public function getSolutions(): array;
}

29
src/Analysis/Solution.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace Aternos\Codex\Analysis;
/**
* Class Solution
*
* @package Aternos\Codex\Analysis
*/
abstract class Solution implements SolutionInterface
{
/**
* @return string
*/
public function __toString(): string
{
return $this->getMessage();
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'message' => $this->getMessage()
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Aternos\Codex\Analysis;
use JsonSerializable;
/**
* Interface SolutionInterface
*
* @package Aternos\Codex\Analysis
*/
interface SolutionInterface extends JsonSerializable
{
/**
* Get the solution as a human-readable message
*
* @return string
*/
public function getMessage(): string;
public function __toString(): string;
}

149
src/Detective/Detective.php Normal file
View File

@@ -0,0 +1,149 @@
<?php
namespace Aternos\Codex\Detective;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\File\LogFileInterface;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Log\LogInterface;
use InvalidArgumentException;
/**
* Class Detective
*
* @package Aternos\Codex\Detective
*/
class Detective implements DetectiveInterface
{
/**
* @var class-string<LogInterface>[]
*/
protected array $possibleLogClasses = [];
/**
* @var class-string<LogInterface>
*/
protected string $defaultLogClass = Log::class;
protected ?LogFileInterface $logFile = null;
/**
* Set possible log classes
*
* Every class must implement DetectableLogInterface
*
* @param class-string<LogInterface>[] $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<LogInterface> $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);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Aternos\Codex\Detective;
use Aternos\Codex\Log\File\LogFileInterface;
use Aternos\Codex\Log\LogInterface;
/**
* Interface DetectiveInterface
*
* @package Aternos\Codex\Detective
*/
interface DetectiveInterface
{
/**
* Set possible log classes
*
* Every class must implement DetectableLogInterface
*
* @param class-string<LogInterface>[] $logClasses
* @return $this
*/
public function setPossibleLogClasses(array $logClasses): static;
/**
* Add a possible log class
*
* The class must implement DetectableLogInterface
*
* @param class-string<LogInterface> $logClass
* @return $this
*/
public function addPossibleLogClass(string $logClass): static;
/**
* Get all possible log classes
*
* @return class-string<LogInterface>[]
*/
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;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Aternos\Codex\Detective;
use Aternos\Codex\Log\File\LogFileInterface;
/**
* Class Detector
*
* @package Aternos\Codex\Detective
*/
abstract class Detector implements DetectorInterface
{
protected ?LogFileInterface $logFile = null;
/**
* Set the log file
*
* @param LogFileInterface $logFile
* @return $this
*/
public function setLogFile(LogFileInterface $logFile): static
{
$this->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());
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Aternos\Codex\Detective;
use Aternos\Codex\Log\File\LogFileInterface;
/**
* Interface DetectorInterface
*
* @package Aternos\Codex\Detective
*/
interface DetectorInterface
{
/**
* Set the log file
*
* @param LogFileInterface $logFile
* @return $this
*/
public function setLogFile(LogFileInterface $logFile): static;
/**
* Detect if the log matches
*
* Return true to directly force the detective to accept your result without considering any other detector
* Return false to force the detective to never use your result
* Return a number between 0 and 1 as probability for this detector
* Possible algorithm to get this number would be (matching lines) / (total lines)
*
* The detective decides which detector wins (and which related log class to use) in this order:
* return === true
* highest return
* default log
*
* @return bool|float
*/
public function detect(): float|bool;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Aternos\Codex\Detective;
/**
* Class LinePatternDetector
*
* @package Aternos\Codex\Detective
*/
class LinePatternDetector extends PatternDetector
{
/**
* Detect if the log matches
*
* Counts the lines matching the pattern and returns the percentage from 0-1 for the detective
*
* Returns false when no match is found
*
* @return bool|float
*/
public function detect(): bool|float
{
$lines = $this->getLogContentAsArray();
$matchingCounter = 0;
foreach ($lines as $line) {
if (preg_match($this->pattern, $line) === 1) {
$matchingCounter++;
}
}
if ($matchingCounter === 0) {
return false;
}
return $matchingCounter / count($lines);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Aternos\Codex\Detective;
/**
* MultiPatternDetector can detect multiple patterns in a log and return true if all patterns are found
*/
class MultiPatternDetector extends Detector
{
protected array $patterns = [];
/**
* Add a pattern to the list of patterns to detect
*
* @param string $pattern
* @return $this
*/
public function addPattern(string $pattern): static
{
$this->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;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Aternos\Codex\Detective;
/**
* Class PatternDetector
*
* @package Aternos\Codex\Detective
*/
abstract class PatternDetector extends Detector
{
protected ?string $pattern = null;
/**
* Set the matching pattern for one line
*
* @param string $pattern
* @return $this
*/
public function setPattern(string $pattern): static
{
$this->pattern = $pattern;
return $this;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Aternos\Codex\Detective;
/**
* Class SinglePatternDetector
*
* @package Aternos\Codex\Detective
*/
class SinglePatternDetector extends PatternDetector
{
/**
* 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 (preg_match($this->pattern, $this->getLogContent()) === 1) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Aternos\Codex\Detective;
/**
* Class WeightedSinglePatternDetector
*
* @package Aternos\Codex\Detective
*/
class WeightedSinglePatternDetector extends SinglePatternDetector
{
protected ?float $weight = null;
/**
* Set the weight that will be returned if the pattern matches
*
* @param float $weight
* @return $this
*/
public function setWeight(float $weight): static
{
$this->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;
}
}
}

50
src/Log/AnalysableLog.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
namespace Aternos\Codex\Log;
use Aternos\Codex\Analyser\AnalyserInterface;
use Aternos\Codex\Analysis\AnalysisInterface;
/**
* Class AnalysableLog
*
* @package Aternos\Codex\Log
*/
abstract class AnalysableLog extends Log implements AnalysableLogInterface
{
protected ?AnalysisInterface $analysis = null;
/**
* Analyse a log file with an analyser
*
* Every log type should have a default analyser,
* but the $analyser argument can be used to override
* the default analyser
*
* @param AnalyserInterface|null $analyser
* @return AnalysisInterface
*/
public function analyse(?AnalyserInterface $analyser = null): AnalysisInterface
{
if ($this->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()
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Aternos\Codex\Log;
use Aternos\Codex\Analyser\AnalyserInterface;
use Aternos\Codex\Analysis\AnalysisInterface;
/**
* Interface AnalysableLogInterface
*
* @package Aternos\Codex\Log
*/
interface AnalysableLogInterface
{
/**
* Get the default analyser
*
* @return AnalyserInterface
*/
public static function getDefaultAnalyser(): AnalyserInterface;
/**
* Analyse a log file with an analyser
*
* Every log type should have a default analyser,
* but the $analyser argument can be used to override
* the default analyser
*
* @param AnalyserInterface|null $analyser
* @return AnalysisInterface
*/
public function analyse(?AnalyserInterface $analyser = null): AnalysisInterface;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Aternos\Codex\Log;
use Aternos\Codex\Detective\DetectorInterface;
/**
* Interface DetectableLogInterface
*
* @package Aternos\Codex\Log
*/
interface DetectableLogInterface extends LogInterface
{
/**
* Get an array of detectors matching DetectorInterface
*
* @return DetectorInterface[]
*/
public static function getDetectors(): array;
}

244
src/Log/Entry.php Normal file
View File

@@ -0,0 +1,244 @@
<?php
namespace Aternos\Codex\Log;
/**
* Class Entry
*
* @package Aternos\Codex\Log
*/
class Entry implements EntryInterface
{
/**
* @var LineInterface[]
*/
protected array $lines = [];
protected ?LevelInterface $level = null;
protected ?int $time = null;
protected ?string $prefix = null;
protected int $iterator = 0;
/**
* Set all lines at once in an array replacing the current lines
*
* @param LineInterface[] $lines
* @return $this
*/
public function setLines(array $lines = []): static
{
$this->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()
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Aternos\Codex\Log;
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface EntryInterface
*
* @package Aternos\Codex\Log
*/
interface EntryInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
{
/**
* Set all lines at once in an array replacing the current lines
*
* @param LineInterface[] $lines
* @return $this
*/
public function setLines(array $lines = []): static;
/**
* Add a line
*
* @param LineInterface $line
* @return $this
*/
public function addLine(LineInterface $line): static;
/**
* Get all lines
*
* @return LineInterface[]
*/
public function getLines(): array;
/**
* @return string
*/
public function __toString(): string;
/**
* Return the current element
*
* @return LineInterface
*/
public function current(): LineInterface;
/**
* Offset to set
*
* @param mixed $offset
* @param LineInterface $value
*/
public function offsetSet(mixed $offset, mixed $value): void;
/**
* Offset to retrieve
*
* @param mixed $offset
* @return LineInterface
*/
public function offsetGet(mixed $offset): LineInterface;
}

23
src/Log/File/LogFile.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace Aternos\Codex\Log\File;
/**
* Class LogFile
*
* @package Aternos\Codex\Log\File
*/
abstract class LogFile implements LogFileInterface
{
protected ?string $content = null;
/**
* Get the log file content
*
* @return string
*/
public function getContent(): string
{
return $this->content;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Aternos\Codex\Log\File;
/**
* Interface LogFileInterface
*
* @package Aternos\Codex\Log\File
*/
interface LogFileInterface
{
/**
* Get the log file content
*
* @return string
*/
public function getContent(): string;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Aternos\Codex\Log\File;
use InvalidArgumentException;
/**
* Class PathLogFile
*
* @package Aternos\Codex\Log\File
*/
class PathLogFile extends LogFile
{
/**
* PathLogFile constructor.
*
* @param string $path
*/
public function __construct(string $path)
{
if (!file_exists($path)) {
throw new InvalidArgumentException("File '" . $path . "' not found.");
}
$this->content = file_get_contents($path);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Aternos\Codex\Log\File;
use InvalidArgumentException;
/**
* Class StreamLogFile
*
* @package Aternos\Codex\Log\File
*/
class StreamLogFile extends LogFile
{
/**
* StreamLogFile constructor.
*
* @param resource $streamResource
*/
public function __construct($streamResource)
{
if (!is_resource($streamResource)) {
throw new InvalidArgumentException("Stream argument is not a resource");
}
$this->content = '';
while (!feof($streamResource)) {
$this->content .= fread($streamResource, 8192);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Aternos\Codex\Log\File;
/**
* Class StringLogFile
*
* @package Aternos\Codex\Log\File
*/
class StringLogFile extends LogFile
{
/**
* StringLogFile constructor.
*
* @param string $string
*/
public function __construct(string $string)
{
$this->content = $string;
}
}

66
src/Log/Level.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
namespace Aternos\Codex\Log;
enum Level: int implements LevelInterface
{
case EMERGENCY = 0;
case ALERT = 1;
case CRITICAL = 2;
case ERROR = 3;
case WARNING = 4;
case NOTICE = 5;
case INFO = 6;
case DEBUG = 7;
/**
* @param string $level
* @return Level
*/
public static function fromString(string $level): Level
{
return match (strtolower($level)) {
"emergency" => 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;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Log;
use JsonSerializable;
interface LevelInterface extends JsonSerializable
{
/**
* @param string $level
* @return LevelInterface
*/
public static function fromString(string $level): LevelInterface;
/**
* @return string
*/
public function asString(): string;
/**
* @return int
*/
public function asInt(): int;
}

84
src/Log/Line.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
namespace Aternos\Codex\Log;
/**
* Class Line
*
* @package Aternos\Codex\Log
*/
class Line implements LineInterface
{
/**
* @param int $number
* @param string $text
*/
public function __construct(
protected int $number,
protected string $text)
{
}
/**
* Set the text of the line
*
* @param string $text
* @return $this
*/
public function setText(string $text): static
{
$this->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()
];
}
}

48
src/Log/LineInterface.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace Aternos\Codex\Log;
use JsonSerializable;
/**
* Interface LineInterface
*
* @package Aternos\Codex\Log
*/
interface LineInterface extends JsonSerializable
{
/**
* Set the text of the line
*
* @param string $text
* @return $this
*/
public function setText(string $text): static;
/**
* Get the text of the line
*
* @return string
*/
public function getText(): string;
/**
* Set the line number
*
* @param int $number
* @return $this
*/
public function setNumber(int $number): static;
/**
* Get the line number
*
* @return int
*/
public function getNumber(): int;
/**
* @return string
*/
public function __toString(): string;
}

253
src/Log/Log.php Normal file
View File

@@ -0,0 +1,253 @@
<?php
namespace Aternos\Codex\Log;
use Aternos\Codex\Log\File\LogFileInterface;
use Aternos\Codex\Parser\DefaultParser;
use Aternos\Codex\Parser\ParserInterface;
/**
* Class Log
*
* @package Aternos\Codex\Log
*/
class Log implements LogInterface
{
/**
* @var EntryInterface[]
*/
protected array $entries = [];
protected int $iterator = 0;
protected ?LogFileInterface $logFile = null;
protected bool $includeEntries = true;
/**
* Get the default parser
*
* @return ParserInterface
*/
public static function getDefaultParser(): ParserInterface
{
return new DefaultParser();
}
/**
* Set the log file
*
* @param LogFileInterface $logFile
* @return $this
*/
public function setLogFile(LogFileInterface $logFile): static
{
$this->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";
}
}

110
src/Log/LogInterface.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
namespace Aternos\Codex\Log;
use ArrayAccess;
use Aternos\Codex\Log\File\LogFileInterface;
use Aternos\Codex\Parser\ParserInterface;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface LogInterface
*
* @package Aternos\Codex\Log
*/
interface LogInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
{
/**
* Get the default parser
*
* @return ParserInterface
*/
public static function getDefaultParser(): ParserInterface;
/**
* Set the log file
*
* @param LogFileInterface $logFile
* @return $this
*/
public function setLogFile(LogFileInterface $logFile): static;
/**
* Get the log file
*
* @return LogFileInterface
*/
public function getLogFile(): LogFileInterface;
/**
* Get a human-readable title for the log
*
* @return string
*/
public function getTitle(): string;
/**
* 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;
/**
* Set all entries of the log at once replacing the current entries
*
* @param EntryInterface[] $entries
* @return $this
*/
public function setEntries(array $entries = []): static;
/**
* Add an entry to the log
*
* @param EntryInterface $entry
* @return $this
*/
public function addEntry(EntryInterface $entry): static;
/**
* Get all entries of the log
*
* @return EntryInterface[]
*/
public function getEntries(): array;
/**
* @return string
*/
public function __toString(): string;
/**
* Return the current element
*
* @return EntryInterface
*/
public function current(): EntryInterface;
/**
* Offset to set
*
* @param mixed $offset
* @param EntryInterface $value
*/
public function offsetSet(mixed $offset, mixed $value): void;
/**
* Offset to retrieve
*
* @param mixed $offset
* @return EntryInterface
*/
public function offsetGet(mixed $offset): EntryInterface;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Aternos\Codex\Parser;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\Line;
/**
* Class DefaultParser
*
* @package Aternos\Codex\Parser
*/
class DefaultParser extends Parser
{
/**
* Parse a log from resource to Log object
*/
public function parse(): void
{
foreach ($this->getLogContentAsArray() as $number => $logLineString) {
$this->log->addEntry((new Entry())
->addLine(new Line($number + 1, $logLineString))
);
}
}
}

47
src/Parser/Parser.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
namespace Aternos\Codex\Parser;
use Aternos\Codex\Log\LogInterface;
/**
* Class Parser
*
* @package Aternos\Codex\Parser
*/
abstract class Parser implements ParserInterface
{
protected ?LogInterface $log = null;
/**
* Set the output log object
*
* @param LogInterface $log
* @return $this
*/
public function setLog(LogInterface $log): static
{
$this->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());
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Aternos\Codex\Parser;
use Aternos\Codex\Log\LogInterface;
/**
* Interface ParserInterface
*
* @package Aternos\Codex\Parser
*/
interface ParserInterface
{
/**
* Set the output log object
*
* @param LogInterface $log
* @return $this
*/
public function setLog(LogInterface $log): static;
/**
* Parse a log from resource to Log object
*/
public function parse(): void;
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Aternos\Codex\Parser;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\EntryInterface;
use Aternos\Codex\Log\Level;
use Aternos\Codex\Log\LevelInterface;
use Aternos\Codex\Log\Line;
use DateTime;
use DateTimeZone;
use InvalidArgumentException;
/**
* Class PatternParser
*
* @package Aternos\Codex\Parser
*/
class PatternParser extends Parser
{
/**
* Match constants, see setMatches()
*/
public const string TIME = "time";
public const string LEVEL = "level";
public const string PREFIX = "prefix";
/**
* @var class-string<EntryInterface>
*/
protected string $entryClass = Entry::class;
/**
* @noinspection PhpDocFieldTypeMismatchInspection
* @var class-string<LevelInterface>|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.");
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Printer;
use Aternos\Codex\Log\LineInterface;
/**
* Class DefaultPrinter
*
* @package Aternos\Codex\Printer
*/
class DefaultPrinter extends Printer
{
/**
* Print a line
*
* @param LineInterface $line
* @return string
*/
protected function printLine(LineInterface $line): string
{
return $line->getText() . PHP_EOL;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Printer;
use Aternos\Codex\Log\LineInterface;
/**
* Class ModifiableDefaultPrinter
*
* @package Aternos\Codex\Printer
*/
class ModifiableDefaultPrinter extends ModifiablePrinter
{
/**
* Print a line
*
* @param LineInterface $line
* @return string
*/
protected function printLine(LineInterface $line): string
{
return $this->runModifications($line->getText()) . PHP_EOL;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Aternos\Codex\Printer;
/**
* Class ModifiablePrinter
*
* @package Aternos\Codex\Printer
*/
abstract class ModifiablePrinter extends Printer implements ModifiablePrinterInterface
{
/**
* @var ModificationInterface[]
*/
protected array $modifications = [];
/**
* Set all modifications replacing the current modifications
*
* @param ModificationInterface[] $modifications
* @return $this
*/
public function setModifications(array $modifications): static
{
$this->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;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Aternos\Codex\Printer;
/**
* Interface ModifiablePrinterInterface
*
* @package Aternos\Codex\Printer
*/
interface ModifiablePrinterInterface extends PrinterInterface
{
/**
* Set all modifications replacing the current modifications
*
* @param ModificationInterface[] $modifications
* @return $this
*/
public function setModifications(array $modifications): static;
/**
* Add a modification
*
* @param ModificationInterface $modification
* @return $this
*/
public function addModification(ModificationInterface $modification): static;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Aternos\Codex\Printer;
/**
* Class Modification
*
* @package Aternos\Codex\Printer
*/
abstract class Modification implements ModificationInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Aternos\Codex\Printer;
/**
* Interface ModificationInterface
*
* @package Aternos\Codex\Printer
*/
interface ModificationInterface
{
/**
* Modify the given string and return it
*
* @param string $text
* @return string
*/
public function modify(string $text): string;
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Aternos\Codex\Printer;
/**
* Class PatternModification
*
* @package Aternos\Codex\Printer
*/
class PatternModification extends Modification
{
/**
* @param string $pattern
* @param string $replacement
*/
public function __construct(
protected string $pattern,
protected string $replacement)
{
}
/**
* Set the pattern
*
* See http://php.net/manual/de/function.preg-replace.php
*
* @param string $pattern
* @return $this
*/
public function setPattern(string $pattern): PatternModification
{
$this->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);
}
}

99
src/Printer/Printer.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
namespace Aternos\Codex\Printer;
use Aternos\Codex\Log\EntryInterface;
use Aternos\Codex\Log\LineInterface;
use Aternos\Codex\Log\LogInterface;
/**
* Class Printer
*
* @package Aternos\Codex\Printer
*/
abstract class Printer implements PrinterInterface
{
protected ?LogInterface $log = null;
protected ?EntryInterface $entry = null;
/**
* Set the log
*
* @param LogInterface $log
* @return $this
*/
public function setLog(LogInterface $log): static
{
$this->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;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Aternos\Codex\Printer;
use Aternos\Codex\Log\EntryInterface;
use Aternos\Codex\Log\LogInterface;
/**
* Interface PrinterInterface
*
* @package Aternos\Codex\Printer
*/
interface PrinterInterface
{
/**
* Set the log
*
* @param LogInterface $log
* @return $this
*/
public function setLog(LogInterface $log): static;
/**
* Set the entry
*
* @param EntryInterface $entry
* @return $this
*/
public function setEntry(EntryInterface $entry): static;
/**
* Print the log
*
* @return string
*/
public function print(): string;
}

8
test/data/problem.log Normal file
View File

@@ -0,0 +1,8 @@
[01.01.1970 00:00:01] [Log/INFO] This is the first message containing no problem
[01.01.1970 00:00:02] [Log/ERROR] I have a problem with ABC
[01.01.1970 00:00:03] [Log/INFO] This is a message without any problem
[01.01.1970 00:00:04] [Log/ERROR] I have a problem with XYZ
[01.01.1970 00:00:05] [Log/ERROR] I have a problem with ABC
[01.01.1970 00:00:06] [Log/ERROR] I have a problem with DEF
I have a problem with GHI
[01.01.1970 00:00:07] [Log/INFO] This log was generated by software v1.2.3

7
test/data/simple.log Normal file
View File

@@ -0,0 +1,7 @@
[01.01.1970 00:00:01] [Log/INFO] This is the first message containing information.
[01.01.1970 00:00:02] [Log/DEBUG] This is the second message containing a debug information.
[01.01.1970 00:00:03] [Log/WARN] This is the third message containing a warning information.
[01.01.1970 00:00:04] [Log/ERROR] This is the third message containing an error information.
This line continues the error entry to add even more information.
This line is also part of the error entry.
[01.01.1970 00:00:05] [Log/INFO] This is the last message of the log.

View File

@@ -0,0 +1,13 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\Information;
/**
* Class TestInformation
*/
class TestInformation extends Information
{
protected ?string $label = "Label";
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\Insight;
use Aternos\Codex\Analysis\InsightInterface;
/**
* Class TestInsight
*/
class TestInsight extends Insight
{
/**
* Get the insight as human-readable message
*
* @return string
*/
public function getMessage(): string
{
return "This is a test insight";
}
/**
* Check if the $insight object is equal with the current object
*
* @param InsightInterface $insight
* @return bool
*/
public function isEqual(InsightInterface $insight): bool
{
return false;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\Information;
use Aternos\Codex\Analysis\PatternInsightInterface;
/**
* Class TestPatternInformation
*/
class TestPatternInformation extends Information implements PatternInsightInterface
{
protected ?string $label = "Software version";
/**
* 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 ['/This log was generated by software (v[0-9\.]*)/'];
}
/**
* Apply the matches from the pattern
*
* @param array $matches
* @param mixed $patternKey
* @return void
*/
public function setMatches(array $matches, mixed $patternKey): void
{
$this->value = $matches[1];
}
public function getLogContent(): ?string
{
return parent::getLogContent();
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\InsightInterface;
use Aternos\Codex\Analysis\PatternInsightInterface;
use Aternos\Codex\Analysis\Problem;
/**
* Class TestPatternProblem
*/
class TestPatternProblem extends Problem implements PatternInsightInterface
{
protected ?string $cause = null;
/**
* @param string $cause
* @return TestPatternProblem
*/
public function setCause(string $cause): static
{
$this->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;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\InsightInterface;
use Aternos\Codex\Analysis\Problem;
/**
* Class TestProblem
*/
class TestProblem extends Problem
{
/**
* Get the problem as human-readable message
*
* @return string
*/
public function getMessage(): string
{
return "This is a test problem";
}
/**
* Check if the $insight object is equal with the current object
*
* @param InsightInterface $insight
* @return bool
*/
public function isEqual(InsightInterface $insight): bool
{
return false;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Aternos\Codex\Test\Src\Analysis;
use Aternos\Codex\Analysis\Solution;
/**
* Class TestSolution
*/
class TestSolution extends Solution
{
/**
* Get the solution as a human-readable message
*
* @return string
*/
public function getMessage(): string
{
return "This is a test solution.";
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Test\Src\Log;
use Aternos\Codex\Detective\DetectorInterface;
use Aternos\Codex\Detective\SinglePatternDetector;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\Log;
/**
* Class TestAlwaysDetectableLog
*/
class TestAlwaysDetectableLog extends Log implements DetectableLogInterface
{
/**
* Get an array of detectors matching DetectorInterface
*
* @return DetectorInterface[]
*/
public static function getDetectors(): array
{
return [(new SinglePatternDetector())->setPattern('/information/')];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Test\Src\Log;
use Aternos\Codex\Detective\DetectorInterface;
use Aternos\Codex\Detective\LinePatternDetector;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\Log;
/**
* Class TestLessDetectableLog
*/
class TestLessDetectableLog extends Log implements DetectableLogInterface
{
/**
* Get an array of detectors matching DetectorInterface
*
* @return DetectorInterface[]
*/
public static function getDetectors(): array
{
return [(new LinePatternDetector())->setPattern('/information/')];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Test\Src\Log;
use Aternos\Codex\Detective\DetectorInterface;
use Aternos\Codex\Detective\LinePatternDetector;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\Log;
/**
* Class TestMoreDetectableLog
*/
class TestMoreDetectableLog extends Log implements DetectableLogInterface
{
/**
* Get an array of detectors matching DetectorInterface
*
* @return DetectorInterface[]
*/
public static function getDetectors(): array
{
return [(new LinePatternDetector())->setPattern('/This/')];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Aternos\Codex\Test\Src\Log;
use Aternos\Codex\Detective\DetectorInterface;
use Aternos\Codex\Detective\SinglePatternDetector;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\Log;
/**
* Class TestNeverDetectableLog
*/
class TestNeverDetectableLog extends Log implements DetectableLogInterface
{
/**
* Get an array of detectors matching DetectorInterface
*
* @return DetectorInterface[]
*/
public static function getDetectors(): array
{
return [(new SinglePatternDetector())->setPattern('/missing/')];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Aternos\Codex\Test\Src\Log;
use Aternos\Codex\Analyser\PatternAnalyser;
use Aternos\Codex\Log\AnalysableLog;
use Aternos\Codex\Parser\PatternParser;
use Aternos\Codex\Test\Src\Analysis\TestPatternInformation;
use Aternos\Codex\Test\Src\Analysis\TestPatternProblem;
/**
* Class TestLog
*/
class TestPatternLog extends AnalysableLog
{
/**
* Get the default parser
*
* @return PatternParser
*/
public static function getDefaultParser(): PatternParser
{
return (new PatternParser())
->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);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Aternos\Codex\Test\Src\Printer;
use Aternos\Codex\Printer\Modification;
/**
* Class TestModification
*/
class TestModification extends Modification
{
/**
* Modify the given string and return it
*
* @param string $text
* @return string
*/
public function modify(string $text): string
{
return str_replace("foo", "bar", $text);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Aternos\Codex\Test\Tests\Analyser;
use Aternos\Codex\Analyser\PatternAnalyser;
use Aternos\Codex\Analysis\Analysis;
use Aternos\Codex\Analysis\PatternInsightInterface;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\File\PathLogFile;
use Aternos\Codex\Log\Level;
use Aternos\Codex\Log\Line;
use Aternos\Codex\Test\Src\Analysis\TestPatternInformation;
use Aternos\Codex\Test\Src\Analysis\TestPatternProblem;
use Aternos\Codex\Test\Src\Analysis\TestSolution;
use Aternos\Codex\Test\Src\Log\TestPatternLog;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class PatternAnalyserTest extends TestCase
{
/**
* @return Analysis
*/
protected function getExpectedAnalysis(): Analysis
{
return (new Analysis())
->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));
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Aternos\Codex\Test\Tests\Analysis;
use Aternos\Codex\Analysis\Analysis;
use Aternos\Codex\Test\Src\Analysis\TestInformation;
use Aternos\Codex\Test\Src\Analysis\TestInsight;
use Aternos\Codex\Test\Src\Analysis\TestPatternProblem;
use Aternos\Codex\Test\Src\Analysis\TestProblem;
use PHPUnit\Framework\TestCase;
class AnalysisTest extends TestCase
{
public function testSetGetInsights(): void
{
$analysis = new Analysis();
$insight = new TestInsight();
$this->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);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Aternos\Codex\Test\Tests\Analysis;
use Aternos\Codex\Log\File\PathLogFile;
use Aternos\Codex\Test\Src\Analysis\TestInformation;
use Aternos\Codex\Test\Src\Analysis\TestPatternInformation;
use Aternos\Codex\Test\Src\Log\TestPatternLog;
use PHPUnit\Framework\TestCase;
class InformationTest extends TestCase
{
public function testSetGetValue(): void
{
$value = uniqid();
$information = new TestInformation();
$information->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());
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Aternos\Codex\Test\Tests\Analysis;
use Aternos\Codex\Test\Src\Analysis\TestProblem;
use Aternos\Codex\Test\Src\Analysis\TestSolution;
use PHPUnit\Framework\TestCase;
class ProblemTest extends TestCase
{
public function testSetGetSolutions(): void
{
$problem = new TestProblem();
$solution = new TestSolution();
$this->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);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Aternos\Codex\Test\Tests\Detective;
use Aternos\Codex\Detective\Detective;
use Aternos\Codex\Detective\DetectorInterface;
use Aternos\Codex\Log\DetectableLogInterface;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Test\Src\Analysis\TestSolution;
use Aternos\Codex\Test\Src\Log\TestAlwaysDetectableLog;
use Aternos\Codex\Test\Src\Log\TestLessDetectableLog;
use Aternos\Codex\Test\Src\Log\TestMoreDetectableLog;
use Aternos\Codex\Test\Src\Log\TestNeverDetectableLog;
use PHPUnit\Framework\TestCase;
class DetectiveTest extends TestCase
{
/**
* @param array $possibleLogClasses
* @return Detective
*/
protected function getDetective(array $possibleLogClasses): Detective
{
$detective = new Detective();
$detective->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());
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Aternos\Codex\Test\Tests\Detective;
use Aternos\Codex\Detective\LinePatternDetector;
use PHPUnit\Framework\TestCase;
class LinePatternDetectorTest extends TestCase
{
public function testDetect(): void
{
$this->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()
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Aternos\Codex\Test\Tests\Detective;
use Aternos\Codex\Detective\MultiPatternDetector;
use Aternos\Codex\Log\File\StringLogFile;
use PHPUnit\Framework\TestCase;
class MultiPatternDetectorTest extends TestCase
{
public function testDetectSinglePattern(): void
{
$this->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()
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Aternos\Codex\Test\Tests\Detective;
use Aternos\Codex\Detective\SinglePatternDetector;
use Aternos\Codex\Log\File\StringLogFile;
use PHPUnit\Framework\TestCase;
class SinglePatternDetectorTest extends TestCase
{
public function testDetect(): void
{
$this->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()
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Aternos\Codex\Test\Tests\Detective;
use Aternos\Codex\Detective\WeightedSinglePatternDetector;
use Aternos\Codex\Log\File\StringLogFile;
use PHPUnit\Framework\TestCase;
class WeightedSinglePatternDetectorTest extends TestCase
{
public function testDetect(): void
{
$this->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()
);
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Aternos\Codex\Test\Tests\Log;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\Level;
use Aternos\Codex\Log\Line;
use PHPUnit\Framework\TestCase;
class EntryTest extends TestCase
{
public function testAddLine(): void
{
$entry = new Entry();
$line = new Line(1, uniqid());
$this->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);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Aternos\Codex\Test\Tests\Log\File;
use Aternos\Codex\Log\File\PathLogFile;
use PHPUnit\Framework\TestCase;
class PathLogFileTest extends TestCase
{
public function testGetContent(): void
{
$path = __DIR__ . "/../../../data/simple.log";
$logFile = new PathLogFile($path);
$this->assertStringEqualsFile($path, $logFile->getContent());
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Aternos\Codex\Test\Tests\Log\File;
use Aternos\Codex\Log\File\StreamLogFile;
use PHPUnit\Framework\TestCase;
class StreamLogFileTest extends TestCase
{
public function testGetContent(): void
{
$path = __DIR__ . "/../../../data/simple.log";
$streamResource = fopen($path, 'r');
$logFile = new StreamLogFile($streamResource);
$this->assertStringEqualsFile($path, $logFile->getContent());
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Aternos\Codex\Test\Tests\Log\File;
use Aternos\Codex\Log\File\StringLogFile;
use PHPUnit\Framework\TestCase;
class StringLogFileTest extends TestCase
{
public function testGetContent(): void
{
$content = uniqid();
$logFile = new StringLogFile($content);
$this->assertEquals($content, $logFile->getContent());
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Aternos\Codex\Test\Tests\Log;
use Aternos\Codex\Log\Line;
use PHPUnit\Framework\TestCase;
class LineTest extends TestCase
{
public function testSetGetText(): void
{
$text = uniqid();
$line = new Line(1, "");
$this->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);
}
}

103
test/tests/Log/LogTest.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
namespace Aternos\Codex\Test\Tests\Log;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\Log;
use PHPUnit\Framework\TestCase;
class LogTest extends TestCase
{
public function testAddEntry(): void
{
$log = new Log();
$entry = new Entry();
$this->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);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Aternos\Codex\Test\Tests\Parser;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\File\PathLogFile;
use Aternos\Codex\Log\Line;
use Aternos\Codex\Log\Log;
use PHPUnit\Framework\TestCase;
class DefaultParserTest extends TestCase
{
/**
* Get the log object expected from parsing data/simple.log
*
* @return Log
*/
protected function getSimpleExpectedLog(): Log
{
return (new Log())
->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);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Aternos\Codex\Test\Tests\Parser;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\File\PathLogFile;
use Aternos\Codex\Log\Level;
use Aternos\Codex\Log\Line;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Parser\PatternParser;
use Aternos\Codex\Test\Src\Log\TestPatternLog;
use PHPUnit\Framework\TestCase;
class PatternParserTest extends TestCase
{
/**
* Get the log object expected from parsing data/simple.log
*
* @return Log
*/
protected function getSimpleExpectedLog(): Log
{
return (new TestPatternLog())
->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());
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Aternos\Codex\Test\Tests\Printer;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\File\PathLogFile;
use Aternos\Codex\Log\Line;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Printer\DefaultPrinter;
use PHPUnit\Framework\TestCase;
class DefaultPrinterTest extends TestCase
{
public function testPrint(): void
{
$logFile = new PathLogFile(__DIR__ . "/../../data/simple.log");
$log = new Log();
$log->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()));
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Aternos\Codex\Test\Tests\Printer;
use Aternos\Codex\Log\Entry;
use Aternos\Codex\Log\File\StringLogFile;
use Aternos\Codex\Log\Line;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Printer\ModifiableDefaultPrinter;
use Aternos\Codex\Test\Src\Printer\TestModification;
use PHPUnit\Framework\TestCase;
class ModifiableDefaultPrinterTest extends TestCase
{
public function testPrint(): void
{
$logFile = new StringLogFile("This is foo!");
$log = new Log();
$log->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()));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Aternos\Codex\Test\Tests\Printer;
use Aternos\Codex\Log\File\StringLogFile;
use Aternos\Codex\Log\Log;
use Aternos\Codex\Printer\ModifiableDefaultPrinter;
use Aternos\Codex\Printer\PatternModification;
use PHPUnit\Framework\TestCase;
class PatternModificationTest extends TestCase
{
public function testPrint(): void
{
$logFile = new StringLogFile("This is foo!");
$log = new Log();
$log->setLogFile($logFile);
$log->parse();
$printer = new ModifiableDefaultPrinter();
$printer->addModification(new PatternModification('/foo/', 'bar'));
$printer->setLog($log);
$this->assertEquals("This is bar!", trim($printer->print()));
}
}