[PHP-WEBMASTER] [web-downloads] main: Add tests

Author: Shivam Mathur (shivammathur)
Date: 2025-01-27T13:16:24+05:30

Commit: Add tests · php/web-downloads@f8980e1 · GitHub
Raw diff: https://github.com/php/web-downloads/commit/f8980e125465cb9e8b5497121856859b3ff4e13b.diff

Add tests

Changed paths:
  A .gitignore
  A composer.json
  A composer.lock
  A phpunit.xml.dist
  A src/Helpers/Helpers.php
  A tests/Actions/FetchArtifactTest.php
  A tests/Actions/GetArtifactsTest.php
  A tests/AuthTest.php
  A tests/BaseControllerTest.php
  A tests/CommandTest.php
  A tests/Console/Command/PeclCommandTest.php
  A tests/Console/Command/PhpCommandTest.php
  A tests/Console/Command/WinlibsCommandTest.php
  A tests/ControllerInterfaceTest.php
  A tests/Helpers/HelpersTest.php
  A tests/Http/Controllers/IndexControllerTest.php
  A tests/Http/Controllers/PeclControllerTest.php
  A tests/Http/Controllers/PhpControllerTest.php
  A tests/Http/Controllers/WinlibsControllerTest.php
  A tests/RouterTest.php
  A tests/ValidatorTest.php
  M src/Actions/FetchArtifact.php
  M src/Actions/GetArtifacts.php
  M src/Console/Command.php
  M src/Console/Command/PeclCommand.php
  M src/Console/Command/PhpCommand.php
  M src/Console/Command/WinlibsCommand.php
  M src/Http/BaseController.php
  M src/Http/Controllers/IndexController.php
  M src/Http/Controllers/PeclController.php
  M src/Http/Controllers/PhpController.php
  M src/Http/Controllers/WinlibsController.php
  M src/Router.php

Diff:

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f394aa8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+/builds/
+xdebug-filter.php
+.phpunit.result.cache
+.phpunit.cache
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..db69f41
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,18 @@
+{
+ "name": "php/web-downloads",
+ "type": "project",
+ "license": "MIT",
+ "require": {
+ "ext-fileinfo": "*",
+ "ext-curl": "*",
+ "ext-zip": "*"
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..3e2d86e
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1708 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at Basic usage - Composer,
+ "This file is @generated automatically"
+ ],
+ "content-hash": "361bbdabce9f9fd3219dcda42e05e0d8",
+ "packages": ,
+ "packages-dev": [
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues&quot;,
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1&quot;
+ },
+ "funding": [
+ {
+ "url": "How to connect Tidelift with GitHub | Tidelift,
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-08T17:47:46+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git&quot;,
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b&quot;,
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues&quot;,
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1&quot;
+ },
+ "time": "2024-10-08T18:51:32+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git&quot;,
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176&quot;,
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues&quot;,
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git&quot;,
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74&quot;,
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues&quot;,
+ "source": "https://github.com/phar-io/version/tree/3.2.1&quot;
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "11.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git&quot;,
+ "reference": "418c59fd080954f8c4aa5631d9502ecda2387118"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118&quot;,
+ "reference": "418c59fd080954f8c4aa5631d9502ecda2387118",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.3.1",
+ "php": ">=8.2",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-text-template": "^4.0.1",
+ "sebastian/code-unit-reverse-lookup": "^4.0.1",
+ "sebastian/complexity": "^4.0.1",
+ "sebastian/environment": "^7.2.0",
+ "sebastian/lines-of-code": "^3.0.1",
+ "sebastian/version": "^5.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5.0"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage&quot;,
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-12-11T12:34:27+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git&quot;,
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6&quot;,
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/&quot;,
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-27T05:02:59+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git&quot;,
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2&quot;,
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/&quot;,
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:07:44+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git&quot;,
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964&quot;,
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/&quot;,
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:08:43+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "7.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git&quot;,
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3&quot;,
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/&quot;,
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:09:35+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "11.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git&quot;,
+ "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a&quot;,
+ "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.12.1",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.2",
+ "phpunit/php-code-coverage": "^11.0.7",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-invoker": "^5.0.1",
+ "phpunit/php-text-template": "^4.0.1",
+ "phpunit/php-timer": "^7.0.1",
+ "sebastian/cli-parser": "^3.0.2",
+ "sebastian/code-unit": "^3.0.1",
+ "sebastian/comparator": "^6.2.1",
+ "sebastian/diff": "^6.0.2",
+ "sebastian/environment": "^7.2.0",
+ "sebastian/exporter": "^6.3.0",
+ "sebastian/global-state": "^7.0.2",
+ "sebastian/object-enumerator": "^6.0.1",
+ "sebastian/type": "^5.1.0",
+ "sebastian/version": "^5.0.2",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/&quot;,
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html&quot;,
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit&quot;,
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-11T10:52:48+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git&quot;,
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180&quot;,
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:41:36+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git&quot;,
+ "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca&quot;,
+ "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/code-unit/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-12-12T09:59:06+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git&quot;,
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e&quot;,
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:45:54+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "6.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git&quot;,
+ "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739&quot;,
+ "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/diff": "^6.0",
+ "sebastian/exporter": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator&quot;,
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-31T05:30:08+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git&quot;,
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0&quot;,
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:49:50+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git&quot;,
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544&quot;,
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff&quot;,
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/diff/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:53:05+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "7.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git&quot;,
+ "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5&quot;,
+ "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment&quot;,
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/environment/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:54:44+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "6.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git&quot;,
+ "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3&quot;,
+ "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter&quot;,
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-12-05T09:17:50+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git&quot;,
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7&quot;,
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state&quot;,
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:57:36+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git&quot;,
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a&quot;,
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:58:38+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git&quot;,
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa&quot;,
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:00:13+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git&quot;,
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9&quot;,
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:01:32+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git&quot;,
+ "reference": "694d156164372abbd149a4b85ccda2e4670c0e16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16&quot;,
+ "reference": "694d156164372abbd149a4b85ccda2e4670c0e16",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:10:34+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git&quot;,
+ "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac&quot;,
+ "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/type/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.0&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-09-17T13:12:04+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git&quot;,
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874&quot;,
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version&quot;,
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues&quot;,
+ "security": "https://github.com/sebastianbergmann/version/security/policy&quot;,
+ "source": "https://github.com/sebastianbergmann/version/tree/5.0.2&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-09T05:16:32+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git&quot;,
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163&quot;,
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues&quot;,
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git&quot;,
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2&quot;,
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/&quot;,
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues&quot;,
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3&quot;
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer&quot;,
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ }
+ ],
+ "aliases": ,
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "ext-fileinfo": "*",
+ "ext-curl": "*",
+ "ext-zip": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..659c34b
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot;
+ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
+ bootstrap="vendor/autoload.php"
+ cacheDirectory=".phpunit.cache"
+ executionOrder="depends,defects"
+ shortenArraysForExportThreshold="10"
+ beStrictAboutOutputDuringTests="true"
+ displayDetailsOnPhpunitDeprecations="true"
+ failOnPhpunitDeprecation="true"
+ failOnRisky="true"
+ failOnWarning="true">
+ <testsuites>
+ <testsuite name="default">
+ <directory>tests</directory>
+ </testsuite>
+ </testsuites>
+
+ <source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
+ <include>
+ <directory>src</directory>
+ </include>
+ </source>
+</phpunit>
diff --git a/src/Actions/FetchArtifact.php b/src/Actions/FetchArtifact.php
index 2339e1c..5f096a3 100644
--- a/src/Actions/FetchArtifact.php
+++ b/src/Actions/FetchArtifact.php
@@ -4,12 +4,12 @@

