diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b2a13f6e..e8433537 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,9 +10,11 @@ jobs: fail-fast: true matrix: php: [7.3, 7.4] - laravel: [7.*] + laravel: [6.*, 7.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 6.* + testbench: 4.* - laravel: 7.* testbench: 5.* diff --git a/README.md b/README.md index 4240aaef..dbe061f5 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,7 @@ ### Installing -The easiest way to install the package is by using composer. The package requires PHP 7.3, Laravel 7.0 or higher or Lumen 7.0 or higher. - -Due to version conflicts, `stats` can't be installed in Laravel 7 projects which are running with phpunit 8. **Phpunit 9 is required.** +The easiest way to install the package is by using composer. The package requires PHP 7.3, Laravel 6.0 or higher or Lumen 6.0 or higher. ```shell composer require "wnx/laravel-stats" --dev @@ -42,78 +40,12 @@ If you're using Lumen you have to manually register the Service Provider in your $app->register(\Wnx\LaravelStats\StatsServiceProvider::class); ``` -Optionally, you can publish the config file of this package with this command (Laravel only): +Optionally, you can publish the config file in your Laravel applications with the following command: ```shell php artisan vendor:publish --provider="Wnx\LaravelStats\StatsServiceProvider" ``` -The following config file will be published in `config/stats.php` - -```php - [ - base_path('app'), - base_path('database'), - base_path('tests'), - ], - - /* - * List of files/folders to be excluded from analysis. - */ - 'exclude' => [ - // base_path('app/helpers.php'), - // base_path('app/Services'), - ], - - /* - * List of your custom Classifiers - */ - 'custom_component_classifier' => [ - // \App\Classifiers\CustomerExportClassifier::class - ], - - /* - * The Strategy used to reject Classes from the project statistics. - * - * By default all Classes located in - * the vendor directory are being rejected and don't - * count to the statistics. - * - * The package ships with 2 strategies: - * - \Wnx\LaravelStats\RejectionStrategies\RejectVendorClasses::class - * - \Wnx\LaravelStats\RejectionStrategies\RejectInternalClasses::class - * - * If none of the default strategies fit for your usecase, you can - * write your own class which implements the RejectionStrategy Contract. - */ - 'rejection_strategy' => \Wnx\LaravelStats\RejectionStrategies\RejectVendorClasses::class, - - /* - * Namespaces which should be ignored. - * Laravel Stats uses the `Str::startsWith()`class to - * check if a Namespace should be ignored. - * - * You can use `Illuminate` to ignore the entire `Illuminate`-namespace - * or `Illuminate\Support` to ignore a subset of the namespace. - */ - 'ignored_namespaces' => [ - 'Wnx\LaravelStats', - 'Illuminate', - 'Symfony', - ], - -]; - -``` - - ## Usage After installing you can generate the statistics by running the following Artisan Command. @@ -134,14 +66,13 @@ If you want a more detailed report and see which classes have been grouped into php artisan stats --verbose ``` -The verbose option is also available for the JSON format. +The verbose option is available for the JSON format also. ``` php artisan stats --json --verbose ``` - ## How does this package detect certain Laravel Components? The package scans the files defined in the `paths`-array in the configuration file. It then applies [Classifiers](https://github.com/stefanzweifel/laravel-stats/tree/master/src/Classifiers) to those classes to determine which Laravel Component the class represents. @@ -225,6 +156,68 @@ class RepositoryClassifier implements Classifier ... ``` +## Share Metrics with the Laravel Community + +You can optionally share your projects statistic by using the `--share` option. + +```shell +php artisan stats --share +``` + +Your project statistics is shared anonymously with [stats.laravelshift.com](https://stats.laravelshift.com). In regular intervals the dashboard and charts on the site are updated with shared data from other Laravel projects. + +To learn more about this feature, please check out PR [#178](https://github.com/stefanzweifel/laravel-stats/pull/178). + +### Share statistic through CI + +If you would like to share your project statistic in a CI environment you can use the `--no-interaction` and `--name`-options. + +Use the following command in your CI script to share your project statistic automatically. (Update `org/repo` with the name of your application (eg. `acme/podcasting-app`)) + +```shell +php artisan stats --share --no-interaction --name=org/repo +``` + +If you're code is hosted on GitHub, you can integrate `stats` with [GitHub Actions](https://docs.github.com/en/actions). +Copy the following Workflow to `.github/workflows/laravel-stats.yml`. It will share data when a commit is pushed to the `master` branch. The Action automatically uses your GitHub repository name in the `--name`-option. + +```yaml +name: stats + +on: + push: + branches: + - master + +jobs: + stats: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-suggest + + - name: Share Stats + run: php artisan stats --share --name=$GITHUB_REPOSITORY --no-interaction +``` + +### Inspect Data shared with the Community + +If you would like to inspect the payload the command is sending to the API you can use the `--dry-run` and `--payload` options. + +```shell +php artisan stats --share --no-interaction --name="org/repo" --dry-run --payload +``` + ## Treeware You're free to use this package, but if it makes it to your production environment you are required to buy the world a tree. diff --git a/composer.json b/composer.json index f3fee7a6..84cf1d3b 100644 --- a/composer.json +++ b/composer.json @@ -14,18 +14,20 @@ ], "require": { "php": ">=7.3.0", - "illuminate/console": "^7.0", - "illuminate/support": "^7.0", - "phploc/phploc": "~6.0", - "symfony/finder": "~5.0" + "ext-json": "*", + "illuminate/console": "^6.0 || ^7.0", + "illuminate/support": "^6.0 || ^7.0", + "stefanzweifel/laravel-stats-phploc": "^6.1", + "symfony/finder": "^4.3 || ^5.0", + "symfony/process": "^4.3 || ^5.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15", - "laravel/browser-kit-testing": "~6.0", + "laravel/browser-kit-testing": "~5.0 || ~6.0", "laravel/dusk": "~5.0", "mockery/mockery": "^1.1", - "orchestra/testbench": "^5.0", - "phpunit/phpunit": "9.*", + "orchestra/testbench": "^4.0 || ^5.0", + "phpunit/phpunit": "8.* || 9.*", "psalm/plugin-laravel": "^1.3", "vimeo/psalm": "^3.12" }, diff --git a/config/stats.php b/config/stats.php index 66f5b5d2..04bca7a6 100644 --- a/config/stats.php +++ b/config/stats.php @@ -44,7 +44,7 @@ /* * Namespaces which should be ignored. - * Laravel Stats uses the `Str::startsWith()`class to + * Laravel Stats uses the `Str::startsWith()` helper to * check if a Namespace should be ignored. * * You can use `Illuminate` to ignore the entire `Illuminate`-namespace diff --git a/src/Classifier.php b/src/Classifier.php index 3686694b..413686d5 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -36,7 +36,7 @@ class Classifier { - private const DEFAULT_CLASSIFIER = [ + public const DEFAULT_CLASSIFIER = [ ControllerClassifier::class, ModelClassifier::class, CommandClassifier::class, diff --git a/src/Classifiers/BladeComponentClassifier.php b/src/Classifiers/BladeComponentClassifier.php index a81b39ac..03bf39f7 100644 --- a/src/Classifiers/BladeComponentClassifier.php +++ b/src/Classifiers/BladeComponentClassifier.php @@ -15,6 +15,10 @@ public function name(): string public function satisfies(ReflectionClass $class): bool { + if (! class_exists(Component::class)) { + return false; + } + return $class->isSubclassOf(Component::class); } diff --git a/src/Classifiers/CustomCastClassifier.php b/src/Classifiers/CustomCastClassifier.php index 21e71da7..d421c27f 100644 --- a/src/Classifiers/CustomCastClassifier.php +++ b/src/Classifiers/CustomCastClassifier.php @@ -16,6 +16,10 @@ public function name(): string public function satisfies(ReflectionClass $class): bool { + if ((float) app()->version() < 7) { + return false; + } + return $class->implementsInterface(CastsAttributes::class) || $class->implementsInterface(CastsInboundAttributes::class); } diff --git a/src/Classifiers/ObserverClassifier.php b/src/Classifiers/ObserverClassifier.php index f30443ab..22908fbe 100644 --- a/src/Classifiers/ObserverClassifier.php +++ b/src/Classifiers/ObserverClassifier.php @@ -20,7 +20,7 @@ public function satisfies(ReflectionClass $class): bool { return collect($this->getEvents()) ->filter(function ($listeners, $event) { - return Str::of($event)->startsWith('eloquent.'); + return Str::startsWith($event, 'eloquent.'); }) ->map(function ($listeners) { return collect($listeners)->map(function (Closure $closure) { @@ -30,7 +30,7 @@ public function satisfies(ReflectionClass $class): bool ->collapse() ->unique() ->filter(function ($eventListenerSignature) use ($class) { - return Str::of($eventListenerSignature)->contains($class->getName()); + return Str::contains($eventListenerSignature, $class->getName()); }) ->count() > 0; } diff --git a/src/Console/StatsListCommand.php b/src/Console/StatsListCommand.php index c5c38bc0..fd31659f 100644 --- a/src/Console/StatsListCommand.php +++ b/src/Console/StatsListCommand.php @@ -2,14 +2,17 @@ namespace Wnx\LaravelStats\Console; -use Illuminate\Support\Str; -use Wnx\LaravelStats\Project; use Illuminate\Console\Command; +use Illuminate\Support\Str; use Wnx\LaravelStats\ClassesFinder; -use Wnx\LaravelStats\ReflectionClass; -use Wnx\LaravelStats\Outputs\JsonOutput; use Wnx\LaravelStats\Outputs\AsciiTableOutput; +use Wnx\LaravelStats\Outputs\JsonOutput; +use Wnx\LaravelStats\Project; +use Wnx\LaravelStats\ReflectionClass; use Wnx\LaravelStats\RejectionStrategies\RejectVendorClasses; +use Wnx\LaravelStats\ShareableMetrics\CollectMetrics; +use Wnx\LaravelStats\ShareableMetrics\ProjectName; +use Wnx\LaravelStats\ShareableMetrics\SendToLaravelShift; class StatsListCommand extends Command { @@ -18,7 +21,13 @@ class StatsListCommand extends Command * * @var string */ - protected $signature = 'stats {--json : Output the statistics as JSON} {-c|--components= : Comma separated list of components which should be displayed}'; + protected $signature = 'stats + {--json : Output the statistics as JSON} + {-c|--components= : Comma separated list of components which should be displayed} + {--s|share : Share project statistic with Laravel community } + {--name= : Name used when sharing project statistic} + {--payload : Output payload to be shared with Laravel community } + {--dry-run : Do not make request to share statistic}'; /** * The console command description. @@ -43,7 +52,7 @@ public function handle() return new ReflectionClass($class); })->reject(function (ReflectionClass $class) { return app(config('stats.rejection_strategy', RejectVendorClasses::class)) - ->shouldClassBeRejected($class); + ->shouldClassBeRejected($class); })->reject(function (ReflectionClass $class) { foreach (config('stats.ignored_namespaces', []) as $namespace) { if (Str::startsWith($class->getNamespaceName(), $namespace)) { @@ -56,6 +65,24 @@ public function handle() $project = new Project($reflectionClasses); + $this->renderOutput($project); + + if ($this->option('share') === true) { + $this->shareDataWithShift($project); + } + } + + private function getArrayOfComponentsToDisplay(): array + { + if (is_null($this->option('components'))) { + return []; + } + + return explode(',', $this->option('components')); + } + + private function renderOutput(Project $project) + { if ($this->option('json') === true) { $json = (new JsonOutput())->render( $project, @@ -63,8 +90,10 @@ public function handle() $this->getArrayOfComponentsToDisplay() ); - $this->output->text(json_encode($json)); - } else { + if ($this->option('payload') !== true) { + $this->output->text(json_encode($json)); + } + } elseif ($this->option('payload') !== true) { (new AsciiTableOutput($this->output))->render( $project, $this->option('verbose'), @@ -73,12 +102,66 @@ public function handle() } } - private function getArrayOfComponentsToDisplay(): array + private function shareDataWithShift(Project $project): void { - if (is_null($this->option('components'))) { - return []; + $metrics = app(CollectMetrics::class)->collect($project); + + $defaultValueForConfirmation = $this->option('no-interaction') ?? false; + + if ($this->confirm("Do you want to share stats above from your project with the Laravel Community to stats.laravelshift.com?", $defaultValueForConfirmation)) { + $projectName = $this->getProjectName(); + + if ($projectName === null) { + $this->error("Please provide a project name."); + return; + } + + if (! Str::contains($projectName, '/')) { + $this->error("Please use the organisation/repository schema for naming your project."); + return; + } + + $payload = $metrics->toHttpPayload($projectName); + + if ($this->option('payload')) { + $this->output->text(json_encode($payload)); + } + + if ($this->option('dry-run')) { + return; + } + + $wasSuccessful = app(SendToLaravelShift::class)->send($metrics->toHttpPayload($projectName)); + + if ($this->option('payload')) { + return; + } + + if ($wasSuccessful) { + $this->info("Thanks for sharing your project statistic with the community!"); + return; + } + + $this->error("Unable to share stats. (Check logs for details)"); + } + } + + private function getProjectName(): ?string + { + if ($this->option('name')) { + return $this->option('name'); + } + + if (app(ProjectName::class)->hasStoredProjectName() === false) { + $generatedProjectName = app(ProjectName::class)->determineProjectNameFromGit(); + + $projectName = $this->ask("We've determined the following name for your project: \"{$generatedProjectName}\".\n Type a new name or leave it blank to continue.", $generatedProjectName); + + app(ProjectName::class)->storeNameInRcFile($projectName); + + return $projectName; } - return explode(',', $this->option('components')); + return app(ProjectName::class)->get(); } } diff --git a/src/Contracts/CollectableMetric.php b/src/Contracts/CollectableMetric.php new file mode 100644 index 00000000..fa3e3c14 --- /dev/null +++ b/src/Contracts/CollectableMetric.php @@ -0,0 +1,10 @@ +getProjectMetricKeys() + ->merge($this->getComponentMetricKeys()) + ->toArray(); + } + + private function getProjectMetricKeys(): Collection + { + return collect(CollectMetrics::PROJECT_METRICS) + ->map(function ($metricClass) { + return new $metricClass(new Project(collect([]))); + }) + ->map(function ($metric) { + return $metric->name(); + }); + } + + private function getComponentMetricKeys(): Collection + { + $coreClassifierNames = array_map(function ($classifier) { + return (new $classifier)->name(); + }, Classifier::DEFAULT_CLASSIFIER); + + $metrics = []; + + /** @var Component $component */ + foreach ($coreClassifierNames as $coreClassifier) { + $slug = Str::slug(strtolower($coreClassifier), '_'); + + $metrics[] = [ + "{$slug}", + "{$slug}_methods", + "{$slug}_loc", + "{$slug}_lloc", + "{$slug}_lloc_per_method", + ]; + } + + return collect($metrics)->flatten(); + } +} diff --git a/src/ShareableMetrics/CollectMetrics.php b/src/ShareableMetrics/CollectMetrics.php new file mode 100644 index 00000000..8ff166d7 --- /dev/null +++ b/src/ShareableMetrics/CollectMetrics.php @@ -0,0 +1,91 @@ + $this->getProjectMetrics($project), + 'component_metrics' => $this->getComponentMetrics($project) + ]); + } + + protected function getProjectMetrics(Project $project): Collection + { + return collect(self::PROJECT_METRICS) + ->map(function ($metricClass) use ($project) { + return new $metricClass($project); + }) + ->map + ->toArray() + ->collapse(); + } + + protected function getComponentMetrics(Project $project): Collection + { + // Get the Names of "Core"-Components + $coreClassifierNames = array_map(function ($classifier) { + return (new $classifier)->name(); + }, Classifier::DEFAULT_CLASSIFIER); + + // Group Into Components + $groupedByComponent = $project->classifiedClassesGroupedAndFilteredByComponentNames($coreClassifierNames) + ->map(function ($classifiedClasses, $componentName) { + return new Component($componentName, $classifiedClasses); + }); + + $metrics = []; + + /** @var Component $component */ + foreach ($groupedByComponent as $component) { + $slug = Str::slug(strtolower($component->name), '_'); + + $metrics["{$slug}"] = $component->getNumberOfClasses(); + $metrics["{$slug}_methods"] = $component->getNumberOfMethods(); + $metrics["{$slug}_loc"] = $component->getLinesOfCode(); + $metrics["{$slug}_lloc"] = $component->getLogicalLinesOfCode(); + $metrics["{$slug}_lloc_per_method"] = $component->getLogicalLinesOfCodePerMethod(); + } + + return collect($metrics); + } +} diff --git a/src/ShareableMetrics/Metrics/CodeTestRatio.php b/src/ShareableMetrics/Metrics/CodeTestRatio.php new file mode 100644 index 00000000..8720908c --- /dev/null +++ b/src/ShareableMetrics/Metrics/CodeTestRatio.php @@ -0,0 +1,18 @@ +project->statistic()->getApplicationCodeToTestCodeRatio(); + } +} diff --git a/src/ShareableMetrics/Metrics/ComposerPsr4Sources.php b/src/ShareableMetrics/Metrics/ComposerPsr4Sources.php new file mode 100644 index 00000000..5ea4c284 --- /dev/null +++ b/src/ShareableMetrics/Metrics/ComposerPsr4Sources.php @@ -0,0 +1,24 @@ +project + ->classifiedClasses() + ->filter(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->classifier->name() === 'Controllers'; + }); + + if ($controllers->count() === 0) { + return null; + } + + return $controllers + ->reject(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->reflectionClass->getParentClass() === false; + }) + + // Remove Controllers, which extend a Class which is located in the vendor folder + ->reject(function (ClassifiedClass $classifiedClass) { + $parentclass = new ReflectionClass($classifiedClass->reflectionClass->getParentClass()->getName()); + + return $parentclass->isVendorProvided(); + }) + ->reject(function (ClassifiedClass $classifiedClass) { + $parentClassName = $classifiedClass->reflectionClass->getParentClass()->getShortName(); + + return $parentClassName === 'Controller'; + }) + ->count() > 0; + } +} diff --git a/src/ShareableMetrics/Metrics/ControllersFormRequestInjection.php b/src/ShareableMetrics/Metrics/ControllersFormRequestInjection.php new file mode 100644 index 00000000..3f74681f --- /dev/null +++ b/src/ShareableMetrics/Metrics/ControllersFormRequestInjection.php @@ -0,0 +1,48 @@ +project + ->classifiedClasses() + ->filter(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->classifier->name() === 'Controllers'; + }) + ->flatMap(function (ClassifiedClass $classifiedClass) { + return collect($classifiedClass->reflectionClass->getDefinedMethods()); + }) + ->filter(function (ReflectionMethod $method) { + return count($method->getParameters()) > 0; + }) + ->flatMap(function (ReflectionMethod $method) { + return collect($method->getParameters()); + }) + ->filter(function (ReflectionParameter $param) { + return $param->hasType(); + }) + ->reject(function (ReflectionParameter $param) { + return $param->getType()->isBuiltin(); + }) + ->filter(function (ReflectionParameter $param) { + $reflectionClass = new ReflectionClass($param->getType()->getName()); + + return app(RequestClassifier::class)->satisfies($reflectionClass); + }) + ->count() > 0; + } +} diff --git a/src/ShareableMetrics/Metrics/InstalledPackages.php b/src/ShareableMetrics/Metrics/InstalledPackages.php new file mode 100644 index 00000000..2ccb280a --- /dev/null +++ b/src/ShareableMetrics/Metrics/InstalledPackages.php @@ -0,0 +1,28 @@ + $packages, + 'require-dev' => $devPackages, + ]; + } +} diff --git a/src/ShareableMetrics/Metrics/Metric.php b/src/ShareableMetrics/Metrics/Metric.php new file mode 100644 index 00000000..544c45cd --- /dev/null +++ b/src/ShareableMetrics/Metrics/Metric.php @@ -0,0 +1,25 @@ +project = $project; + } + + public function toArray() + { + return [ + $this->name() => $this->value() + ]; + } +} diff --git a/src/ShareableMetrics/Metrics/ModelsCustomInheritance.php b/src/ShareableMetrics/Metrics/ModelsCustomInheritance.php new file mode 100644 index 00000000..d0c43961 --- /dev/null +++ b/src/ShareableMetrics/Metrics/ModelsCustomInheritance.php @@ -0,0 +1,46 @@ +project + ->classifiedClasses() + ->filter(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->classifier->name() === 'Models'; + }); + + if ($models->count() === 0) { + return null; + } + + return $models + + // Remove Models, which extend a Class which is located in the vendor folder + ->reject(function (ClassifiedClass $classifiedClass) { + $parentclass = new ReflectionClass($classifiedClass->reflectionClass->getParentClass()->getName()); + + return $parentclass->isVendorProvided(); + }) + ->reject(function (ClassifiedClass $classifiedClass) { + $parentClassName = $classifiedClass->reflectionClass->getParentClass()->getName(); + + // If a Model extends an Illuminate-class, remove it from the collection + // as we see it as a "normal" Model + return Str::startsWith($parentClassName, 'Illuminate'); + }) + ->count() > 0; + } +} diff --git a/src/ShareableMetrics/Metrics/ModelsFolder.php b/src/ShareableMetrics/Metrics/ModelsFolder.php new file mode 100644 index 00000000..01d1f428 --- /dev/null +++ b/src/ShareableMetrics/Metrics/ModelsFolder.php @@ -0,0 +1,39 @@ +project + ->classifiedClasses() + ->filter(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->classifier->name() === 'Models'; + }); + + if ($models->count() === 0) { + return null; + } + + return $models + ->map(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->reflectionClass->getNamespaceName(); + }) + ->filter(function (string $namespace) { + // If a Models namespace contains a back-slash, we assume that the Model + // is not located in the default location under /app, but somewhere else + return Str::contains($namespace, "\\"); + }) + ->count() > 0; + } +} diff --git a/src/ShareableMetrics/Metrics/ModelsMassAssignment.php b/src/ShareableMetrics/Metrics/ModelsMassAssignment.php new file mode 100644 index 00000000..5f864acc --- /dev/null +++ b/src/ShareableMetrics/Metrics/ModelsMassAssignment.php @@ -0,0 +1,44 @@ +project + ->classifiedClasses() + ->filter(function (ClassifiedClass $classifiedClass) { + return $classifiedClass->classifier->name() === 'Models'; + }) + ->map(function (ClassifiedClass $classifiedClass) { + + // Collect all properties of the given class, + // filter to only keep "guarded" and "fillable" + // and only keep those which have been set by the developer. + return collect($classifiedClass->reflectionClass->getProperties()) + ->filter(function (ReflectionProperty $property) { + return in_array($property->getName(), ['guarded', 'fillable']); + }) + ->filter(function (ReflectionProperty $property) use ($classifiedClass) { + return $property->class === $classifiedClass->reflectionClass->name; + }) + ->toArray(); + }) + ->filter() + ->flatten(1) + ->countBy(function (ReflectionProperty $property) { + return $property->getName(); + }) + ->toArray(); + } +} diff --git a/src/ShareableMetrics/Metrics/NumberOfRoutes.php b/src/ShareableMetrics/Metrics/NumberOfRoutes.php new file mode 100644 index 00000000..7b7e262e --- /dev/null +++ b/src/ShareableMetrics/Metrics/NumberOfRoutes.php @@ -0,0 +1,19 @@ +get(); + } +} diff --git a/src/ShareableMetrics/Metrics/ProjectLinesOfCode.php b/src/ShareableMetrics/Metrics/ProjectLinesOfCode.php new file mode 100644 index 00000000..c1812ca8 --- /dev/null +++ b/src/ShareableMetrics/Metrics/ProjectLinesOfCode.php @@ -0,0 +1,18 @@ +project->statistic()->getLinesOfCode(); + } +} diff --git a/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCode.php b/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCode.php new file mode 100644 index 00000000..9cc9c38e --- /dev/null +++ b/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCode.php @@ -0,0 +1,18 @@ +project->statistic()->getLogicalLinesOfCode(); + } +} diff --git a/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodePerMethod.php b/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodePerMethod.php new file mode 100644 index 00000000..acc32d32 --- /dev/null +++ b/src/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodePerMethod.php @@ -0,0 +1,18 @@ +project->statistic()->getLogicalLinesOfCodePerMethod(); + } +} diff --git a/src/ShareableMetrics/Metrics/ProjectNumberOfClasses.php b/src/ShareableMetrics/Metrics/ProjectNumberOfClasses.php new file mode 100644 index 00000000..317d525b --- /dev/null +++ b/src/ShareableMetrics/Metrics/ProjectNumberOfClasses.php @@ -0,0 +1,18 @@ +project->statistic()->getNumberOfClasses(); + } +} diff --git a/src/ShareableMetrics/Metrics/ScheduledTasks.php b/src/ShareableMetrics/Metrics/ScheduledTasks.php new file mode 100644 index 00000000..17a0497d --- /dev/null +++ b/src/ShareableMetrics/Metrics/ScheduledTasks.php @@ -0,0 +1,19 @@ +events()); + } +} diff --git a/src/ShareableMetrics/MetricsCollection.php b/src/ShareableMetrics/MetricsCollection.php new file mode 100644 index 00000000..3adae10b --- /dev/null +++ b/src/ShareableMetrics/MetricsCollection.php @@ -0,0 +1,22 @@ +get('project_metrics'); + $componentMetrics = $this->get('component_metrics'); + + return [ + 'project' => $projectName, + 'metrics' => $projectMetrics + ->merge($componentMetrics) + ->sortKeys() + ->toArray() + ]; + } +} diff --git a/src/ShareableMetrics/ProjectName.php b/src/ShareableMetrics/ProjectName.php new file mode 100644 index 00000000..8ad6087f --- /dev/null +++ b/src/ShareableMetrics/ProjectName.php @@ -0,0 +1,70 @@ +hasStoredProjectName()) { + return File::get($this->pathToRcFile()); + } + + return null; + } + + public function determineProjectNameFromGit(): ?string + { + $gitPath = $this->getGitBinaryPath(); + + if (is_null($gitPath)) { + return null; + } + + $process = Process::fromShellCommandline("{$gitPath} config --get remote.origin.url"); + $process->run(); + + if ($process->isSuccessful() === false) { + return null; + } + + $remoteUrl = parse_url(trim($process->getOutput())); + + $remoteUrl = Str::replaceLast('.git', '', $remoteUrl['path']); + + return Str::replaceFirst('/', '', $remoteUrl); + } + + protected function getGitBinaryPath(): ?string + { + $process = Process::fromShellCommandline('which git'); + $process->run(); + + if ($process->isSuccessful() === false) { + return null; + } + + return trim($process->getOutput()); + } + + protected function pathToRcFile(): string + { + return base_path(self::RC_FILE); + } + + public function hasStoredProjectName(): bool + { + return File::exists($this->pathToRcFile()); + } + + public function storeNameInRcFile(string $projectName): void + { + File::put($this->pathToRcFile(), $projectName); + } +} diff --git a/src/ShareableMetrics/SendToLaravelShift.php b/src/ShareableMetrics/SendToLaravelShift.php new file mode 100644 index 00000000..87873907 --- /dev/null +++ b/src/ShareableMetrics/SendToLaravelShift.php @@ -0,0 +1,40 @@ +environment('testing')) { + return true; + } + + try { + $client = new Client([ + 'base_uri' => 'https://laravelshift.com', + 'timeout' => 5, + 'headers' => ['Accept' => 'application/json'], + ]); + + $response = $client->request('POST', '/api/stat', [ + 'json' => $payload, + ]); + } catch (GuzzleException $exception) { + info('Unable to share stats: ' . $exception->getMessage()); + return false; + } + + if (in_array($response->getStatusCode(), [201, 204])) { + info("Thanks for sharing your project data with the community!"); + return true; + } + + return false; + } +} diff --git a/tests/ClassesFinderTest.php b/tests/ClassesFinderTest.php index d8ee8289..997be6b1 100644 --- a/tests/ClassesFinderTest.php +++ b/tests/ClassesFinderTest.php @@ -10,13 +10,21 @@ public function setUp() : void { parent::setUp(); + $excludedFiles = [ + __DIR__.'/../tests/Stubs/ExcludedFile.php', + ]; + + if ($this->getLaravelVersion() < 7) { + $excludedFiles[] = __DIR__.'/../tests/Stubs/CustomCasts/StubCast.php'; + $excludedFiles[] = __DIR__.'/../tests/Stubs/CustomCasts/StubInboundCast.php'; + $excludedFiles[] = __DIR__.'/../tests/Stubs/BladeComponents/StubBladeComponent.php'; + } + config()->set('stats', [ 'paths' => [ __DIR__.'/../tests/Stubs', ], - 'exclude' => [ - __DIR__.'/../tests/Stubs/ExcludedFile.php', - ], + 'exclude' => $excludedFiles, 'ignored_namespaces' => [], ]); diff --git a/tests/Classifiers/BladeComponentClassifierTest.php b/tests/Classifiers/BladeComponentClassifierTest.php index df7fb153..cd9b8242 100644 --- a/tests/Classifiers/BladeComponentClassifierTest.php +++ b/tests/Classifiers/BladeComponentClassifierTest.php @@ -12,6 +12,10 @@ class BladeComponentClassifierTest extends TestCase /** @test */ public function it_returns_true_if_given_class_is_a_blade_component() { + if ($this->getLaravelVersion() < 7) { + $this->markTestSkipped("Can't run test on older Laravel Versions"); + } + $this->assertTrue( (new BladeComponentClassifier())->satisfies( new ReflectionClass(StubBladeComponent::class) diff --git a/tests/Classifiers/CustomCastClassifierTest.php b/tests/Classifiers/CustomCastClassifierTest.php index 299e099b..76772b04 100644 --- a/tests/Classifiers/CustomCastClassifierTest.php +++ b/tests/Classifiers/CustomCastClassifierTest.php @@ -13,6 +13,10 @@ class CustomCastClassifierTest extends TestCase /** @test */ public function it_returns_true_if_given_class_is_a_custom_cast() { + if ($this->getLaravelVersion() < 7) { + $this->markTestSkipped("Can't run test on older Laravel Versions"); + } + $this->assertTrue( (new CustomCastClassifier())->satisfies( new ReflectionClass(StubCast::class) @@ -23,6 +27,10 @@ public function it_returns_true_if_given_class_is_a_custom_cast() /** @test */ public function it_returns_true_if_given_class_is_a_custom_inbound_cast() { + if ($this->getLaravelVersion() < 7) { + $this->markTestSkipped("Can't run test on older Laravel Versions"); + } + $this->assertTrue( (new CustomCastClassifier())->satisfies( new ReflectionClass(StubInboundCast::class) diff --git a/tests/Console/StatsListCommandTest.php b/tests/Console/StatsListCommandTest.php index 52eead06..7e80a55f 100644 --- a/tests/Console/StatsListCommandTest.php +++ b/tests/Console/StatsListCommandTest.php @@ -120,4 +120,64 @@ public function it_only_returns_stats_for_given_components() $this->assertStringContainsString('StatsListCommand', $output); $this->assertStringNotContainsString('StatsListCommandTest', $output); } + + /** @test */ + public function it_allows_users_to_share_project_statistics_with_the_community() + { + $this->artisan('stats', [ + '--share' => true, + '--no-interaction' => true, + '--name' => 'repo/org', + ]); + $output = Artisan::output(); + + $this->assertStringContainsString('Thanks for sharing your project statistic with the community!', $output); + } + + /** @test */ + public function it_shows_error_message_when_project_name_does_not_follow_org_repo_schema_when_sharing() + { + $this->artisan('stats', [ + '--share' => true, + '--no-interaction' => true, + '--name' => 'foo', + ]); + $output = Artisan::output(); + + $this->assertStringContainsString('Please use the organisation/repository schema for naming your project.', $output); + } + + /** @test */ + public function it_does_not_show_success_message_for_share_option_if_dry_run_option_is_passed() + { + $this->artisan('stats', [ + '--share' => true, + '--no-interaction' => true, + '--dry-run' => true, + '--name' => 'repo/org', + ]); + $output = Artisan::output(); + + $this->assertStringNotContainsString('Thanks for sharing your project statistic with the community!', $output); + } + + /** @test */ + public function it_output_payload_to_be_sent_to_shift() + { + $this->artisan('stats', [ + '--share' => true, + '--no-interaction' => true, + '--payload' => true, + '--name' => 'repo/org', + ]); + $output = Artisan::output(); + + $output = json_decode(trim($output), true); + + $this->assertIsArray($output); + $this->assertArrayHasKey('project', $output); + $this->assertArrayHasKey('metrics', $output); + + $this->assertEquals('repo/org', $output['project']); + } } diff --git a/tests/ShareableMetrics/AllMetricPayloadKeysTest.php b/tests/ShareableMetrics/AllMetricPayloadKeysTest.php new file mode 100644 index 00000000..57ce002d --- /dev/null +++ b/tests/ShareableMetrics/AllMetricPayloadKeysTest.php @@ -0,0 +1,23 @@ +get(); + + $this->assertIsArray($result); + + $this->assertContains('packages', $result); + $this->assertContains('routes', $result); + $this->assertContains('models_mass_assignment', $result); + $this->assertContains('controllers_lloc_per_method', $result); + $this->assertContains('nova_lenses_methods', $result); + } +} diff --git a/tests/ShareableMetrics/Metrics/CodeTestRatioTest.php b/tests/ShareableMetrics/Metrics/CodeTestRatioTest.php new file mode 100644 index 00000000..762310a6 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/CodeTestRatioTest.php @@ -0,0 +1,38 @@ +createProjectFromClasses([]); + + $metric = new CodeTestRatio($project); + + $this->assertEquals('code_test_ratio', $metric->name()); + } + + /** @test */ + public function it_returns_ratio_of_app_and_test_code() + { + $project = $this->createProjectFromClasses([ + User::class, + DemoDuskTest::class, + DemoUnitTest::class, + DemoBrowserKit::class, + ]); + + $metric = new CodeTestRatio($project); + + $this->assertEquals(0.8, $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ComposerPsr4SourcesTest.php b/tests/ShareableMetrics/Metrics/ComposerPsr4SourcesTest.php new file mode 100644 index 00000000..587ccf79 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ComposerPsr4SourcesTest.php @@ -0,0 +1,37 @@ +createProjectFromClasses([]); + + $metric = new ComposerPsr4Sources($project); + + $this->assertEquals('composer_psr_4_namespaces', $metric->name()); + } + + /** @test */ + public function it_returns_composer_psr_4_sources() + { + $project = $this->createProjectFromClasses([]); + + $metric = new ComposerPsr4Sources($project); + + $value = $metric->value(); + + $this->assertIsArray($value); + + // dd($value); + + $this->assertEquals([ + "App\\" => 'app/' + ], $value); + } +} diff --git a/tests/ShareableMetrics/Metrics/ControllersCustomInheritanceTest.php b/tests/ShareableMetrics/Metrics/ControllersCustomInheritanceTest.php new file mode 100644 index 00000000..7dc1637d --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ControllersCustomInheritanceTest.php @@ -0,0 +1,65 @@ +createProjectFromClasses([]); + + $metric = new ControllersCustomInheritance($project); + + $this->assertEquals('controllers_custom_inheritance', $metric->name()); + } + + /** @test */ + public function it_returns_true_if_controller_extends_another_controller_in_the_project() + { + Route::get('posts', [PostsController::class, 'index']); + + $project = $this->createProjectFromClasses([ + PostsController::class + ]); + + $metric = new ControllersCustomInheritance($project); + + $this->assertTrue($metric->value()); + } + + /** @test */ + public function it_returns_false_if_controller_does_not_extend_another_controller_in_the_project() + { + Route::get('projects', [ProjectsController::class, 'index']); + + $project = $this->createProjectFromClasses([ + ProjectsController::class + ]); + + $metric = new ControllersCustomInheritance($project); + + $this->assertFalse($metric->value()); + } + + /** @test */ + public function it_returns_false_if_controller_extends_the_default_laravel_controller() + { + Route::get('users', [UsersController::class, 'index']); + + $project = $this->createProjectFromClasses([ + UsersController::class + ]); + + $metric = new ControllersCustomInheritance($project); + + $this->assertFalse($metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ControllersFormRequestInjectionTest.php b/tests/ShareableMetrics/Metrics/ControllersFormRequestInjectionTest.php new file mode 100644 index 00000000..29c1c286 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ControllersFormRequestInjectionTest.php @@ -0,0 +1,50 @@ +createProjectFromClasses([]); + + $metric = new ControllersFormRequestInjection($project); + + $this->assertEquals('controllers_form_request_injection', $metric->name()); + } + + /** @test */ + public function it_returns_true_if_controllers_do_use_form_request_injection() + { + Route::get('users', [UsersController::class, 'index']); + + $project = $this->createProjectFromClasses([ + UsersController::class + ]); + + $metric = new ControllersFormRequestInjection($project); + + $this->assertTrue($metric->value()); + } + + /** @test */ + public function it_returns_false_if_controllers_do_not_use_form_request_injection() + { + Route::get('projects', [ProjectsController::class, 'index']); + + $project = $this->createProjectFromClasses([ + ProjectsController::class + ]); + + $metric = new ControllersFormRequestInjection($project); + + $this->assertFalse($metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/InstalledPackagesTest.php b/tests/ShareableMetrics/Metrics/InstalledPackagesTest.php new file mode 100644 index 00000000..901d608b --- /dev/null +++ b/tests/ShareableMetrics/Metrics/InstalledPackagesTest.php @@ -0,0 +1,42 @@ +createProjectFromClasses([]); + + $metric = new InstalledPackages($project); + + $this->assertEquals('packages', $metric->name()); + } + + /** @test */ + public function it_returns_array_of_installed_packages_for_the_project() + { + $project = $this->createProjectFromClasses([]); + + $metric = new InstalledPackages($project); + + $value = $metric->value(); + + $this->assertIsArray($value); + $this->assertArrayHasKey('require', $value); + $this->assertArrayHasKey('require-dev', $value); + + $this->assertEquals([ + 'require' => [ + 'laravel/framework' => '~5.0' + ], + 'require-dev' => [ + 'phpunit/phpunit' => '~4.0' + ] + ], $value); + } +} diff --git a/tests/ShareableMetrics/Metrics/ModelsCustomInheritanceTest.php b/tests/ShareableMetrics/Metrics/ModelsCustomInheritanceTest.php new file mode 100644 index 00000000..592e6628 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ModelsCustomInheritanceTest.php @@ -0,0 +1,58 @@ +createProjectFromClasses([]); + + $metric = new ModelsCustomInheritance($project); + + $this->assertEquals('models_custom_inheritance', $metric->name()); + } + + /** @test */ + public function it_returns_null_if_no_models_are_found_in_the_project() + { + $project = $this->createProjectFromClasses([ + // + ]); + + $metric = new ModelsCustomInheritance($project); + + $this->assertNull($metric->value()); + } + + + /** @test */ + public function it_returns_true_if_models_extends_another_model_in_the_project() + { + $project = $this->createProjectFromClasses([ + Group::class + ]); + + $metric = new ModelsCustomInheritance($project); + + $this->assertTrue($metric->value()); + } + + /** @test */ + public function it_returns_false_if_models_does_not_extend_another_model_in_the_project() + { + $project = $this->createProjectFromClasses([ + User::class + ]); + + $metric = new ModelsCustomInheritance($project); + + $this->assertFalse($metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ModelsFolderTest.php b/tests/ShareableMetrics/Metrics/ModelsFolderTest.php new file mode 100644 index 00000000..5d8bef5f --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ModelsFolderTest.php @@ -0,0 +1,44 @@ +createProjectFromClasses([]); + + $metric = new ModelsFolder($project); + + $this->assertEquals('models_folder', $metric->name()); + } + + /** @test */ + public function it_returns_null_if_no_models_are_found_in_the_project() + { + $project = $this->createProjectFromClasses([ + // + ]); + + $metric = new ModelsFolder($project); + + $this->assertNull($metric->value()); + } + + /** @test */ + public function it_returns_true_if_models_are_stored_in_non_default_namespace() + { + $project = $this->createProjectFromClasses([ + User::class, + ]); + + $metric = new ModelsFolder($project); + + $this->assertTrue($metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ModelsMassAssignmentTest.php b/tests/ShareableMetrics/Metrics/ModelsMassAssignmentTest.php new file mode 100644 index 00000000..b2095d0d --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ModelsMassAssignmentTest.php @@ -0,0 +1,49 @@ +createProjectFromClasses([]); + + $metric = new ModelsMassAssignment($project); + + $this->assertEquals('models_mass_assignment', $metric->name()); + } + + /** @test */ + public function it_returns_an_empty_array_if_project_does_not_contain_models() + { + $project = $this->createProjectFromClasses([]); + + $metric = new ModelsMassAssignment($project); + + $this->assertEquals([], $metric->value()); + } + + /** @test */ + public function it_returns_the_number_of_guarded_and_unguarded_models_in_the_return_array() + { + $project = $this->createProjectFromClasses([ + User::class, + Post::class, + Group::class + ]); + + $metric = new ModelsMassAssignment($project); + + $this->assertEquals([ + 'guarded' => 1, + 'fillable' => 1, + ], $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/NumberOfRoutesTest.php b/tests/ShareableMetrics/Metrics/NumberOfRoutesTest.php new file mode 100644 index 00000000..88a042c8 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/NumberOfRoutesTest.php @@ -0,0 +1,35 @@ +createProjectFromClasses([]); + + $metric = new NumberOfRoutes($project); + + $this->assertEquals('routes', $metric->name()); + } + + /** @test */ + public function it_returns_correct_number_of_routes_for_the_project() + { + Route::get('users', 'Wnx\LaravelStats\Tests\Stubs\Controllers\UsersController@index'); + Route::get('users/create', 'Wnx\LaravelStats\Tests\Stubs\Controllers\UsersController@create'); + Route::post('users', 'Wnx\LaravelStats\Tests\Stubs\Controllers\UsersController@store'); + Route::get('users/{user}', 'Wnx\LaravelStats\Tests\Stubs\Controllers\UsersController@show'); + + $project = $this->createProjectFromClasses([]); + + $metric = new NumberOfRoutes($project); + + $this->assertEquals(4, $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ProjectLinesOfCodeTest.php b/tests/ShareableMetrics/Metrics/ProjectLinesOfCodeTest.php new file mode 100644 index 00000000..79c225e7 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ProjectLinesOfCodeTest.php @@ -0,0 +1,38 @@ +createProjectFromClasses([]); + + $metric = new ProjectLinesOfCode($project); + + $this->assertEquals('loc', $metric->name()); + } + + /** @test */ + public function it_returns_the_correct_total_number_lines_of_code_for_the_project() + { + $project = $this->createProjectFromClasses([ + User::class, + UsersController::class, + DemoJob::class, + DemoUnitTest::class, + ]); + + $metric = new ProjectLinesOfCode($project); + + $this->assertEquals(132, $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodeTest.php b/tests/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodeTest.php new file mode 100644 index 00000000..72d5ed9c --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ProjectLogicalLinesOfCodeTest.php @@ -0,0 +1,38 @@ +createProjectFromClasses([]); + + $metric = new ProjectLogicalLinesOfCode($project); + + $this->assertEquals('lloc', $metric->name()); + } + + /** @test */ + public function it_returns_the_correct_total_number_logical_lines_of_code_for_the_project() + { + $project = $this->createProjectFromClasses([ + User::class, + UsersController::class, + DemoJob::class, + DemoUnitTest::class, + ]); + + $metric = new ProjectLogicalLinesOfCode($project); + + $this->assertEquals(12.0, $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ProjectNumberOfClassesTest.php b/tests/ShareableMetrics/Metrics/ProjectNumberOfClassesTest.php new file mode 100644 index 00000000..8d7b8994 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ProjectNumberOfClassesTest.php @@ -0,0 +1,38 @@ +createProjectFromClasses([]); + + $metric = new ProjectNumberOfClasses($project); + + $this->assertEquals('classes', $metric->name()); + } + + /** @test */ + public function it_returns_correct_number_of_classes_for_the_given_project() + { + $project = $this->createProjectFromClasses([ + User::class, + UsersController::class, + DemoJob::class, + DemoUnitTest::class, + ]); + + $metric = new ProjectNumberOfClasses($project); + + $this->assertEquals(4, $metric->value()); + } +} diff --git a/tests/ShareableMetrics/Metrics/ScheduledTasksTest.php b/tests/ShareableMetrics/Metrics/ScheduledTasksTest.php new file mode 100644 index 00000000..72e9c202 --- /dev/null +++ b/tests/ShareableMetrics/Metrics/ScheduledTasksTest.php @@ -0,0 +1,45 @@ +createProjectFromClasses([]); + + $metric = new ScheduledTasks($project); + + $this->assertEquals('scheduled_tasks', $metric->name()); + } + + /** @test */ + public function it_returns_0_if_no_scheduled_tasks_can_be_found() + { + $project = $this->createProjectFromClasses([]); + + $metric = new ScheduledTasks($project); + + $this->assertEquals(0, $metric->value()); + } + + /** @test */ + public function it_returns_correct_number_of_scheduled_tasks() + { + app(Schedule::class)->command('inspire')->hourly(); + app(Schedule::class)->call(function () { + // Don't panic! + })->daily(); + + $project = $this->createProjectFromClasses([]); + + $metric = new ScheduledTasks($project); + + $this->assertEquals(2, $metric->value()); + } +} diff --git a/tests/Stubs/Controllers/BaseController.php b/tests/Stubs/Controllers/BaseController.php new file mode 100644 index 00000000..8dddb392 --- /dev/null +++ b/tests/Stubs/Controllers/BaseController.php @@ -0,0 +1,12 @@ +hasMany(User::class); + } +} diff --git a/tests/Stubs/Models/Post.php b/tests/Stubs/Models/Post.php new file mode 100644 index 00000000..65f566fc --- /dev/null +++ b/tests/Stubs/Models/Post.php @@ -0,0 +1,16 @@ +belongsTo(User::class); + } +} diff --git a/tests/Stubs/Models/User.php b/tests/Stubs/Models/User.php index 83b6fa8f..2279fb6e 100644 --- a/tests/Stubs/Models/User.php +++ b/tests/Stubs/Models/User.php @@ -3,8 +3,25 @@ namespace Wnx\LaravelStats\Tests\Stubs\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model { - // + protected $fillable = []; + + public function projects(): HasMany + { + return $this->hasMany(Project::class); + } + + public function posts(): HasMany + { + return $this->hasMany(Post::class); + } + + public function group(): BelongsTo + { + return $this->belongsTo(Group::class); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 12f2f70f..110fb76b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,9 +3,11 @@ namespace Wnx\LaravelStats\Tests; use Illuminate\Contracts\Http\Kernel; +use Orchestra\Testbench\TestCase as Orchestra; +use Wnx\LaravelStats\Project; +use Wnx\LaravelStats\ReflectionClass; use Wnx\LaravelStats\StatsServiceProvider; use Wnx\LaravelStats\Tests\Stubs\HttpKernel; -use Orchestra\Testbench\TestCase as Orchestra; use Wnx\LaravelStats\Tests\Stubs\ServiceProviders\EventServiceProvider; abstract class TestCase extends Orchestra @@ -41,4 +43,31 @@ protected function resolveApplicationHttpKernel($app) { $app->singleton(Kernel::class, HttpKernel::class); } + + /** + * Create a new Project based on the passed FQDNs of Classes + * + * @param array $classes + * + * @return \Wnx\LaravelStats\Project + */ + public function createProjectFromClasses(array $classes = []) + { + $classes = collect($classes) + ->map(function ($class) { + return new ReflectionClass($class); + }); + + return new Project($classes); + } + + /** + * Get currently installed Laravel Version + * + * @return float + */ + public function getLaravelVersion(): float + { + return (float) app()->version(); + } } diff --git a/tests/ValueObjects/ClassifiedClassTest.php b/tests/ValueObjects/ClassifiedClassTest.php index ee76f60e..3e0bea51 100644 --- a/tests/ValueObjects/ClassifiedClassTest.php +++ b/tests/ValueObjects/ClassifiedClassTest.php @@ -49,7 +49,7 @@ public function it_returns_number_of_non_public_methods_for_a_classified_class() public function it_returns_number_of_lines_of_code_for_a_classified_class() { $this->assertEquals( - 51, + 53, $this->getClassifiedClass()->getLines() ); } diff --git a/tests/ValueObjects/ComponentTest.php b/tests/ValueObjects/ComponentTest.php index 5a715d3b..c4633efd 100644 --- a/tests/ValueObjects/ComponentTest.php +++ b/tests/ValueObjects/ComponentTest.php @@ -75,8 +75,7 @@ public function it_returns_average_number_of_methods_per_class_for_all_classes_w public function it_returns_total_number_of_lines_of_code_for_all_classes_within_a_component() { $component = $this->getTestComponent(); - - $this->assertEquals(113, $component->getLinesOfCode()); + $this->assertEquals(115, $component->getLinesOfCode()); } /** @test */