From 176bf2e0941526d545be1b540026d5ed7c4825a4 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 16 Sep 2026 19:21:09 +0200 Subject: [PATCH 1/4] test: add server-free unit tests for note, path and cursor logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite so far has been integration-only: everything under tests/api/ boots a real Nextcloud, so there was no way to exercise the app's pure logic without a server, and no test at all covered the functions that turn user input into file names. composer.json already declared a `test:unit` script pointing at tests/unit/phpunit.xml, but neither the config nor PHPUnit itself was present. This makes that script real: * tests/unit/phpunit.xml + bootstrap.php — no server, no database, no web server. `composer test:unit` works on a bare checkout. The bootstrap declares OC\Hooks\Emitter, which OCP\Files\IRootFolder extends but nextcloud/ocp does not ship, the same way tests/stubs/ocp.php already fills gaps for Psalm. * phpunit/phpunit and doctrine/dbal as dev dependencies. DBAL is needed because mocking OCP\IDBConnection reflects over IQueryBuilder, whose signatures reference Doctrine's types; the server provides it at runtime. * OCA\Notes\ is mapped in autoload-dev — the app relies on Nextcloud's own app autoloader, which is absent outside a server. * A separate phpunit-unit.yml workflow so these run on every pull request in seconds, independently of the server-backed test.yml. The tests cover NoteUtil (category-path normalisation including traversal attempts, title derivation, collision-safe file names, markdown stripping), NotesService (which files count as notes, the folder walk, titles from content), Note (title, category, excerpt, BOM and object-storage content handling), Util::retryIfLocked and ChunkCursor. No production code is touched by this commit. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger --- .github/workflows/phpunit-unit.yml | 74 + .gitignore | 2 + Makefile | 7 +- composer.json | 4 + composer.lock | 2263 ++++++++++++++++++--- tests/psalm-baseline.xml | 3 + tests/unit/Controller/ChunkCursorTest.php | 72 + tests/unit/NotesTestCase.php | 42 + tests/unit/Service/NoteTest.php | 113 + tests/unit/Service/NoteUtilTest.php | 150 ++ tests/unit/Service/NotesServiceTest.php | 173 ++ tests/unit/Service/UtilTest.php | 71 + tests/unit/bootstrap.php | 31 + tests/unit/phpunit.xml | 25 + 14 files changed, 2800 insertions(+), 230 deletions(-) create mode 100644 .github/workflows/phpunit-unit.yml create mode 100644 tests/unit/Controller/ChunkCursorTest.php create mode 100644 tests/unit/NotesTestCase.php create mode 100644 tests/unit/Service/NoteTest.php create mode 100644 tests/unit/Service/NoteUtilTest.php create mode 100644 tests/unit/Service/NotesServiceTest.php create mode 100644 tests/unit/Service/UtilTest.php create mode 100644 tests/unit/bootstrap.php create mode 100644 tests/unit/phpunit.xml diff --git a/.github/workflows/phpunit-unit.yml b/.github/workflows/phpunit-unit.yml new file mode 100644 index 000000000..f5d7a8c8f --- /dev/null +++ b/.github/workflows/phpunit-unit.yml @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: PHPUnit unit + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-unit-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + matrix: + runs-on: ubuntu-latest-low + outputs: + php-versions: ${{ steps.versions.outputs.php-versions }} + steps: + - name: Checkout app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Get version matrix + id: versions + uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.0.0 + + unit-tests: + runs-on: ubuntu-latest + needs: matrix + strategy: + fail-fast: false + matrix: + php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}} + + name: unit-tests (php ${{ matrix.php-versions }}) + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 + with: + php-version: ${{ matrix.php-versions }} + extensions: ctype, curl, dom, fileinfo, iconv, intl, json, libxml, mbstring, openssl, posix, simplexml, xmlreader, xmlwriter, zip, zlib + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run unit tests + run: composer run test:unit + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: unit-tests + + if: always() + + name: unit-tests-summary + + steps: + - name: Summary status + run: if ${{ needs.unit-tests.result != 'success' && needs.unit-tests.result != 'skipped' }}; then exit 1; fi diff --git a/.gitignore b/.gitignore index 797b6a852..dcc555988 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ report coverage/ clover.xml .php-cs-fixer.cache +.phpunit.cache/ +.phpunit.result.cache appinfo/info.xsd # just sane ignores diff --git a/Makefile b/Makefile index fd0d4395f..f51dc8973 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ app_name=notes project_dir=$(CURDIR)/../$(app_name) build_dir=$(CURDIR)/build/artifacts cert_dir=$(HOME)/.nextcloud/certificates -php_dirs=appinfo/ lib/ tests/api/ +php_dirs=appinfo/ lib/ tests/api/ tests/unit/ all: dev-setup build @@ -87,7 +87,10 @@ watch-js: ##### Testing ##### -test: test-api +test: test-unit test-api + +test-unit: + composer run test:unit test-api: phpunit --bootstrap vendor/autoload.php --testdox tests/api/ diff --git a/composer.json b/composer.json index 84083c5ee..723264bb6 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,21 @@ { "require-dev": { + "doctrine/dbal": "^4", "guzzlehttp/guzzle": "^8", "nextcloud/coding-standard": "^1.0", "nextcloud/ocp": "dev-stable33", "phan/phan": "^6", "php-cs-fixer/shim": "3.95.23", + "phpunit/phpunit": "^10", "psalm/phar": "^5.26", "squizlabs/php_codesniffer": "^4", "staabm/annotate-pull-request-from-checkstyle": "^1.1.0" }, "autoload-dev": { "psr-4": { + "OCA\\Notes\\": "lib/", "OCA\\Notes\\Tests\\API\\": "tests/api/", + "OCA\\Notes\\Tests\\Unit\\": "tests/unit/", "OCP\\": "vendor/nextcloud/ocp/OCP/", "OC\\": "vendor/nextcloud/ocp/OC/" } diff --git a/composer.lock b/composer.lock index 49b2eeb87..ac026fa08 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "247ab3a67d53a67542fe94920affdece", + "content-hash": "e710552208a4f7d0637bb2cef3f75e7a", "packages": [], "packages-dev": [ { @@ -278,6 +278,112 @@ }, "time": "2026-01-12T21:07:10+00:00" }, + { + "name": "doctrine/dbal", + "version": "4.4.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-07-21T14:34:40+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -706,6 +812,66 @@ ], "time": "2026-05-12T16:22:19+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, { "name": "netresearch/jsonmapper", "version": "v5.0.1", @@ -850,6 +1016,63 @@ }, "time": "2026-09-02T01:51:47+00:00" }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, { "name": "phan/phan", "version": "6.0.7", @@ -1040,6 +1263,124 @@ ], "time": "2026-01-28T23:32:31+00:00" }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "php-cs-fixer/shim", "version": "v3.95.23", @@ -1322,157 +1663,470 @@ "time": "2026-01-25T14:56:51+00:00" }, { - "name": "psalm/phar", - "version": "5.26.1", + "name": "phpunit/php-code-coverage", + "version": "10.1.16", "source": { "type": "git", - "url": "https://github.com/psalm/phar.git", - "reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/psalm/phar/zipball/8a38e7ad04499a0ccd2c506fd1da6fc01fff4547", - "reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, - "conflict": { - "vimeo/psalm": "*" + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, - "bin": [ - "psalm.phar" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" ], - "description": "Composer-based Psalm Phar", "support": { - "issues": "https://github.com/psalm/phar/issues", - "source": "https://github.com/psalm/phar/tree/5.26.1" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, - "time": "2024-09-09T16:22:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" }, { - "name": "psr/clock", - "version": "1.0.0", + "name": "phpunit/php-file-iterator", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/clock.git", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0" + "php": ">=8.1" }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Clock\\": "src/" + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common interface for reading the clock.", - "homepage": "https://github.com/php-fig/clock", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "clock", - "now", - "psr", - "psr-20", - "time" + "filesystem", + "iterator" ], "support": { - "issues": "https://github.com/php-fig/clock/issues", - "source": "https://github.com/php-fig/clock/tree/1.0.0" + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, - "time": "2022-11-25T14:36:26+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "phpunit/php-invoker", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "template" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, - "time": "2021-11-05T16:47:00+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "phpunit/php-timer", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.64", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.64" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-06T14:50:35+00:00" + }, + { + "name": "psalm/phar", + "version": "5.26.1", + "source": { + "type": "git", + "url": "https://github.com/psalm/phar.git", + "reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/psalm/phar/zipball/8a38e7ad04499a0ccd2c506fd1da6fc01fff4547", + "reference": "8a38e7ad04499a0ccd2c506fd1da6fc01fff4547", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "vimeo/psalm": "*" + }, + "bin": [ + "psalm.phar" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer-based Psalm Phar", + "support": { + "issues": "https://github.com/psalm/phar/issues", + "source": "https://github.com/psalm/phar/tree/5.26.1" + }, + "time": "2024-09-09T16:22:43+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" }, "type": "library", "extra": { @@ -1482,7 +2136,7 @@ }, "autoload": { "psr-4": { - "Psr\\EventDispatcher\\": "src/" + "Psr\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1492,300 +2146,1403 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Standard interfaces for event handling.", + "description": "Common interface for caching libraries", "keywords": [ - "events", + "cache", "psr", - "psr-14" + "psr-6" ], "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sabre/event", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "bc31c95c94c0a7104a7565a2a41ffddc8dcf3012" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/bc31c95c94c0a7104a7565a2a41ffddc8dcf3012", + "reference": "bc31c95c94c0a7104a7565a2a41ffddc8dcf3012", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2026-04-27T12:18:32+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "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/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" }, { - "name": "psr/http-client", - "version": "1.0.3", + "name": "sebastian/object-enumerator", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "5.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "source": "https://github.com/php-fig/http-client" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, - "time": "2023-09-23T14:17:50+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" }, { - "name": "psr/http-factory", - "version": "1.1.0", + "name": "sebastian/object-reflector", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "3.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "source": "https://github.com/php-fig/http-factory" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, - "time": "2024-04-15T12:06:14+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" }, { - "name": "psr/http-message", - "version": "2.0", + "name": "sebastian/recursion-context", + "version": "5.0.2", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5d32fe257a9b39cb63146924d6b4e32a22d4502a", + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "5.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.2" }, - "time": "2023-04-04T09:54:51+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-11T05:27:39+00:00" }, { - "name": "psr/log", - "version": "3.0.2", + "name": "sebastian/type", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, - "time": "2024-09-11T13:17:53+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" }, { - "name": "sabre/event", - "version": "6.1.0", + "name": "sebastian/version", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/sabre-io/event.git", - "reference": "bc31c95c94c0a7104a7565a2a41ffddc8dcf3012" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/event/zipball/bc31c95c94c0a7104a7565a2a41ffddc8dcf3012", - "reference": "bc31c95c94c0a7104a7565a2a41ffddc8dcf3012", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": "^8.2" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^10.5", - "rector/rector": "^2.4" + "php": ">=8.1" }, "type": "library", - "autoload": { - "files": [ - "lib/coroutine.php", - "lib/Loop/functions.php", - "lib/Promise/functions.php" - ], - "psr-4": { - "Sabre\\Event\\": "lib/" + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "sabre/event is a library for lightweight event-based programming", - "homepage": "http://sabre.io/event/", - "keywords": [ - "EventEmitter", - "async", - "coroutine", - "eventloop", - "events", - "hooks", - "plugin", - "promise", - "reactor", - "signal" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/event/issues", - "source": "https://github.com/fruux/sabre-event" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, - "time": "2026-04-27T12:18:32+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -2769,6 +4526,56 @@ ], "time": "2026-07-28T07:33:02+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + }, { "name": "webmozart/assert", "version": "2.4.1", diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index f9077f1cf..b7e9d441e 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -110,6 +110,9 @@ + + + diff --git a/tests/unit/Controller/ChunkCursorTest.php b/tests/unit/Controller/ChunkCursorTest.php new file mode 100644 index 000000000..d2d75376e --- /dev/null +++ b/tests/unit/Controller/ChunkCursorTest.php @@ -0,0 +1,72 @@ +timeStart->getTimestamp()); + self::assertSame(1749000000, $cursor->noteLastUpdate); + self::assertSame(4711, $cursor->noteId); + self::assertSame('1750000000-1749000000-4711', $cursor->toString()); + } + + public function testZeroesAreAValidCursor(): void { + $cursor = ChunkCursor::fromString('0-0-0'); + + self::assertNotNull($cursor); + self::assertSame('0-0-0', $cursor->toString()); + } + + /** + * @return array + */ + public static function malformedCursors(): array { + return [ + 'empty' => [''], + 'too few parts' => ['1750000000-1749000000'], + 'too many parts' => ['1-2-3-4'], + 'leading separator' => ['-1-2-3'], + 'non numeric' => ['a-b-c'], + 'float' => ['1.5-2-3'], + 'whitespace padded' => [' 1-2-3 '], + 'newline injected' => ["1-2-3\n4-5-6"], + ]; + } + + #[DataProvider('malformedCursors')] + public function testMalformedCursorIsRejected(string $input): void { + self::assertNull(ChunkCursor::fromString($input)); + } + + public function testFromNoteUsesTheGivenSyncStartAndNotTheNotesOwnTime(): void { + $meta = new Meta(); + $meta->setLastUpdate(1749000000); + + $note = $this->createMock(Note::class); + $note->method('getId')->willReturn(4711); + + $cursor = ChunkCursor::fromNote( + (new \DateTime())->setTimestamp(1750000000), + new MetaNote($note, $meta), + ); + + self::assertSame('1750000000-1749000000-4711', $cursor->toString()); + } +} diff --git a/tests/unit/NotesTestCase.php b/tests/unit/NotesTestCase.php new file mode 100644 index 000000000..275b1805f --- /dev/null +++ b/tests/unit/NotesTestCase.php @@ -0,0 +1,42 @@ +createMock(IL10N::class); + $l10n->method('t')->willReturnArgument(0); + + $db = $this->createMock(IDBConnection::class); + $db->method('supports4ByteText')->willReturn(true); + + return new NoteUtil( + new Util($l10n, $this->createMock(LoggerInterface::class)), + $this->createMock(IRootFolder::class), + $db, + $this->createMock(TagService::class), + $this->createMock(IManager::class), + $this->createMock(IUserSession::class), + $this->createMock(SettingsService::class), + ); + } +} diff --git a/tests/unit/Service/NoteTest.php b/tests/unit/Service/NoteTest.php new file mode 100644 index 000000000..66c058682 --- /dev/null +++ b/tests/unit/Service/NoteTest.php @@ -0,0 +1,113 @@ +noteUtil = $this->createNoteUtil(); + } + + /** + * @param string|false $content what the storage returns for getContent() + */ + private function note(string $path, string|false $content = '', int $size = 1): Note { + $file = $this->createMock(File::class); + $file->method('getName')->willReturn(basename($path)); + $file->method('getPath')->willReturn($path); + $file->method('getContent')->willReturn($content); + $file->method('getSize')->willReturn($size); + + $notesFolder = $this->createMock(Folder::class); + $notesFolder->method('getPath')->willReturn(self::NOTES_PATH); + + return new Note($file, $notesFolder, $this->noteUtil); + } + + /** + * @return array + */ + public static function fileNames(): array { + return [ + 'txt' => ['Shopping.txt', 'Shopping'], + 'md' => ['Shopping.md', 'Shopping'], + 'dots in name' => ['v1.2.notes.md', 'v1.2.notes'], + 'no extension' => ['Makefile', 'Makefile'], + 'unicode' => ['Grüße 日本語.md', 'Grüße 日本語'], + 'numbered collision' => ['Shopping (2).txt', 'Shopping (2)'], + ]; + } + + #[DataProvider('fileNames')] + public function testTitleComesFromTheFileName(string $fileName, string $expected): void { + self::assertSame($expected, $this->note(self::NOTES_PATH . '/' . $fileName)->getTitle()); + } + + public function testNoteDirectlyInTheNotesFolderHasNoCategory(): void { + self::assertSame('', $this->note(self::NOTES_PATH . '/loose.txt')->getCategory()); + } + + public function testCategoryIsTheFullPathBelowTheNotesFolder(): void { + self::assertSame( + 'Work/Projects/2026', + $this->note(self::NOTES_PATH . '/Work/Projects/2026/a.txt')->getCategory(), + ); + } + + public function testUtf8ByteOrderMarkIsStripped(): void { + self::assertSame('# Heading', $this->note(self::NOTES_PATH . '/a.md', "\u{FEFF}# Heading")->getContent()); + } + + public function testEmptyFileOnObjectStorageReadsAsEmptyString(): void { + // object storage returns false rather than '' for a zero-byte file + self::assertSame('', $this->note(self::NOTES_PATH . '/a.md', false, 0)->getContent()); + } + + public function testUnreadableContentThrows(): void { + $note = $this->note(self::NOTES_PATH . '/a.md', false, 42); + + $this->expectException(\Exception::class); + $note->getContent(); + } + + public function testExcerptSkipsTheTitleLine(): void { + $note = $this->note(self::NOTES_PATH . '/Shopping.txt', "Shopping\nmilk and eggs"); + + self::assertSame('milk and eggs', $note->getExcerpt()); + } + + public function testExcerptStripsMarkdownAndFlattensNewlines(): void { + $note = $this->note(self::NOTES_PATH . '/Shopping.txt', "Shopping\n- milk\n- eggs"); + + self::assertSame("milk\u{2003}eggs", $note->getExcerpt()); + } + + public function testExcerptIsTruncatedWithAnEllipsis(): void { + $excerpt = $this->note(self::NOTES_PATH . '/a.txt', str_repeat('x', 250))->getExcerpt(); + + self::assertSame(101, mb_strlen($excerpt, 'utf-8')); + self::assertStringEndsWith('…', $excerpt); + } + + public function testEmptyNoteHasAnEmptyExcerpt(): void { + self::assertSame('', $this->note(self::NOTES_PATH . '/a.txt', '')->getExcerpt()); + } +} diff --git a/tests/unit/Service/NoteUtilTest.php b/tests/unit/Service/NoteUtilTest.php new file mode 100644 index 000000000..f5039e607 --- /dev/null +++ b/tests/unit/Service/NoteUtilTest.php @@ -0,0 +1,150 @@ +noteUtil = $this->createNoteUtil(); + } + + /** + * @return array + */ + public static function categoryPaths(): array { + return [ + 'nested' => ['Work/Projects', 'Work/Projects'], + 'leading slash' => ['/Work', 'Work'], + 'trailing slash' => ['Work/', 'Work'], + 'double slash collapses' => ['Work//Projects', 'Work/Projects'], + 'whitespace trimmed' => [' Work ', 'Work'], + 'unicode kept' => ['Grüße/日本語', 'Grüße/日本語'], + 'leading dot dropped' => ['.hidden', 'hidden'], + 'windows-illegal stripped' => ['a*b|c:d"eg?h', 'abcdefgh'], + 'relative parent' => ['../../etc', 'etc'], + 'parent between names' => ['Work/../Secret', 'Work/Secret'], + 'absolute unix' => ['/etc/passwd', 'etc/passwd'], + 'backslash stripped' => ['C:\Windows', 'CWindows'], + ]; + } + + #[DataProvider('categoryPaths')] + public function testNormalizeCategoryPath(string $input, string $expected): void { + $normalized = $this->noteUtil->normalizeCategoryPath($input); + + self::assertSame($expected, $normalized); + self::assertNotContains('..', explode('/', $normalized)); + } + + /** + * @return array + */ + public static function titles(): array { + return [ + 'plain' => ['Shopping list', 'Shopping list'], + 'first line only' => ["Title\nbody text", 'Title'], + 'trimmed' => [' Title ', 'Title'], + 'slash stripped' => ['a/b', 'ab'], + 'leading dot dropped' => ['.hidden', 'hidden'], + 'markdown kept' => ['# Heading', '# Heading'], + 'tabs become spaces' => ["A\tB", 'A B'], + 'empty falls back' => ['', 'New note'], + 'nothing usable falls back' => ['///', 'New note'], + ]; + } + + #[DataProvider('titles')] + public function testGetSafeTitle(string $input, string $expected): void { + self::assertSame($expected, $this->noteUtil->getSafeTitle($input)); + } + + public function testGetSafeTitleCapCountsCharactersNotBytes(): void { + $title = $this->noteUtil->getSafeTitle(str_repeat('日', 250)); + + self::assertSame(100, mb_strlen($title, 'UTF-8')); + self::assertTrue(mb_check_encoding($title, 'UTF-8')); + } + + /** + * @return array + */ + public static function markdown(): array { + return [ + 'atx heading' => ['# Heading', 'Heading'], + 'atx heading closed' => ['## Heading ##', 'Heading'], + 'setext underline removed' => ["Heading\n=======", "Heading\n"], + 'bullet' => ['- item', 'item'], + 'bold' => ['**bold**', 'bold'], + 'italic underscore' => ['_italic_', 'italic'], + 'inner dash kept' => ['well-known', 'well-known'], + ]; + } + + #[DataProvider('markdown')] + public function testStripMarkdown(string $input, string $expected): void { + self::assertSame($expected, $this->noteUtil->stripMarkdown($input)); + } + + /** + * @param array $existing filename => file id already in the folder + */ + private function folderContaining(array $existing): Folder { + $folder = $this->createMock(Folder::class); + $folder->method('nodeExists') + ->willReturnCallback(static fn (string $name): bool => array_key_exists($name, $existing)); + $folder->method('get') + ->willReturnCallback(function (string $name) use ($existing): Node { + $node = $this->createMock(Node::class); + $node->method('getId')->willReturn($existing[$name] ?? 0); + return $node; + }); + return $folder; + } + + public function testGenerateFileNameUsesTheTitleWhenTheNameIsFree(): void { + self::assertSame( + 'Title.txt', + $this->noteUtil->generateFileName($this->folderContaining([]), 'Title', '.txt', -1), + ); + } + + public function testGenerateFileNameKeepsTheNameOfTheNoteItself(): void { + $folder = $this->folderContaining(['Title.txt' => 42]); + + self::assertSame('Title.txt', $this->noteUtil->generateFileName($folder, 'Title', '.txt', 42)); + } + + public function testGenerateFileNameCountsUpPastExistingSuffixes(): void { + $folder = $this->folderContaining([ + 'Title.txt' => 42, + 'Title (2).txt' => 43, + 'Title (3).txt' => 44, + ]); + + self::assertSame('Title (4).txt', $this->noteUtil->generateFileName($folder, 'Title', '.txt', 7)); + } + + public function testGenerateFileNameShortensATitleAtTheLengthCapToFitTheSuffix(): void { + $longTitle = str_repeat('a', 100); + $folder = $this->folderContaining([$longTitle . '.txt' => 42]); + + $filename = $this->noteUtil->generateFileName($folder, $longTitle, '.txt', 7); + + self::assertSame(str_repeat('a', 96) . ' (2).txt', $filename); + } +} diff --git a/tests/unit/Service/NotesServiceTest.php b/tests/unit/Service/NotesServiceTest.php new file mode 100644 index 000000000..92e300d85 --- /dev/null +++ b/tests/unit/Service/NotesServiceTest.php @@ -0,0 +1,173 @@ +notesService = new NotesService( + $this->createMock(MetaService::class), + $this->createMock(SettingsService::class), + $this->createNoteUtil(), + $this->createMock(IFilenameValidator::class), + ); + } + + /** + * Builds a mocked folder tree. An array value is a subfolder, a string + * value is a file name. + * + * @param array> $spec + * @return Folder&MockObject + */ + private function folder(array $spec): Folder { + $children = []; + foreach ($spec as $name => $value) { + if (is_array($value)) { + $sub = $this->folder($value); + $sub->method('getName')->willReturn((string)$name); + $children[] = $sub; + } else { + $children[] = $this->file($value); + } + } + + $folder = $this->createMock(Folder::class); + $folder->method('getType')->willReturn(FileInfo::TYPE_FOLDER); + $folder->method('getDirectoryListing')->willReturn($children); + return $folder; + } + + /** @return File&MockObject */ + private function file(string $name): File { + $file = $this->createMock(File::class); + $file->method('getType')->willReturn(FileInfo::TYPE_FILE); + $file->method('getName')->willReturn($name); + $file->method('getId')->willReturn($this->nextFileId++); + return $file; + } + + /** + * @param array> $spec + * @return array{files: array, categories: list} + */ + private function gather(array $spec, string $customExtension = 'md', bool $showHidden = false): array { + $method = new \ReflectionMethod(NotesService::class, 'gatherNoteFiles'); + /** @var array{files: array, categories: list} $result */ + $result = $method->invoke(null, $customExtension, $this->folder($spec), $showHidden); + return $result; + } + + /** + * @param array> $spec + * @return list + */ + private function gatheredNames(array $spec, string $customExtension = 'md', bool $showHidden = false): array { + $names = array_map( + static fn (File $f): string => $f->getName(), + $this->gather($spec, $customExtension, $showHidden)['files'], + ); + sort($names); + return array_values($names); + } + + public function testRecognisesTheBuiltInNoteExtensions(): void { + self::assertSame( + ['a.markdown', 'b.md', 'c.note', 'd.org', 'e.txt'], + $this->gatheredNames(['a.markdown', 'b.md', 'c.note', 'd.org', 'e.txt']), + ); + } + + public function testIgnoresFilesThatAreNotNotes(): void { + self::assertSame( + ['keep.md'], + $this->gatheredNames(['keep.md', 'photo.jpg', 'report.pdf', 'archive.zip', 'noextension']), + ); + } + + public function testExtensionMatchingIsCaseInsensitive(): void { + self::assertSame(['LOUD.TXT', 'Mixed.Md'], $this->gatheredNames(['LOUD.TXT', 'Mixed.Md'])); + } + + public function testHonoursTheUsersCustomExtension(): void { + self::assertSame( + ['note.adoc', 'plain.txt'], + $this->gatheredNames(['note.adoc', 'plain.txt', 'other.rst'], 'adoc'), + ); + } + + public function testCollectsNotesFromEverySubfolder(): void { + self::assertSame( + ['deep.md', 'nested.txt', 'top.txt'], + $this->gatheredNames([ + 'top.txt', + 'Work' => [ + 'nested.txt', + 'Projects' => ['deep.md'], + ], + ]), + ); + } + + public function testHiddenNotesAreOnlyCollectedWhenRequested(): void { + self::assertSame(['shown.md'], $this->gatheredNames(['shown.md', '.hidden.md'])); + self::assertSame(['.hidden.md', 'shown.md'], $this->gatheredNames(['shown.md', '.hidden.md'], 'md', true)); + } + + public function testAttachmentFoldersAreNotCategories(): void { + $categories = $this->gather(['.attachments.42' => [], 'Work' => []], 'md', true)['categories']; + + self::assertSame(['Work'], array_values($categories)); + } + + public function testCollectsCategoriesIncludingFoldersWithoutNotes(): void { + $categories = $this->gather([ + 'loose.txt', + 'Work' => ['a.txt'], + 'Empty' => [], + ])['categories']; + + self::assertSame(['Work', 'Empty'], array_values($categories)); + } + + /** + * @return array + */ + public static function contents(): array { + return [ + 'plain first line' => ["Shopping\nmilk", 'Shopping'], + 'heading becomes title' => ["# Shopping\nmilk", 'Shopping'], + 'bullet becomes title' => ["- Shopping\n- milk", 'Shopping'], + 'bold is unwrapped' => ['**Shopping**', 'Shopping'], + 'empty falls back' => ['', 'New note'], + 'slash is removed' => ['a/b', 'ab'], + ]; + } + + #[DataProvider('contents')] + public function testGetTitleFromContent(string $content, string $expected): void { + self::assertSame($expected, $this->notesService->getTitleFromContent($content)); + } +} diff --git a/tests/unit/Service/UtilTest.php b/tests/unit/Service/UtilTest.php new file mode 100644 index 000000000..980c3b7bb --- /dev/null +++ b/tests/unit/Service/UtilTest.php @@ -0,0 +1,71 @@ +expectException(LockedException::class); + try { + Util::retryIfLocked(function () use (&$calls): void { + $calls++; + throw new LockedException('/Notes/a.txt'); + }, 3, 0); + } finally { + self::assertSame(3, $calls); + } + } + + public function testOtherExceptionsAreNotRetried(): void { + $calls = 0; + + $this->expectException(\RuntimeException::class); + try { + Util::retryIfLocked(function () use (&$calls): void { + $calls++; + throw new \RuntimeException('unrelated'); + }, 5, 0); + } finally { + self::assertSame(1, $calls); + } + } +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php new file mode 100644 index 000000000..7a2be096c --- /dev/null +++ b/tests/unit/bootstrap.php @@ -0,0 +1,31 @@ + + + + + + . + + + + + ../../lib + + + From cb5f909cdbcc82956fa5286aa3be38eef8d50c01 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 16 Sep 2026 19:34:37 +0200 Subject: [PATCH 2/4] fix(notes): keep nested categories when merging the folder walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gatherNoteFiles() merged the recursion's category list into the parent's with the `+` array union. Both operands are sequentially-keyed lists, so the union keeps the left-hand value for every index that is already occupied and discards the rest: a subcategory was dropped whenever the parent's list had already reached that index. Which ones were lost therefore depended on position rather than depth — for a folder 'Work' containing A, B and C, 'Work/A' collided with 'Work' at index 0 and vanished while its siblings survived. The visible effect is a nested folder without notes of its own missing from the sidebar, because the frontend derives categories from the notes themselves and only consults this list for folders that have none. The files union above it is keyed by file id, which is unique across the tree, so no note was ever lost. array_merge() appends instead of filling gaps, which is what the recursion needs. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger --- lib/Service/NotesService.php | 2 +- tests/unit/Service/NotesServiceTest.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/Service/NotesService.php b/lib/Service/NotesService.php index fda555e60..b172ff796 100644 --- a/lib/Service/NotesService.php +++ b/lib/Service/NotesService.php @@ -270,7 +270,7 @@ private static function gatherNoteFiles( $data['categories'][] = $subCategory; $data_sub = self::gatherNoteFiles($customExtension, $node, $showHidden, $subCategory . '/'); $data['files'] = $data['files'] + $data_sub['files']; - $data['categories'] = $data['categories'] + $data_sub['categories']; + $data['categories'] = array_merge($data['categories'], $data_sub['categories']); } elseif (self::isNote($node, $customExtension)) { $data['files'][$node->getId()] = $node; } diff --git a/tests/unit/Service/NotesServiceTest.php b/tests/unit/Service/NotesServiceTest.php index 92e300d85..0ca9f8311 100644 --- a/tests/unit/Service/NotesServiceTest.php +++ b/tests/unit/Service/NotesServiceTest.php @@ -152,6 +152,24 @@ public function testCollectsCategoriesIncludingFoldersWithoutNotes(): void { self::assertSame(['Work', 'Empty'], array_values($categories)); } + public function testCollectsNestedCategoriesAtEveryDepth(): void { + $categories = $this->gather([ + 'Work' => [ + 'Projects' => [ + '2026' => [], + ], + ], + 'Personal' => [ + 'Recipes' => [], + ], + ])['categories']; + + self::assertSame( + ['Work', 'Work/Projects', 'Work/Projects/2026', 'Personal', 'Personal/Recipes'], + array_values($categories), + ); + } + /** * @return array */ From b833573b5ea8aeef0970fc2cd16bb4493be7f1ae Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 16 Sep 2026 19:36:17 +0200 Subject: [PATCH 3/4] fix(notes): derive titles and excerpts by character, not by truthiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two title comparisons treated a string as a byte sequence or as a boolean, and both are wrong for real note content. getExcerpt() decided whether the content repeats the note's title by handing a character count to strncasecmp(), which counts bytes: $length = mb_strlen($title, 'utf-8'); // characters strncasecmp($excerpt, $title, $length) // bytes mb_substr($excerpt, $length, null, 'utf-8') // characters For an ASCII title the two agree. For a multi-byte one strncasecmp only ever compared the first few bytes, so content that merely started with the same character was mistaken for a repeated title and that many characters were cut off. A note titled 日本語 whose body is "日曜日 is Sunday" was excerpted as "is Sunday". Comparing a case-folded prefix of the same character length fixes it and keeps the case-insensitive match the byte version provided. getSafeTitle() guarded its fallback with empty(), and empty('0') is true in PHP, so a note whose first line was exactly "0" was titled "New note". Comparing against '' is what the guard meant. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger --- lib/Service/Note.php | 5 +++-- lib/Service/NoteUtil.php | 2 +- tests/unit/Service/NoteTest.php | 12 ++++++++++++ tests/unit/Service/NoteUtilTest.php | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/Service/Note.php b/lib/Service/Note.php index d01a8e2b3..5b5e4e14d 100644 --- a/lib/Service/Note.php +++ b/lib/Service/Note.php @@ -63,9 +63,10 @@ public function getContent() : string { public function getExcerpt(int $maxlen = 100) : string { $excerpt = trim($this->noteUtil->stripMarkdown($this->getContent())); $title = $this->getTitle(); - if (!empty($title)) { + if ($title !== '') { $length = mb_strlen($title, 'utf-8'); - if (strncasecmp($excerpt, $title, $length) === 0) { + $prefix = mb_substr($excerpt, 0, $length, 'utf-8'); + if (mb_strtolower($prefix, 'utf-8') === mb_strtolower($title, 'utf-8')) { $excerpt = mb_substr($excerpt, $length, null, 'utf-8'); } } diff --git a/lib/Service/NoteUtil.php b/lib/Service/NoteUtil.php index 3eaee14e7..5b4faf8ea 100644 --- a/lib/Service/NoteUtil.php +++ b/lib/Service/NoteUtil.php @@ -170,7 +170,7 @@ public function getSafeTitle(string $content) : string { $title = mb_substr($title, 0, self::MAX_TITLE_LENGTH, 'UTF-8'); // ensure that title is not empty - if (empty($title)) { + if ($title === '') { $title = $this->util->l10n->t('New note'); } diff --git a/tests/unit/Service/NoteTest.php b/tests/unit/Service/NoteTest.php index 66c058682..2485a5170 100644 --- a/tests/unit/Service/NoteTest.php +++ b/tests/unit/Service/NoteTest.php @@ -100,6 +100,18 @@ public function testExcerptStripsMarkdownAndFlattensNewlines(): void { self::assertSame("milk\u{2003}eggs", $note->getExcerpt()); } + public function testExcerptStripsARepeatedTitleCaseInsensitively(): void { + $note = $this->note(self::NOTES_PATH . '/Sunday.md', 'SUNDAYs are quiet'); + + self::assertSame('s are quiet', $note->getExcerpt()); + } + + public function testExcerptKeepsContentThatOnlySharesTheFirstCharacterOfAMultibyteTitle(): void { + $note = $this->note(self::NOTES_PATH . '/日本語.md', '日曜日 is Sunday'); + + self::assertSame('日曜日 is Sunday', $note->getExcerpt()); + } + public function testExcerptIsTruncatedWithAnEllipsis(): void { $excerpt = $this->note(self::NOTES_PATH . '/a.txt', str_repeat('x', 250))->getExcerpt(); diff --git a/tests/unit/Service/NoteUtilTest.php b/tests/unit/Service/NoteUtilTest.php index f5039e607..6428dace2 100644 --- a/tests/unit/Service/NoteUtilTest.php +++ b/tests/unit/Service/NoteUtilTest.php @@ -65,6 +65,7 @@ public static function titles(): array { 'tabs become spaces' => ["A\tB", 'A B'], 'empty falls back' => ['', 'New note'], 'nothing usable falls back' => ['///', 'New note'], + 'a bare zero is a title' => ['0', '0'], ]; } From 6ad562d6a607f7ba2873c063129c4d3119b762b5 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 17 Sep 2026 10:23:44 +0200 Subject: [PATCH 4/4] fix(lint): drop the blank line before the SettingsService class brace phpcs reports "The closing brace for the class must go on the next line after the body" for the stray blank line between the last method and the end of the class. It is the only phpcs error in the tree, so removing it makes `make lint` pass. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger --- lib/Service/SettingsService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 41d463d25..c5d7ac33c 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -267,5 +267,4 @@ private function get(string $uid, string $name, string $type, bool $saveInitial throw new \OCP\PreConditionNotMetException('Setting ' . $name . ' not found for user ' . $uid . '.'); } } - }