class FetchArtifact
{
- public static function handle($url, $filepath, $token = null): void
+ public function handle($url, $filepath, $token = null): void
     {
         $ch = curl_init();
+ $fp = fopen($filepath, 'w');
         curl_setopt($ch, CURLOPT_URL, $url);
         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- $fp = fopen($filepath, 'w+');
         curl_setopt($ch, CURLOPT_FILE, $fp);
         if (str_contains($url, 'api.github.com')) {
             $headers = [
diff --git a/src/Actions/GetArtifacts.php b/src/Actions/GetArtifacts.php
index 140480e..0dc4c5a 100644
--- a/src/Actions/GetArtifacts.php
+++ b/src/Actions/GetArtifacts.php
@@ -2,9 +2,11 @@

namespace App\Actions;

+use App\Helpers\Helpers;
+
class GetArtifacts
{
- public static function handle($workflow_run_id, $token): void
+ public function handle($workflow_run_id, $token): void
     {
         $ch = curl_init();

@@ -33,14 +35,13 @@ public static function handle($workflow_run_id, $token): void
             $artifacts = json_decode($response, true);
             $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/" . $workflow_run_id;
             if (is_dir($workflowRunDirectory)) {
- rmdir($workflowRunDirectory);
+ (new Helpers)->rmdirr($workflowRunDirectory);
             }
             mkdir($workflowRunDirectory, 0755, true);
             foreach ($artifacts['artifacts'] as $artifact) {
                 $filepath = $workflowRunDirectory . "/" . $artifact['name'] . ".zip";
- FetchArtifact::handle($artifact['archive_download_url'], $filepath, $token);
+ (new FetchArtifact)->handle($artifact['archive_download_url'], $filepath, $token);
             }
         }
     }
-
}
\ No newline at end of file
diff --git a/src/Console/Command.php b/src/Console/Command.php
index fcb12eb..56d9ac6 100644
--- a/src/Console/Command.php
+++ b/src/Console/Command.php
@@ -4,9 +4,9 @@

abstract class Command
{
- public const int SUCCESS = 0;
- public const int FAILURE = 1;
- public const int INVALID = 2;
+ public const SUCCESS = 0;
+ public const FAILURE = 1;
+ public const INVALID = 2;

     protected string $signature = '';

@@ -57,11 +57,18 @@ public function getDescription(): string {
         return $this->description;
     }

- public function getArgument($index) {
+ public function getArgument($index): mixed
+ {
         return $this->arguments[$index] ?? null;
     }

- public function getOption($name) {
+ public function getOption($name): mixed
+ {
         return $this->options[$name] ?? null;
     }
+
+ public function setOption($name, $value): void
+ {
+ $this->options[$name] = $value;
+ }
}
\ No newline at end of file
diff --git a/src/Console/Command/PeclCommand.php b/src/Console/Command/PeclCommand.php
index 807a785..53fb0e3 100644
--- a/src/Console/Command/PeclCommand.php
+++ b/src/Console/Command/PeclCommand.php
@@ -20,7 +20,13 @@ public function handle(): int
                 throw new Exception('Base directory is required');
             }

- $files = glob(getenv('BUILDS_DIRECTORY') . '/php/*.zip');
+ $zips_directory = getenv('BUILDS_DIRECTORY') . '/pecl';
+ if(!is_dir($zips_directory)) {
+ return Command::SUCCESS;
+ }
+
+ $files = glob($zips_directory . '/*.zip');
+

             // We lock the files we are working on
             // so that we don't process them again if the command is run again
diff --git a/src/Console/Command/PhpCommand.php b/src/Console/Command/PhpCommand.php
index 005febc..596b3dc 100644
--- a/src/Console/Command/PhpCommand.php
+++ b/src/Console/Command/PhpCommand.php
@@ -3,6 +3,7 @@
namespace App\Console\Command;

use App\Console\Command;
+use App\Helpers\Helpers;
use DateTimeImmutable;
use Exception;
use ZipArchive;
@@ -22,7 +23,12 @@ public function handle(): int
                 throw new Exception('Base directory is required');
             }

- $files = glob(getenv('BUILDS_DIRECTORY') . '/php/*.zip');
+ $zips_directory = getenv('BUILDS_DIRECTORY') . '/php';
+ if(!is_dir($zips_directory)) {
+ return Command::SUCCESS;
+ }
+
+ $files = glob($zips_directory . '/*.zip');

             // We lock the files we are working on
             // so that we don't process them again if the command is run again
@@ -40,19 +46,18 @@ public function handle(): int
                 $tempDirectory = "/tmp/php-" . $hash;

                 if (is_dir($tempDirectory)) {
- rmdir($tempDirectory);
+ (new Helpers)->rmdirr($tempDirectory);
                 }
                 mkdir($tempDirectory, 0755, true);

                 $zip = new ZipArchive();
-
                 if ($zip->open($filepath) === TRUE) {
                     if ($zip->extractTo($tempDirectory) === FALSE) {
- throw new Exception('Failed to extract the extension build');
+ throw new Exception('Failed to extract the php build');
                     }
                     $zip->close();
                 } else {
- throw new Exception('Failed to extract the extension');
+ throw new Exception('Failed to extract the php build');
                 }

                 unlink($filepath);
@@ -63,20 +68,33 @@ public function handle(): int

                 $this->generateListing($destinationDirectory);

- rmdir($tempDirectory);
+ (new Helpers)->rmdirr($tempDirectory);

                 unlink($filepath . '.lock');
             }
             return Command::SUCCESS;
         } catch (Exception $e) {
             echo $e->getMessage();
+ $tempDirectories = glob('/tmp/php-*');
+ if($tempDirectories) {
+ foreach ($tempDirectories as $tempDirectory) {
+ (new Helpers)->rmdirr($tempDirectory);
+ }
+ }
             return Command::FAILURE;
         }
     }

+ /**
+ * @throws Exception
+ */
     private function getDestinationDirectory(string $tempDirectory): string
     {
- $testPackFile = basename(glob($tempDirectory . '/php-test-pack-*.zip')[0]);
+ $testPackFiles = glob($tempDirectory . '/php-test-pack-*.zip');
+ if(empty($testPackFiles)) {
+ throw new Exception('No test pack found in the artifact');
+ }
+ $testPackFile = basename($testPackFiles[0]);
         $testPackFileName = str_replace('.zip', '', $testPackFile);
         $version = explode('-', $testPackFileName)[3];
         return $this->baseDirectory . (preg_match('/^\d+\.\d+\.\d+$/', $version) ? '/releases' : '/qa');
@@ -95,7 +113,7 @@ private function moveBuild(string $tempDirectory, string $destinationDirectory):
                 $destination = $destinationDirectory . '/' . $fileName;
                 rename($file, $destination);
             }
- rmdir($tempDirectory);
+ (new Helpers)->rmdirr($tempDirectory);
             $this->copyBuildsToArchive($destinationDirectory, $version);
         } else {
             throw new Exception('No builds found in the artifact');
@@ -105,11 +123,14 @@ private function moveBuild(string $tempDirectory, string $destinationDirectory):
     private function copyBuildsToArchive(string $directory, string $version): void
     {
         $version_short = substr($version, 0, 3);
- $files = glob($directory . '/php*-' . $version_short . '-*.zip');
+ $files = glob($directory . '/php*' . $version_short . '*.zip');
+ if(!is_dir($directory . '/archive')) {
+ mkdir($directory . '/archive', 0755, true);
+ }
         foreach ($files as $file) {
             $fileVersion = $this->getFileVersion($file);
             if ($fileVersion) {
- copy($directory . '/' . basename($file), $directory . '/archive/' . $file);
+ copy($directory . '/' . basename($file), $directory . '/archive/' . basename($file));
                 if (version_compare($fileVersion, $version) < 0) {
                     unlink($file);
                 }
@@ -119,8 +140,15 @@ private function copyBuildsToArchive(string $directory, string $version): void

     private function getFileVersion(string $file): string
     {
- $file = preg_replace('/^php-((debug|devel|test)-pack)?/', '', $file);
- return explode('-', $file)[0];
+ $file = basename($file);
+ if(preg_match('/^php-((debug|devel|test)-pack-).*/', $file)) {
+ $pattern = '/^php-((debug|devel|test)-pack-)?/';
+ } else {
+ $pattern = '/php-/';
+ }
+ $file = preg_replace($pattern, '', $file);
+ $parts = explode('-', $file);
+ return str_replace('.zip', '', $parts[0]);
     }

     /**
@@ -147,9 +175,9 @@ private function generateListing(string $directory): void
             }
             $releases[$version_short][$key]['mtime'] = $mtime;
             $releases[$version_short][$key]['zip'] = [
- 'path' => $file_ori,
+ 'path' => basename($file_ori),
                 'size' => $this->bytes2string(filesize($file_ori)),
- 'sha256' => $sha256sums[strtolower($file_ori)]
+ 'sha256' => $sha256sums[strtolower(basename($file_ori))]
             ];
             $namingPattern = $parts['version'] . ($parts['nts'] ? '-' . $parts['nts'] : '') . '-Win32-' . $parts['vc'] . '-' . $parts['arch'] . ($parts['ts'] ? '-' . $parts['ts'] : '');
             $build_types = [
@@ -162,10 +190,19 @@ private function generateListing(string $directory): void
             foreach ($build_types as $type => $fileName) {
                 $filePath = $directory . '/' . $fileName;
                 if (file_exists($filePath)) {
- $releases[$version_short][$type] = [
- 'path' => $fileName,
- 'size' => $this->bytes2string(filesize($filePath))
- ];
+ if(in_array($type, ['test_pack', 'source'])) {
+ $releases[$version_short][$type] = [
+ 'path' => $fileName,
+ 'size' => $this->bytes2string(filesize($filePath)),
+ 'sha256' => $sha256sums[strtolower(basename($file_ori))]
+ ];
+ } else {
+ $releases[$version_short][$key][$type] = [
+ 'path' => $fileName,
+ 'size' => $this->bytes2string(filesize($filePath)),
+ 'sha256' => $sha256sums[strtolower(basename($file_ori))]
+ ];
+ }
                 }
             }
         }
@@ -206,16 +243,30 @@ private function updateReleasesJson(array $releases, string $directory): void

     private function updateLatestBuilds($releases, $directory): void
     {
+ if(!is_dir($directory . '/latest')) {
+ mkdir($directory . '/latest', 0755, true);
+ }
         foreach ($releases as $versionShort => $release) {
- $latestFileName = str_replace($release['version'], $versionShort, $release['path']);
- $latestFileName = str_replace('.zip', '-latest.zip', $latestFileName);
- copy($directory . '/' . $release['path'], $directory . '/latest/' . $latestFileName);
+ foreach ($release as $value) {
+ $filePath = $value['path'] ?? $value['zip']['path'] ?? null;
+ if($filePath === null) {
+ continue;
+ } else {
+ $filePath = basename($filePath);
+ }
+ $latestFileName = str_replace($release['version'], $versionShort, $filePath);
+ $latestFileName = str_replace('.zip', '-latest.zip', $latestFileName);
+ copy($directory . '/' . $filePath, $directory . '/latest/' . $latestFileName);
+ }
         }
     }

     private function getSha256Sums($directory): array
     {
         $result = ;
+ if(!file_exists("$directory/sha256sum.txt")) {
+ file_put_contents("$directory/sha256sum.txt", '');
+ }
         $sha_file = fopen("$directory/sha256sum.txt", 'w');
         foreach (scandir($directory) as $filename) {
             if (pathinfo($filename, PATHINFO_EXTENSION) !== 'zip') {
@@ -229,7 +280,7 @@ private function getSha256Sums($directory): array
         return $result;
     }

- private function bytes2string(int $size): float
+ private function bytes2string(int $size): string
     {
         $sizes = ['YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'kB', 'B'];

diff --git a/src/Console/Command/WinlibsCommand.php b/src/Console/Command/WinlibsCommand.php
index 92543cc..ebec0f4 100644
--- a/src/Console/Command/WinlibsCommand.php
+++ b/src/Console/Command/WinlibsCommand.php
@@ -3,6 +3,7 @@
namespace App\Console\Command;

use App\Console\Command;
+use App\Helpers\Helpers;
use Exception;

class WinlibsCommand extends Command
@@ -34,16 +35,16 @@ public function handle(): int
             }

             foreach ($filteredDirectories as $directoryPath) {
- $data = json_decode(file_get_contents($directoryPath . '/data.json'), true);
+ $data = json_decode(file_get_contents($directoryPath . '/data.json'), true, 512, JSON_THROW_ON_ERROR);
                 extract($data);
                 $files = glob($directoryPath . '/*.zip');
                 $files = $this->parseFiles($files);
                 if ($files) {
- $this->copyFiles($files, $library, $ref, $vs_version_targets);
- $this->updateSeriesFiles($files, $library, $ref, $php_versions, $vs_version_targets, $stability);
+ $this->copyFiles($files, $library, $vs_version_targets);
+ $this->updateSeriesFiles($files, $library, $php_versions, $vs_version_targets, $stability);
                 }

- rmdir($directoryPath);
+ (new Helpers)->rmdirr($directoryPath);

                 unlink($directoryPath . '.lock');
             }
@@ -60,20 +61,21 @@ private function parseFiles(array $files): array
         foreach ($files as $file) {
             $fileName = basename($file);
             $fileNameParts = explode('.', $fileName);
- $parsedFileNameParts = explode('-', $fileNameParts[0]);
+ $parsedFileNameParts = explode('-', $fileName);
+ $archParts = explode('.', $parsedFileNameParts[3]);
             $data = [
                 'file_path' => $file,
                 'file_name' => $fileName,
- 'extension' => $fileNameParts[1],
+ 'extension' => $fileNameParts[count($fileNameParts)-1],
                 'artifact_name' => $parsedFileNameParts[0],
- 'vs_version' => $parsedFileNameParts[1],
- 'arch' => $parsedFileNameParts[2],
+ 'vs_version' => $parsedFileNameParts[2],
+ 'arch' => $archParts[0],
             ];
         }
         return $data;
     }

- private function copyFiles(array $files, string $library, string $ref, string $vs_version_targets): void
+ private function copyFiles(array $files, string $library, string $vs_version_targets): void
     {
         $baseDirectory = $this->baseDirectory . "/php-sdk/deps";
         if (!is_dir($baseDirectory)) {
@@ -83,7 +85,10 @@ private function copyFiles(array $files, string $library, string $ref, string $v
         foreach ($files as $file) {
             foreach ($vs_version_targets as $vs_version_target) {
                 $destinationDirectory = $baseDirectory . '/' . $vs_version_target . '/' . $file['arch'];
- $destinationFileName = str_replace($file['artifact_name'], $library . '-' . $ref, $file['file_name']);
+ if (!is_dir($destinationDirectory)) {
+ mkdir($destinationDirectory, 0755, true);
+ }
+ $destinationFileName = str_replace($file['artifact_name'], $library, $file['file_name']);
                 copy($file['file_path'], $destinationDirectory . '/' . $destinationFileName);
             }
         }
@@ -92,7 +97,6 @@ private function copyFiles(array $files, string $library, string $ref, string $v
     private function updateSeriesFiles(
         array $files,
         string $library,
- string $ref,
         string $php_versions,
         string $vs_version_targets,
         string $stability
@@ -104,17 +108,30 @@ private function updateSeriesFiles(

         $baseDirectory = $this->baseDirectory . "/php-sdk/deps/series";

+ if (!is_dir($baseDirectory)) {
+ mkdir($baseDirectory, 0755, true);
+ }
+
         foreach ($php_versions as $php_version) {
             foreach ($vs_version_targets as $vs_version_target) {
                 foreach ($stability_values as $stability_value) {
                     foreach ($files as $file) {
- $fileName = str_replace($file['artifact_name'], $library . '-' . $ref, $file['file_name']);
+ $fileName = str_replace($file['artifact_name'], $library, $file['file_name']);
                         $arch = $file['arch'];
                         $seriesFile = $baseDirectory . "/packages-$php_version-$vs_version_target-$arch-$stability_value.txt";
- $file_lines = file($seriesFile, FILE_IGNORE_NEW_LINES);
- foreach ($file_lines as $no => $line) {
- if (str_starts_with($line, $library)) {
- $file_lines[$no] = $fileName;
+ if (!file_exists($seriesFile)) {
+ $file_lines = [$fileName];
+ } else {
+ $file_lines = file($seriesFile, FILE_IGNORE_NEW_LINES);
+ $found = false;
+ foreach ($file_lines as $no => $line) {
+ if (str_starts_with($line, $library)) {
+ $file_lines[$no] = $fileName;
+ $found = true;
+ }
+ }
+ if (!$found) {
+ $file_lines = $fileName;
                             }
                         }
                         file_put_contents($seriesFile, implode("\n", $file_lines));
diff --git a/src/Helpers/Helpers.php b/src/Helpers/Helpers.php
new file mode 100644
index 0000000..7a7556b
--- /dev/null
+++ b/src/Helpers/Helpers.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Helpers;
+
+class Helpers
+{
+ public function rmdirr($node): bool
+ {
+ if (!file_exists($node)) {
+ return false;
+ }
+ if (is_file($node) || is_link($node)) {
+ return unlink($node);
+ }
+ $dir = dir($node);
+ while (false !== $leaf = $dir->read()) {
+ if ($leaf == '.' || $leaf == '..') {
+ continue;
+ }
+ $this->rmdirr($node . DIRECTORY_SEPARATOR . $leaf);
+ }
+ $dir->close();
+ return rmdir($node);
+ }
+}
\ No newline at end of file
diff --git a/src/Http/BaseController.php b/src/Http/BaseController.php
index 9bc8f35..b8cd681 100644
--- a/src/Http/BaseController.php
+++ b/src/Http/BaseController.php
@@ -2,12 +2,20 @@

namespace App\Http;

+use JsonException;
+
abstract class BaseController implements ControllerInterface
{
+ public function __construct(protected string $inputPath = "php://input") {
+ //
+ }
+
+ /**
+ * @throws JsonException
+ */
     public function handle(): void
     {
- $data = json_decode(file_get_contents('php://input'), true);
-
+ $data = json_decode(file_get_contents($this->inputPath), true, 512, JSON_THROW_ON_ERROR);
         if ($this->validate($data)) {
             $this->execute($data);
         }
diff --git a/src/Http/Controllers/IndexController.php b/src/Http/Controllers/IndexController.php
index c80478a..4296a95 100644
--- a/src/Http/Controllers/IndexController.php
+++ b/src/Http/Controllers/IndexController.php
@@ -11,12 +11,12 @@ public function handle(): void
         echo 'Welcome!';
     }

- protected function validate(array $data): bool
+ public function validate(array $data): bool
     {
         return true;
     }

- protected function execute(array $data): void
+ public function execute(array $data): void
     {
         //
     }
diff --git a/src/Http/Controllers/PeclController.php b/src/Http/Controllers/PeclController.php
index 25423d0..43b0b4d 100644
--- a/src/Http/Controllers/PeclController.php
+++ b/src/Http/Controllers/PeclController.php
@@ -9,7 +9,7 @@

class PeclController extends BaseController
{
- protected function validate(mixed $data): bool
+ public function validate(array $data): bool
     {
         $validator = new Validator([
             'url' => 'required|url',
@@ -29,7 +29,7 @@ protected function validate(mixed $data): bool
         return $valid;
     }

- protected function execute(array $data): void
+ public function execute(array $data): void
     {
         try {
             extract($data);
@@ -43,11 +43,11 @@ protected function execute(array $data): void
     /**
      * @throws Exception
      */
- private function fetchExtension(string $extension, string $ref, string $url, string $token): void
+ protected function fetchExtension(string $extension, string $ref, string $url, string $token): void
     {
         $filepath = getenv('BUILDS_DIRECTORY') . "/pecl/$extension-$ref-" . hash('sha256', $url) . strtotime('now') . ".zip";

- FetchArtifact::handle($url, $filepath, $token);
+ (new FetchArtifact)->handle($url, $filepath, $token);

         if (!file_exists($filepath) || mime_content_type($filepath) !== 'application/zip') {
             throw new Exception('Failed to fetch the extension');
diff --git a/src/Http/Controllers/PhpController.php b/src/Http/Controllers/PhpController.php
index 3711a47..4c25442 100644
--- a/src/Http/Controllers/PhpController.php
+++ b/src/Http/Controllers/PhpController.php
@@ -48,7 +48,7 @@ private function fetchPhpBuild(string $url, string $token): void

         $filepath = getenv('BUILDS_DIRECTORY') . "/php/php-" . $hash . ".tar.gz";

- FetchArtifact::handle($url, $filepath, $token);
+ (new FetchArtifact)->handle($url, $filepath, $token);

         if (!file_exists($filepath) || mime_content_type($filepath) !== 'application/zip') {
             throw new Exception('Failed to fetch the PHP build');
diff --git a/src/Http/Controllers/WinlibsController.php b/src/Http/Controllers/WinlibsController.php
index 363e681..88f4d0d 100644
--- a/src/Http/Controllers/WinlibsController.php
+++ b/src/Http/Controllers/WinlibsController.php
@@ -36,7 +36,7 @@ protected function validate(array $data): bool
     protected function execute(array $data): void
     {
         extract($data);
- GetArtifacts::handle($workflow_run_id, $token);
+ (new GetArtifacts)->handle($workflow_run_id, $token);
         $directory = getenv('BUILDS_DIRECTORY') . '/winlibs/' . $workflow_run_id;
         file_put_contents($directory . '/data.json', json_encode($data));
     }
diff --git a/src/Router.php b/src/Router.php
index 06e7ca4..59c5861 100644
--- a/src/Router.php
+++ b/src/Router.php
@@ -3,6 +3,7 @@
namespace App;

use App\Http\BaseController;
+use JsonException;

class Router
{
@@ -22,6 +23,9 @@ public function registerRoute(string $path, string $method, string $handler, boo
         ];
     }

+ /**
+ * @throws JsonException
+ */
     public function handleRequest(): void
     {
         $path = $_SERVER['REQUEST_URI'];
diff --git a/tests/Actions/FetchArtifactTest.php b/tests/Actions/FetchArtifactTest.php
new file mode 100644
index 0000000..27a956e
--- /dev/null
+++ b/tests/Actions/FetchArtifactTest.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Actions;
+
+use App\Actions\FetchArtifact;
+use PHPUnit\Framework\TestCase;
+
+class MockFetchArtifact extends FetchArtifact {
+
+ public function handle($url, $filepath, $token = null): void
+ {
+ file_put_contents($filepath, $url . $token);
+ }
+}
+
+class FetchArtifactTest extends TestCase {
+ public function testHandleWithValidData() {
+ $url = "https://example.com";
+ $filepath = "test.txt";
+ $token = "test_token";
+ $fetchArtifact = new MockFetchArtifact();
+ $fetchArtifact->handle($url, $filepath, $token);
+ $this->assertFileExists($filepath);
+ $this->assertEquals($url . $token, file_get_contents($filepath));
+ unlink($filepath);
+ }
+}
diff --git a/tests/Actions/GetArtifactsTest.php b/tests/Actions/GetArtifactsTest.php
new file mode 100644
index 0000000..72e279c
--- /dev/null
+++ b/tests/Actions/GetArtifactsTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Actions;
+
+use App\Actions\GetArtifacts;
+use App\Helpers\Helpers;
+use PHPUnit\Framework\TestCase;
+
+class MockGetArtifacts extends GetArtifacts {
+
+ public function handle($workflow_run_id, $token): void
+ {
+ $data = [
+ 'artifacts' => [
+ [
+ 'name' => 'test1',
+ 'archive_download_url' => 'https://example1.com'
+ ],
+ [
+ 'name' => 'test2',
+ 'archive_download_url' => 'https://example2.com'
+ ],
+ ]
+ ];
+
+ $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/" . $workflow_run_id;
+ if (is_dir($workflowRunDirectory)) {
+ (new Helpers)->rmdirr($workflowRunDirectory);
+ }
+ mkdir($workflowRunDirectory, 0755, true);
+ foreach ($data['artifacts'] as $artifact) {
+ $filepath = $workflowRunDirectory . "/" . $artifact['name'] . ".zip";
+ (new MockFetchArtifact)->handle($artifact['archive_download_url'], $filepath, $token);
+ }
+ }
+}
+
+class GetArtifactsTest extends TestCase {
+
+ public function setUp(): void
+ {
+ $temp_dir = sys_get_temp_dir();
+ putenv("BUILDS_DIRECTORY=$temp_dir");
+ }
+
+ public function testHandleWithValidData(): void
+ {
+ $workflow_run_id = 123456;
+ $token = "test_token";
+ $getArtifacts = new MockGetArtifacts();
+ $getArtifacts->handle($workflow_run_id, $token);
+ $this->assertDirectoryExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" . $workflow_run_id);
+ $this->assertFileExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" . $workflow_run_id . "/test1.zip");
+ $this->assertFileExists(getenv('BUILDS_DIRECTORY') . "/winlibs/" . $workflow_run_id . "/test2.zip");
+ }
+
+ public function tearDown(): void
+ {
+ $workflowRunDirectory = getenv('BUILDS_DIRECTORY') . "/winlibs/123456";
+ if (is_dir($workflowRunDirectory)) {
+ (new Helpers)->rmdirr($workflowRunDirectory);
+ }
+ }
+}
diff --git a/tests/AuthTest.php b/tests/AuthTest.php
new file mode 100644
index 0000000..4a8b497
--- /dev/null
+++ b/tests/AuthTest.php
@@ -0,0 +1,27 @@
+<?php
+
+use App\Auth;
+use PHPUnit\Framework\TestCase;
+
+class AuthTest extends TestCase {
+ public function testAuthenticateWithValidToken() {
+ $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer valid_token';
+ putenv('AUTH_TOKEN=valid_token');
+ $auth = new Auth();
+ $this->assertTrue($auth->authenticate(), 'Authentication should succeed with valid token.');
+ }
+
+ public function testAuthenticateWithInvalidToken() {
+ $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer invalid_token';
+ putenv('AUTH_TOKEN=valid_token');
+ $auth = new Auth();
+ $this->assertFalse($auth->authenticate(), 'Authentication should fail with invalid token.');
+ }
+
+ public function testAuthenticateWithNoToken() {
+ unset($_SERVER['HTTP_AUTHORIZATION']);
+ putenv('AUTH_TOKEN=valid_token');
+ $auth = new Auth();
+ $this->assertFalse($auth->authenticate(), 'Authentication should fail with no token provided.');
+ }
+}
diff --git a/tests/BaseControllerTest.php b/tests/BaseControllerTest.php
new file mode 100644
index 0000000..c3e8099
--- /dev/null
+++ b/tests/BaseControllerTest.php
@@ -0,0 +1,60 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+use App\Http\BaseController;
+
+class MockBaseController extends BaseController {
+ protected function validate(array $data): bool {
+ return isset($data['key']);
+ }
+
+ protected function execute(array $data): void {
+ echo "Executed";
+ }
+}
+
+class BaseControllerTest extends TestCase {
+ private string $tempFile;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+ }
+
+ protected function tearDown(): void {
+ if (file_exists($this->tempFile)) {
+ unlink($this->tempFile);
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @throws JsonException
+ */
+ public function testHandleWithValidData() {
+ $data = json_encode(["key" => "value"]);
+ file_put_contents($this->tempFile, $data);
+ $controller = new MockBaseController($this->tempFile);
+ $this->expectOutputString("Executed");
+ $controller->handle();
+ }
+
+ /**
+ * @throws JsonException
+ */
+ public function testHandleWithInvalidData() {
+ $data = json_encode();
+ file_put_contents($this->tempFile, $data);
+ $controller = new MockBaseController($this->tempFile);
+ $this->expectOutputString('');
+ $controller->handle();
+ }
+
+ public function testHandleWithMalformedJson() {
+ $data = "{key: 'value'}";
+ file_put_contents($this->tempFile, $data);
+ $controller = new MockBaseController($this->tempFile);
+ $this->expectException(JsonException::class);
+ $controller->handle();
+ }
+}
diff --git a/tests/CommandTest.php b/tests/CommandTest.php
new file mode 100644
index 0000000..3d0f799
--- /dev/null
+++ b/tests/CommandTest.php
@@ -0,0 +1,27 @@
+<?php
+use PHPUnit\Framework\TestCase;
+use App\Console\Command;
+
+class TestCommand extends Command {
+ protected string $signature = "test {arg} {--option=}";
+
+ public function handle(): int {
+ return Command::SUCCESS;
+ }
+}
+
+class CommandTest extends TestCase {
+ public function testParseArgumentsAndOptions() {
+ $argv = ["script.php", "value", "--option=optValue"];
+ $command = new TestCommand(count($argv), $argv);
+
+ $this->assertEquals("value", $command->getArgument("arg"), "Argument parsing failed.");
+ $this->assertEquals("optValue", $command->getOption("option"), "Option parsing failed.");
+
+ $command->setOption("option", "newOptValue");
+ $this->assertEquals("newOptValue", $command->getOption("option"), "Option setting failed.");
+
+ $this->assertEquals("", $command->getDescription());
+ $this->assertEquals("test", $command->getSignature());
+ }
+}
diff --git a/tests/Console/Command/PeclCommandTest.php b/tests/Console/Command/PeclCommandTest.php
new file mode 100644
index 0000000..3810b11
--- /dev/null
+++ b/tests/Console/Command/PeclCommandTest.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Console\Command;
+
+use App\Helpers\Helpers;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\PeclCommand;
+use ZipArchive;
+
+class PeclCommandTest extends TestCase
+{
+ private string $baseDirectory;
+ private string $buildsDirectory;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->baseDirectory = sys_get_temp_dir() . '/pecl_test_base';
+ $this->buildsDirectory = sys_get_temp_dir() . '/builds';
+
+ mkdir($this->baseDirectory, 0755, true);
+ mkdir($this->buildsDirectory . '/pecl', 0755, true);
+
+ putenv("BUILDS_DIRECTORY=$this->buildsDirectory");
+
+ $zipPath = $this->buildsDirectory . '/pecl/test.zip';
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+ $zip->addFromString("test_file.txt", "content");
+ $zip->close();
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ (new Helpers)->rmdirr($this->baseDirectory);
+ (new Helpers)->rmdirr($this->buildsDirectory);
+ }
+
+ public function testPeclAddSuccessfullyExtractsZip(): void
+ {
+ $command = new PeclCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ $result = $command->handle();
+ $this->assertEquals(0, $result);
+
+ $extractedFiles = glob($this->baseDirectory . '/pecl/releases/*.*');
+ $this->assertCount(1, $extractedFiles);
+ $this->assertStringContainsString('test_file.txt', $extractedFiles[0]);
+
+ $content = file_get_contents($extractedFiles[0]);
+ $this->assertEquals('content', $content);
+ }
+
+ public function testPeclAddFailsWithoutBaseDirectory(): void
+ {
+ $command = new PeclCommand();
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertEquals('Base directory is required', $output);
+ $this->assertEquals(1, $result);
+
+ (new Helpers)->rmdirr($this->buildsDirectory . '/pecl');
+ $command->setOption('base-directory', $this->baseDirectory);
+ $result = $command->handle();
+ $this->assertEquals(0, $result);
+ }
+
+ public function testPeclAddFailsWithBrokenZip(): void
+ {
+ $zipPath = $this->buildsDirectory . '/pecl/broken.zip';
+ file_put_contents($zipPath, 'broken zip');
+
+ $command = new PeclCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertEquals('Failed to extract the extension', $output);
+ $this->assertEquals(1, $result);
+ }
+
+ public function testPeclAddFailsToExtractBuild(): void
+ {
+ $destinationDirectory = $this->baseDirectory . '/pecl/releases';
+ mkdir($destinationDirectory, 0555, true);
+ $command = new PeclCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ ob_start();
+ $result = @$command->handle();
+ $output = ob_get_clean();
+ chmod($destinationDirectory, 0755);
+ $this->assertStringContainsString('Failed to extract the extension build', $output);
+ $this->assertEquals(1, $result);
+ }
+}
diff --git a/tests/Console/Command/PhpCommandTest.php b/tests/Console/Command/PhpCommandTest.php
new file mode 100644
index 0000000..30808ce
--- /dev/null
+++ b/tests/Console/Command/PhpCommandTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Console\Command;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\PhpCommand;
+use App\Helpers\Helpers;
+use ZipArchive;
+
+class PhpCommandTest extends TestCase
+{
+ private string $baseDirectory;
+ private string $buildsDirectory;
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ // Set up temporary directories
+ $this->baseDirectory = sys_get_temp_dir() . '/php_command_base';
+ $this->buildsDirectory = sys_get_temp_dir() . '/builds';
+
+ mkdir($this->baseDirectory . '/releases', 0755, true);
+ mkdir($this->baseDirectory . '/qa', 0755, true);
+ mkdir($this->buildsDirectory . '/php', 0755, true);
+
+ putenv("BUILDS_DIRECTORY=$this->buildsDirectory");
+ }
+
+ protected function tearDown(): void
+
+ {
+ parent::tearDown();
+ // Clean up directories
+ (new Helpers)->rmdirr($this->baseDirectory);
+ (new Helpers)->rmdirr($this->buildsDirectory);
+ }
+
+ public static function buildsProvider(): array
+ {
+ return [
+ [[
+ 'php-8.4.1-Win32-vs17-x64.zip',
+ 'php-8.4.1-Win32-vs17-x86.zip',
+ 'php-8.4.1-nts-Win32-vs17-x64.zip',
+ 'php-8.4.1-nts-Win32-vs17-x86.zip',
+ 'php-8.4.1-src.zip',
+ 'php-debug-pack-8.4.1-Win32-vs17-x64.zip',
+ 'php-debug-pack-8.4.1-Win32-vs17-x86.zip',
+ 'php-debug-pack-8.4.1-nts-Win32-vs17-x64.zip',
+ 'php-debug-pack-8.4.1-nts-Win32-vs17-x86.zip',
+ 'php-devel-pack-8.4.1-Win32-vs17-x64.zip',
+ 'php-devel-pack-8.4.1-Win32-vs17-x86.zip',
+ 'php-devel-pack-8.4.1-nts-Win32-vs17-x64.zip',
+ 'php-devel-pack-8.4.1-nts-Win32-vs17-x86.zip',
+ 'php-test-pack-8.4.1.zip',
+ ]],[[
+ 'php-8.4.0-dev-Win32-vs17-x64.zip',
+ 'php-8.4.0-dev-Win32-vs17-x86.zip',
+ 'php-8.4.0-dev-nts-Win32-vs17-x64.zip',
+ 'php-8.4.0-dev-nts-Win32-vs17-x86.zip',
+ 'php-8.4.0-dev-src.zip',
+ 'php-debug-pack-8.4.0-dev-Win32-vs17-x64.zip',
+ 'php-debug-pack-8.4.0-dev-Win32-vs17-x86.zip',
+ 'php-debug-pack-8.4.0-dev-nts-Win32-vs17-x64.zip',
+ 'php-debug-pack-8.4.0-dev-nts-Win32-vs17-x86.zip',
+ 'php-devel-pack-8.4.0-dev-Win32-vs17-x64.zip',
+ 'php-devel-pack-8.4.0-dev-Win32-vs17-x86.zip',
+ 'php-devel-pack-8.4.0-dev-nts-Win32-vs17-x64.zip',
+ 'php-devel-pack-8.4.0-dev-nts-Win32-vs17-x86.zip',
+ 'php-test-pack-8.4.0-dev.zip',
+ ]]
+ ];
+ }
+
+ private function stageBuilds(array $phpZips, $zipPath): void
+ {
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+ foreach ($phpZips as $zipFileName) {
+ $zipFilePath = $this->buildsDirectory . '/php/' . $zipFileName;
+ $innerZip = new ZipArchive();
+ if ($innerZip->open($zipFilePath, ZipArchive::CREATE) === TRUE) {
+ $innerZip->addFromString("test_file.php", "<?php echo 'Hello, world!'; ?>");
+ $innerZip->close();
+ }
+ $zip->addFile($zipFilePath, $zipFileName);
+ }
+ $zip->close();
+ }
+ foreach ($phpZips as $zipFileName) {
+ unlink($this->buildsDirectory . '/php/' . $zipFileName);
+ }
+ }
+
+ #[DataProvider('buildsProvider')]
+ public function testCommandHandlesSuccessfulExecution(array $phpZips): void
+ {
+ $command = new PhpCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+
+ $this->stageBuilds($phpZips, $this->buildsDirectory . '/php/test.zip');
+
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result, "Command should return success.");
+
+ $expectedDestination = $this->baseDirectory . '/releases';
+ $this->assertDirectoryExists($expectedDestination, "Destination directory should exist.");
+ }
+
+ public function testCommandHandlerWithMissingTestPackZip(): void
+ {
+ $command = new PhpCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+
+ $this->stageBuilds(['php-8.4.0-dev-Win32-vs17-x64.zip'], $this->buildsDirectory . '/php/test.zip');
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertEquals('No test pack found in the artifact', $output);
+ $this->assertEquals(1, $result, "Command should return failure.");
+ }
+
+ public function testCommandHandlesMissingBaseDirectory(): void
+ {
+ $command = new PhpCommand();
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertEquals('Base directory is required', $output);
+ $this->assertEquals(1, $result);
+ }
+
+ public function testFailsToOpenZip(): void
+ {
+ $zipPath = $this->buildsDirectory . '/php/broken.zip';
+ file_put_contents($zipPath, "invalid zip content");
+ $command = new PhpCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ ob_start();
+ $result = $command->handle();
+ ob_get_clean();
+ $this->assertEquals(1, $result, "Command should return failure on broken zip.");
+ }
+
+ public function testCleanupAfterCommand(): void
+ {
+ $command = new PhpCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ $command->handle();
+ $tempDirectory = "/tmp/php-*";
+ $this->assertEmpty(glob($tempDirectory));
+ }
+}
diff --git a/tests/Console/Command/WinlibsCommandTest.php b/tests/Console/Command/WinlibsCommandTest.php
new file mode 100644
index 0000000..1d105f4
--- /dev/null
+++ b/tests/Console/Command/WinlibsCommandTest.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Console\Command;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+use App\Console\Command\WinlibsCommand;
+use App\Helpers\Helpers;
+use ZipArchive;
+
+class WinlibsCommandTest extends TestCase
+{
+ private string $baseDirectory;
+
+ private string $winlibsDirectory;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->baseDirectory = sys_get_temp_dir() . '/winlibs_test';
+ mkdir($this->baseDirectory, 0755, true);
+ putenv("BASE_DIRECTORY=$this->baseDirectory");
+
+ $this->winlibsDirectory = $this->baseDirectory . '/winlibs';
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ (new Helpers)->rmdirr($this->baseDirectory);
+ }
+
+ #[DataProvider('versionProvider')]
+ public function testSuccessfulFileOperations($phpVersion, $vsVersion, $arch, $stability): void
+ {
+ mkdir($this->winlibsDirectory . '/lib', 0755, true);
+
+ $library = 'lib';
+ $ref = '2.0.0';
+ $seriesFilePath = $this->baseDirectory . "/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+ file_put_contents($this->winlibsDirectory . '/lib/data.json', json_encode([
+ 'library' => $library,
+ 'ref' => $ref,
+ 'vs_version_targets' => $vsVersion,
+ 'php_versions' => $phpVersion,
+ 'stability' => $stability
+ ]));
+
+ $zipPath = $this->winlibsDirectory . "/lib/lib-$ref-$vsVersion-$arch.zip";
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+ $zip->addFromString("dummy_file.txt", "dummy content");
+ $zip->close();
+ }
+
+ $command = new WinlibsCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result, "Command should return success.");
+ $this->assertStringEqualsFile($seriesFilePath, "lib-$ref-$vsVersion-$arch.zip", "Series file should be updated correctly.");
+ }
+
+ #[DataProvider('versionProvider')]
+ public function testSuccessfulFileOperationsWithExistingSeriesFile($phpVersion, $vsVersion, $arch, $stability): void
+ {
+ mkdir($this->winlibsDirectory . '/lib', 0755, true);
+ mkdir($this->baseDirectory . '/php-sdk/deps/series', 0755, true);
+
+ $library = 'lib';
+ $ref = '2.0.0';
+ $seriesFilePath = $this->baseDirectory . "/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+ file_put_contents($this->winlibsDirectory . '/lib/data.json', json_encode([
+ 'library' => $library,
+ 'ref' => $ref,
+ 'vs_version_targets' => $vsVersion,
+ 'php_versions' => $phpVersion,
+ 'stability' => $stability
+ ]));
+
+ file_put_contents($seriesFilePath, "existing-$ref-$vsVersion-$arch.zip");
+
+ $zipPath = $this->winlibsDirectory . "/lib/lib-$ref-$vsVersion-$arch.zip";
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+ $zip->addFromString("dummy_file.txt", "dummy content");
+ $zip->close();
+ }
+
+ $command = new WinlibsCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result, "Command should return success.");
+ $this->assertStringContainsString("lib-$ref-$vsVersion-$arch.zip", file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+ }
+
+ #[DataProvider('versionProvider')]
+ public function testSuccessfulFileOperationsWithExistingOldLibraryInSeriesFile($phpVersion, $vsVersion, $arch, $stability): void
+ {
+ mkdir($this->winlibsDirectory . '/lib', 0755, true);
+ mkdir($this->baseDirectory . '/php-sdk/deps/series', 0755, true);
+
+ $library = 'lib';
+ $ref = '2.0.0';
+ $seriesFilePath = $this->baseDirectory . "/php-sdk/deps/series/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+
+ file_put_contents($this->winlibsDirectory . '/lib/data.json', json_encode([
+ 'library' => $library,
+ 'ref' => $ref,
+ 'vs_version_targets' => $vsVersion,
+ 'php_versions' => $phpVersion,
+ 'stability' => $stability
+ ]));
+
+ file_put_contents($seriesFilePath, "lib-1.0.0-$vsVersion-$arch.zip");
+
+ $zipPath = $this->winlibsDirectory . "/lib/lib-$ref-$vsVersion-$arch.zip";
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
+ $zip->addFromString("dummy_file.txt", "dummy content");
+ $zip->close();
+ }
+
+ $command = new WinlibsCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+
+ $result = $command->handle();
+
+ $this->assertEquals(0, $result, "Command should return success.");
+ $this->assertStringContainsString("lib-$ref-$vsVersion-$arch.zip", file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+ $this->assertStringNotContainsString("lib-1.0.0-$vsVersion-$arch.zip", file_get_contents($seriesFilePath), "Series file should be updated correctly.");
+ }
+
+ public static function versionProvider(): array
+ {
+ return [
+ ['7.4', 'vs15', 'x86', 'stable'],
+ ['8.0', 'vs16', 'x64', 'staging'],
+ ['8.1', 'vs17', 'x86', 'stable'],
+ ];
+ }
+
+ public function testCommandHandlesMissingBaseDirectory(): void
+ {
+ $command = new WinlibsCommand();
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertEquals('Base directory is required', $output);
+ $this->assertEquals(1, $result);
+ }
+
+ public function testHandlesCorruptDataFile(): void
+ {
+ $this->winlibsDirectory = $this->baseDirectory . '/winlibs/lib';
+ mkdir($this->winlibsDirectory, 0755, true);
+ file_put_contents($this->winlibsDirectory . '/data.json', '{corrupt json');
+
+ $command = new WinlibsCommand();
+ $command->setOption('base-directory', $this->baseDirectory);
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+ $this->assertStringContainsString('Syntax error', $output);
+ $this->assertEquals(1, $result);
+ }
+}
diff --git a/tests/ControllerInterfaceTest.php b/tests/ControllerInterfaceTest.php
new file mode 100644
index 0000000..cb97400
--- /dev/null
+++ b/tests/ControllerInterfaceTest.php
@@ -0,0 +1,8 @@
+<?php
+use PHPUnit\Framework\TestCase;
+
+class ControllerInterfaceTest extends TestCase {
+ public function testInterfaceExists() {
+ $this->assertTrue(interface_exists(App\Http\ControllerInterface::class), "ControllerInterface should exist.");
+ }
+}
diff --git a/tests/Helpers/HelpersTest.php b/tests/Helpers/HelpersTest.php
new file mode 100644
index 0000000..a89d18a
--- /dev/null
+++ b/tests/Helpers/HelpersTest.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Helpers;
+
+use PHPUnit\Framework\TestCase;
+use App\Helpers\Helpers;
+
+class HelpersTest extends TestCase
+{
+ private string $testDir;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->testDir = sys_get_temp_dir() . '/testDir';
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ // Ensure all files and directories are cleaned up after each test
+ if (file_exists($this->testDir)) {
+ $helper = new Helpers();
+ $helper->rmdirr($this->testDir);
+ }
+ }
+
+ public function testRemoveNonExistentDirectory(): void
+ {
+ $helper = new Helpers();
+ $this->assertFalse($helper->rmdirr($this->testDir . '/nonexistent'));
+ }
+
+ public function testRemoveDirectoryWithFiles(): void
+ {
+ mkdir($this->testDir, 0777, true);
+ file_put_contents($this->testDir . '/file.txt', 'Hello World');
+
+ $helper = new Helpers();
+ $result = $helper->rmdirr($this->testDir);
+ $this->assertTrue($result);
+ $this->assertDirectoryDoesNotExist($this->testDir);
+ }
+
+ public function testRemoveDirectoryWithNestedDirectories(): void
+ {
+ mkdir($this->testDir . '/nested', 0777, true);
+ file_put_contents($this->testDir . '/nested/file.txt', 'Hello World');
+
+ $helper = new Helpers();
+ $result = $helper->rmdirr($this->testDir);
+ $this->assertTrue($result);
+ $this->assertDirectoryDoesNotExist($this->testDir);
+ }
+}
diff --git a/tests/Http/Controllers/IndexControllerTest.php b/tests/Http/Controllers/IndexControllerTest.php
new file mode 100644
index 0000000..7f1bca7
--- /dev/null
+++ b/tests/Http/Controllers/IndexControllerTest.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\IndexController;
+use PHPUnit\Framework\TestCase;
+
+class IndexControllerTest extends TestCase {
+ public function testHandle() {
+ $controller = new IndexController();
+ $controller->handle();
+ $this->expectOutputString('Welcome!');
+ $this->assertTrue($controller->validate());
+ ob_start();
+ $controller->execute();
+ $this->assertEmpty(ob_get_clean());
+ }
+}
diff --git a/tests/Http/Controllers/PeclControllerTest.php b/tests/Http/Controllers/PeclControllerTest.php
new file mode 100644
index 0000000..ffae927
--- /dev/null
+++ b/tests/Http/Controllers/PeclControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\BaseController;
+use PHPUnit\Framework\TestCase;
+
+class MockPeclController extends BaseController {
+ protected function validate(array $data): bool {
+ return isset($data['key']);
+ }
+
+ protected function execute(array $data): void {
+ echo "Executed";
+ }
+
+ public function handle(): void
+ {
+ $data = json_decode(file_get_contents($this->inputPath), true);
+
+ if ($this->validate($data)) {
+ $this->execute($data);
+ }
+ }
+}
+
+class PeclControllerTest extends TestCase {
+ public function testHandleWithValidData() {
+ $data = json_encode(["key" => "value"]);
+ $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+ file_put_contents($tempFile, $data);
+ $controller = new MockPeclController($tempFile);
+ $this->expectOutputString("Executed");
+ $controller->handle();
+ unlink($tempFile);
+ }
+}
diff --git a/tests/Http/Controllers/PhpControllerTest.php b/tests/Http/Controllers/PhpControllerTest.php
new file mode 100644
index 0000000..d677f7f
--- /dev/null
+++ b/tests/Http/Controllers/PhpControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\PhpController;
+use PHPUnit\Framework\TestCase;
+
+class MockPhpController extends PhpController {
+ protected function validate(array $data): bool {
+ return isset($data['key']);
+ }
+
+ protected function execute(array $data): void {
+ echo "Executed";
+ }
+
+ public function handle(): void
+ {
+ $data = json_decode(file_get_contents($this->inputPath), true);
+
+ if ($this->validate($data)) {
+ $this->execute($data);
+ }
+ }
+}
+
+class PhpControllerTest extends TestCase {
+ public function testHandleWithValidData() {
+ $data = json_encode(["key" => "value"]);
+ $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+ file_put_contents($tempFile, $data);
+ $controller = new MockPhpController($tempFile);
+ $this->expectOutputString("Executed");
+ $controller->handle();
+ unlink($tempFile);
+ }
+}
diff --git a/tests/Http/Controllers/WinlibsControllerTest.php b/tests/Http/Controllers/WinlibsControllerTest.php
new file mode 100644
index 0000000..f4ed100
--- /dev/null
+++ b/tests/Http/Controllers/WinlibsControllerTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Http\Controllers;
+
+use App\Http\Controllers\WinlibsController;
+use PHPUnit\Framework\TestCase;
+
+class MockWinlibsController extends WinlibsController {
+ protected function validate(array $data): bool {
+ return isset($data['key']);
+ }
+
+ protected function execute(array $data): void {
+ echo "Executed";
+ }
+
+ public function handle(): void
+ {
+ $data = json_decode(file_get_contents($this->inputPath), true);
+
+ if ($this->validate($data)) {
+ $this->execute($data);
+ }
+ }
+}
+
+class WinlibsControllerTest extends TestCase {
+ public function testHandleWithValidData() {
+ $data = json_encode(["key" => "value"]);
+ $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
+ file_put_contents($tempFile, $data);
+ $controller = new MockWinlibsController($tempFile);
+ $this->expectOutputString("Executed");
+ $controller->handle();
+ unlink($tempFile);
+ }
+}
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
new file mode 100644
index 0000000..de3a176
--- /dev/null
+++ b/tests/RouterTest.php
@@ -0,0 +1,51 @@
+<?php
+use PHPUnit\Framework\TestCase;
+use App\Router;
+
+class RouterTest extends TestCase {
+ public function testHandleIndexRequest() {
+ $_SERVER['REQUEST_URI'] = '/';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['HTTP_AUTHORIZATION'] = '';
+ $router = new Router();
+ $router->registerRoute('/', 'GET', 'App\Http\Controllers\IndexController'
+ );
+ ob_start();
+ $router->handleRequest();
+ $output = ob_get_clean();
+ $this->assertEquals('Welcome!', $output, 'Should respond with Welcome! for index route.');
+ }
+
+ public function testHandleRequestUnauthorized() {
+ $_SERVER['REQUEST_URI'] = '/protected';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['HTTP_AUTHORIZATION'] = '';
+ $router = new Router();
+ $router->registerRoute('/protected', 'GET', 'TestHandler', true);
+ ob_start();
+ $router->handleRequest();
+ $output = ob_get_clean();
+ $this->assertEquals('Unauthorized', $output, 'Should respond with Unauthorized for protected routes.');
+ }
+
+ public function testHandleRequestMethodNotAllowed() {
+ $_SERVER['REQUEST_URI'] = '/test';
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $router = new Router();
+ $router->registerRoute('/test', 'GET', 'TestHandler');
+ ob_start();
+ $router->handleRequest();
+ $output = ob_get_clean();
+ $this->assertStringContainsString('Method Not Allowed', $output, 'Should respond with Method Not Allowed.');
+ }
+
+ public function testHandleRequestNotFound() {
+ $_SERVER['REQUEST_URI'] = '/nonexistent';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $router = new Router();
+ ob_start();
+ $router->handleRequest();
+ $output = ob_get_clean();
+ $this->assertEquals('Not Found', $output, 'Should respond with Not Found for unregistered routes.');
+ }
+}
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
new file mode 100644
index 0000000..c355ffb
--- /dev/null
+++ b/tests/ValidatorTest.php
@@ -0,0 +1,54 @@
+<?php
+
+use App\Validator;
+use PHPUnit\Framework\TestCase;
+
+class ValidatorTest extends TestCase {
+ public function testValidateRequiredField() {
+ $validator = new Validator(['name' => 'required']);
+ $validator->validate(['name' => 'John']);
+ $this->assertTrue($validator->isValid(), 'Validation should pass when required field is present.');
+ }
+
+ public function testValidateMissingRequiredField() {
+ $validator = new Validator(['name' => 'required']);
+ $validator->validate();
+ $this->assertFalse($validator->isValid(), 'Validation should fail when required field is missing.');
+ }
+
+ public function testValidateStringField() {
+ $validator = new Validator(['name' => 'string']);
+ $validator->validate(['name' => 123]);
+ $this->assertFalse($validator->isValid(), 'Validation should fail when required field is missing.');
+ }
+
+ public function testValidationRegexField() {
+ $validator = new Validator(['date' => 'regex:/\d{4}-\d{2}-\d{2}/']);
+ $validator->validate(['date' => '01/01/2025']);
+ $this->assertFalse($validator->isValid(), 'Validation should fail when regex does not match.');
+
+ $validator->validate(['date' => '2025-01-01']);
+ $this->assertTrue($validator->isValid(), 'Validation should pass when regex matches.');
+ }
+
+ public function testValidateUrlField() {
+ $validator = new Validator(['website' => 'url']);
+ $validator->validate(['website' => 'https://example.com']);
+ $this->assertTrue($validator->isValid(), 'Validation should pass for a valid URL.');
+ }
+
+ public function testValidateInvalidUrlField() {
+ $validator = new Validator(['website' => 'url']);
+ $validator->validate(['website' => 'invalid-url']);
+ $this->assertFalse($validator->isValid(), 'Validation should fail for an invalid URL.');
+ }
+
+ public function testErrorMessages() {
+ $validator = new Validator(['website' => 'url']);
+ $validator->validate(['website' => 'invalid-url']);
+ $errors = $validator->errors();
+ $this->assertNotEmpty($errors, 'Errors should not be empty for invalid validation.');
+ $this->assertStringContainsString('must be a valid URL', $errors['website'][0], 'Error message should match.');
+ $this->assertStringContainsString('website: The website field must be a valid URL.', $validator->__toString());
+ }
+}